Repository: palantir/python-language-server
Pull request #30 (2017-03-29 20:36:54)

Revert "Use jsonrpc library to better support the JSONRPC protocol"
Reverts palantir/python-language-server#29


Patch:
diff --git a/pyls/jsonrpc.py b/pyls/jsonrpc.py
new file mode 100644
index 0000000..1a1e19d
--- /dev/null
+++ b/pyls/jsonrpc.py
@@ -0,0 +1,170 @@
+# Copyright 2017 Palantir Technologies, Inc.
+import json
+import logging
+import re
+import socketserver
+
+log = logging.getLogger(__name__)
+
+
+class JSONRPCError(Exception):
+
+    PARSE_ERROR = -32700
+    INVALID_REQUEST = -32600
+    METHOD_NOT_FOUND = -32601
+    INVALID_PARAMS = -32602
+    INTERNAL_ERROR = -32603
+    SERVER_ERROR_START = -32099
+    SERVER_ERROR_END = -32000
+
+    def __init__(self, code, message, data=None):
+        """ Init the JSONRPCError class
+
+        :param self: the instance
+        :param code: the error code
+        :type code: integer
+        """
+        self.data = data or {}
+        self.code = code
+        self.message = message
+
+    def to_rpc(self):
+        return {
+            'code': self.code,
+            'message': self.message,
+            'data': self.data
+        }
+
+
+class JSONRPCServer(socketserver.StreamRequestHandler, object):
+    """ Read/Write JSON RPC messages """
+
+    _msg_id = None
+
+    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:
+            # TODO: need to time out here eventually??
+            try:
+                self._handle_rpc_call()
+            except Exception:
+                break
+
+    def handle_json(self, msg):
+        # Convert JSONRPC methods to ones on this class
+        # e.g. textDocument/didOpen => m_text_document__did_open
+        method = "m_" + _method_to_string(msg['method'])
+
+        # If the message is not implemented, then say so
+        if not hasattr(self, method):
+            log.warning("Missing method %s", method)
+            raise JSONRPCError(JSONRPCError.METHOD_NOT_FOUND, msg['method'] + " not implemented")
+
+        log.debug("Got message: %s", msg)
+
+        # Else pass the message with params as kwargs
+        # Params may be a dictionary or undefined, so force them to be a dictionary
+        params = msg.get('params', {}) or {}
+        return getattr(self, method)(**params)
+
+    def call(self, method, params=None):
+        """ Call a remote method, for now we ignore the response... """
+        call = {
+            'jsonrpc': '2.0',
+            'method': method,
+            'params': params
+        }
+        self._write_message(call)
+
+    def _handle_rpc_call(self):
+        msg = self._read_message()
+
+        # Save the msg id to reply to
+        if msg.get('id') is not None:
+            self._msg_id = msg['id']
+
+        # Make sure we're running 2.0 protocol
+        assert msg['jsonrpc'] == '2.0'
+
+        # Default response
+        response = {'jsonrpc': '2.0', 'id': self._msg_id}
+
+        try:
+            reply = self.handle_json(msg)
+            if reply is None:
+                return
+            response['result'] = reply
+        except JSONRPCError as e:
+            log.error("Failed to process message with id %s: %s", self._msg_id, msg)
+            response['error'] = e.to_rpc()
+        except Exception as e:
+            log.exception("Caught internal error")
+            err = JSONRPCError(JSONRPCError.INTERNAL_ERROR, str(e))
+            response['error'] = err.to_rpc()
+
+        log.debug("Responding to msg %s with %s", self._msg_id, response)
+        self._write_message(response)
+
+    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 and handle it
+        body = self.rfile.read(content_length)
+
+        return json.loads(body)
+
+    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 _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()
diff --git a/pyls/language_server.py b/pyls/language_server.py
index 37307ca..0863b15 100644
--- a/pyls/language_server.py
+++ b/pyls/language_server.py
@@ -1,7 +1,7 @@
 # Copyright 2017 Palantir Technologies, Inc.
 import logging
 import socketserver
