Skip to content

Use a jsonrpc library instead of rolling our own #32

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 29, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 0 additions & 170 deletions pyls/jsonrpc.py

This file was deleted.

4 changes: 2 additions & 2 deletions pyls/language_server.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Copyright 2017 Palantir Technologies, Inc.
import logging
import socketserver
from .jsonrpc import JSONRPCServer
from .server import JSONRPCServer
from .workspace import Workspace

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -46,7 +46,7 @@ def capabilities(self):
def publish_diagnostics(self, uri, diagnostics):
log.debug("Publishing diagnostics: %s", diagnostics)
params = {'uri': uri, 'diagnostics': diagnostics}
self.call(self.M_PUBLISH_DIAGNOSTICS, params)
self.notify(self.M_PUBLISH_DIAGNOSTICS, params)

def m_initialize(self, **kwargs):
log.debug("Language server intialized with %s", kwargs)
Expand Down
105 changes: 105 additions & 0 deletions pyls/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Copyright 2017 Palantir Technologies, Inc.
import json
import logging
import re
import socketserver
import jsonrpc

log = logging.getLogger(__name__)


class JSONRPCServer(socketserver.StreamRequestHandler, object):
""" Read/Write JSON RPC messages """

def __init__(self, rfile, wfile):
self.rfile = rfile
self.wfile = wfile

def shutdown(self):
# TODO: we should handle this much better
self.rfile.close()
self.wfile.close()

def handle(self):
# VSCode wants us to keep the connection open, so let's handle messages in a loop
while True:
try:
data = self._read_message()
response = jsonrpc.JSONRPCResponseManager.handle(data, self)
if response is not None:
self._write_message(response.data)
except Exception:
log.exception("Language server shutting down for uncaught exception")
break

def call(self, method, params=None):
""" Call a method on the client. TODO: return the result. """
req = jsonrpc.jsonrpc2.JSONRPC20Request(method=method, params=params)
self._write_message(req.data)

def notify(self, method, params=None):
""" Send a notification to the client, expects no response. """
req = jsonrpc.jsonrpc2.JSONRPC20Request(
method=method, params=params, is_notification=True
)
self._write_message(req.data)

def __getitem__(self, item):
# The jsonrpc dispatcher uses getitem to retrieve the RPC method implementation.
# We convert that to our own convention.
if not hasattr(self, _method_to_string(item)):
raise KeyError("Cannot find method %s" % item)
return getattr(self, _method_to_string(item))

def _content_length(self, line):
if line.startswith("Content-Length: "):
_, value = line.split("Content-Length: ")
value = value.strip()
try:
return int(value)
except ValueError:
raise ValueError("Invalid Content-Length header: {}".format(value))

def _read_message(self):
line = self.rfile.readline()

if not line:
raise EOFError()

content_length = self._content_length(line)

# Blindly consume all header lines
while line and line.strip():
line = self.rfile.readline()

if not line:
raise EOFError()

# Grab the body
return self.rfile.read(content_length)

def _write_message(self, msg):
body = json.dumps(msg, separators=(",", ":"))
content_length = len(body)
response = (
"Content-Length: {}\r\n"
"Content-Type: application/vscode-jsonrpc; charset=utf8\r\n\r\n"
"{}".format(content_length, body)
)
self.wfile.write(response)
self.wfile.flush()


_RE_FIRST_CAP = re.compile('(.)([A-Z][a-z]+)')
_RE_ALL_CAP = re.compile('([a-z0-9])([A-Z])')


def _method_to_string(method):
return "m_" + _camel_to_underscore(
method.replace("/", "__").replace("$", "")
)


def _camel_to_underscore(string):
s1 = _RE_FIRST_CAP.sub(r'\1_\2', string)
return _RE_ALL_CAP.sub(r'\1_\2', s1).lower()
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
configparser
future
jedi>=0.10
json-rpc
pycodestyle
pyflakes
yapf
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@
install_requires=[
'configparser',
'future',
'jedi>=0.10',
'json-rpc',
'pycodestyle',
'pyflakes',
'jedi>=0.10',
'yapf'
],

Expand Down
5 changes: 5 additions & 0 deletions test/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Copyright 2017 Palantir Technologies, Inc.
""" py.test configuration"""
import logging
from pyls.__main__ import LOG_FORMAT

logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT)


pytest_plugins = [
'test.fixtures'
Expand Down
28 changes: 21 additions & 7 deletions test/test_language_server.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# Copyright 2017 Palantir Technologies, Inc.
from threading import Thread
import json
import os
from threading import Thread

import jsonrpc
import pytest
from pyls.jsonrpc import JSONRPCServer

from pyls.server import JSONRPCServer
from pyls.language_server import start_io_lang_server
from pyls.python_ls import PythonLanguageServer

Expand Down Expand Up @@ -47,7 +51,7 @@ def test_initialize(client_server):
'rootPath': os.path.dirname(__file__),
'initializationOptions': {}
})
response = client._read_message()
response = _get_response(client)

assert 'capabilities' in response['result']

Expand All @@ -56,14 +60,14 @@ def test_file_closed(client_server):
client, server = client_server
client.rfile.close()
with pytest.raises(Exception):
client._read_message()
_get_response(client)


def test_missing_message(client_server):
client, server = client_server

client.call('unknown_method')
response = client._read_message()
response = _get_response(client)
assert response['error']['code'] == -32601 # Method not implemented error


Expand All @@ -76,15 +80,25 @@ def test_linting(client_server):
'rootPath': os.path.dirname(__file__),
'initializationOptions': {}
})
response = client._read_message()
response = _get_response(client)

assert 'capabilities' in response['result']

# didOpen
client.call('textDocument/didOpen', {
'textDocument': {'uri': 'file:///test', 'text': 'import sys'}
})
response = client._read_message()
response = _get_notification(client)

assert response['method'] == 'textDocument/publishDiagnostics'
assert len(response['params']['diagnostics']) > 0


def _get_notification(client):
request = jsonrpc.jsonrpc.JSONRPCRequest.from_json(client._read_message())
assert request.is_notification
return request.data


def _get_response(client):
return json.loads(client._read_message())
Loading