-from .server import JSONRPCServer
+from .jsonrpc import JSONRPCServer
 from .workspace import Workspace
 
 log = logging.getLogger(__name__)
diff --git a/pyls/server.py b/pyls/server.py
deleted file mode 100644
index 20a9737..0000000
--- a/pyls/server.py
+++ /dev/null
@@ -1,105 +0,0 @@
-# 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()
diff --git a/requirements.txt b/requirements.txt
index 0e01002..071f331 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,6 @@
 configparser
 future
 jedi>=0.10
-json-rpc
 pycodestyle
 pyflakes
 yapf
diff --git a/setup.py b/setup.py
index b255a68..5780e32 100755
--- a/setup.py
+++ b/setup.py
@@ -34,10 +34,9 @@ setup(
     install_requires=[
         'configparser',
         'future',
-        'jedi>=0.10',
-        'json-rpc',
         'pycodestyle',
         'pyflakes',
+        'jedi>=0.10',
         'yapf'
     ],
 
diff --git a/tox.ini b/tox.ini
index a64a02f..9d4952e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,7 +4,7 @@
 # and then run "tox" from this directory.
 
 [tox]
-envlist = py27,py36,lint
+envlist = py27,py36
 
 [pycodestyle]
 ignore = E226
@@ -18,13 +18,10 @@ addopts =
 
 [testenv]
 commands =
-    py.test {posargs}
+    pycodestyle pyls
+    pyflakes pyls
+    py.test
 deps =
     pytest
     coverage
     pytest-cov
-
-[testenv:lint]
-commands =
-    pycodestyle pyls
-    pyflakes pyls


Test patch:
diff --git a/test/conftest.py b/test/conftest.py
index 59542dd..29a1482 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -1,10 +1,5 @@
 # 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'
diff --git a/test/test_language_server.py b/test/test_language_server.py
index 8994f87..26b3235 100644
--- a/test/test_language_server.py
+++ b/test/test_language_server.py
@@ -1,12 +1,8 @@
 # Copyright 2017 Palantir Technologies, Inc.
-import json
-import os
 from threading import Thread
-
-import jsonrpc
+import os
 import pytest
-
-from pyls.server import JSONRPCServer
+from pyls.jsonrpc import JSONRPCServer
 from pyls.language_server import start_io_lang_server
 from pyls.python_ls import PythonLanguageServer
 
@@ -51,7 +47,7 @@ def test_initialize(client_server):
         'rootPath': os.path.dirname(__file__),
         'initializationOptions': {}
     })
-    response = _next_message(client)
+    response = client._read_message()
 
     assert 'capabilities' in response['result']
 
@@ -60,14 +56,14 @@ def test_file_closed(client_server):
     client, server = client_server
     client.rfile.close()
     with pytest.raises(Exception):
-        _next_message(client)
+        client._read_message()
 
 
 def test_missing_message(client_server):
     client, server = client_server
 
     client.call('unknown_method')
-    response = _next_message(client)
+    response = client._read_message()
     assert response['error']['code'] == -32601  # Method not implemented error
 
 
@@ -80,7 +76,7 @@ def test_linting(client_server):
         'rootPath': os.path.dirname(__file__),
         'initializationOptions': {}
     })
-    response = _next_message(client)
+    response = client._read_message()
 
     assert 'capabilities' in response['result']
 
@@ -88,11 +84,7 @@ def test_linting(client_server):
     client.call('textDocument/didOpen', {
         'textDocument': {'uri': 'file:///test', 'text': 'import sys'}
     })
-    response = _next_message(client)
+    response = client._read_message()
 
     assert response['method'] == 'textDocument/publishDiagnostics'
     assert len(response['params']['diagnostics']) > 0
-
-
-def _next_message(client):
-    return json.loads(client._read_message())