From 8b705bca0acd3de08fdb1b5c869ca5e0d7080585 Mon Sep 17 00:00:00 2001 From: Felipe Orozco Date: Tue, 20 Feb 2018 11:57:10 +0000 Subject: [PATCH] Add support for asynchronously handling requests (#261) --- pyls/__main__.py | 8 +- pyls/dispatcher.py | 30 ----- pyls/json_rpc_server.py | 138 ++++++++++++++++++++++ pyls/language_server.py | 95 ---------------- pyls/python_ls.py | 144 ++++++++++++++++++----- pyls/rpc_manager.py | 189 +++++++++++++++++++++++++++++++ pyls/server.py | 127 --------------------- pyls/workspace.py | 12 +- setup.py | 3 +- test/fixtures.py | 63 +++++++++-- test/plugins/test_completion.py | 4 +- test/plugins/test_definitions.py | 8 +- test/plugins/test_format.py | 6 +- test/plugins/test_references.py | 2 +- test/plugins/test_signature.py | 2 +- test/test_dispatcher.py | 21 ---- test/test_document.py | 15 +-- test/test_json_rpc_server.py | 77 +++++++++++++ test/test_language_server.py | 94 +++++---------- test/test_rpc_manager.py | 178 +++++++++++++++++++++++++++++ test/test_uris.py | 2 +- tox.ini | 8 +- vscode-client/src/extension.ts | 2 +- 23 files changed, 812 insertions(+), 416 deletions(-) delete mode 100644 pyls/dispatcher.py create mode 100644 pyls/json_rpc_server.py delete mode 100644 pyls/language_server.py create mode 100644 pyls/rpc_manager.py delete mode 100644 pyls/server.py delete mode 100644 test/test_dispatcher.py create mode 100644 test/test_json_rpc_server.py create mode 100644 test/test_rpc_manager.py diff --git a/pyls/__main__.py b/pyls/__main__.py index a5ea1d89..16bcd4fd 100644 --- a/pyls/__main__.py +++ b/pyls/__main__.py @@ -4,9 +4,7 @@ import logging import logging.config import sys - -from . import language_server -from .python_ls import PythonLanguageServer +from .python_ls import start_io_lang_server, start_tcp_lang_server, PythonLanguageServer LOG_FORMAT = "%(asctime)s UTC - %(levelname)s - %(name)s - %(message)s" @@ -51,10 +49,10 @@ def main(): _configure_logger(args.verbose, args.log_config, args.log_file) if args.tcp: - language_server.start_tcp_lang_server(args.host, args.port, PythonLanguageServer) + start_tcp_lang_server(args.host, args.port, PythonLanguageServer) else: stdin, stdout = _binary_stdio() - language_server.start_io_lang_server(stdin, stdout, PythonLanguageServer) + start_io_lang_server(stdin, stdout, PythonLanguageServer) def _binary_stdio(): diff --git a/pyls/dispatcher.py b/pyls/dispatcher.py deleted file mode 100644 index b6a1c376..00000000 --- a/pyls/dispatcher.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2017 Palantir Technologies, Inc. -import re - -_RE_FIRST_CAP = re.compile('(.)([A-Z][a-z]+)') -_RE_ALL_CAP = re.compile('([a-z0-9])([A-Z])') - - -class JSONRPCMethodDispatcher(object): - """JSON RPC method dispatcher that calls methods on itself with params.""" - - def __getitem__(self, item): - """The jsonrpc dispatcher uses getitem to retrieve the RPC method implementation.""" - method_name = "m_" + _method_to_string(item) - if not hasattr(self, method_name): - raise KeyError("Cannot find method %s" % method_name) - func = getattr(self, method_name) - - def wrapped(*args, **kwargs): - return func(*args, **kwargs) - - return wrapped - - -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/json_rpc_server.py b/pyls/json_rpc_server.py new file mode 100644 index 00000000..8c0ad25f --- /dev/null +++ b/pyls/json_rpc_server.py @@ -0,0 +1,138 @@ +# Copyright 2017 Palantir Technologies, Inc. +import json +import logging +import threading + +from jsonrpc.jsonrpc2 import JSONRPC20Response, JSONRPC20BatchRequest, JSONRPC20BatchResponse +from jsonrpc.jsonrpc import JSONRPCRequest +from jsonrpc.exceptions import JSONRPCInvalidRequestException + +log = logging.getLogger(__name__) + + +class JSONRPCServer(object): + """ Read/Write JSON RPC messages """ + + def __init__(self, rfile, wfile): + self.pending_request = {} + self.rfile = rfile + self.wfile = wfile + self.write_lock = threading.Lock() + + def close(self): + with self.write_lock: + self.wfile.close() + self.rfile.close() + + def get_messages(self): + """Generator that produces well structured JSON RPC message. + + Yields: + message: received message + + Note: + This method is not thread safe and should only invoked from a single thread + """ + while not self.rfile.closed: + request_str = self._read_message() + + if request_str is None: + break + if isinstance(request_str, bytes): + request_str = request_str.decode("utf-8") + + try: + try: + message_blob = json.loads(request_str) + request = JSONRPCRequest.from_data(message_blob) + if isinstance(request, JSONRPC20BatchRequest): + self._add_batch_request(request) + messages = request + else: + messages = [request] + except JSONRPCInvalidRequestException: + # work around where JSONRPC20Reponse expects _id key + message_blob['_id'] = message_blob['id'] + # we do not send out batch requests so no need to support batch responses + messages = [JSONRPC20Response(**message_blob)] + except (KeyError, ValueError): + log.exception("Could not parse message %s", request_str) + continue + + for message in messages: + yield message + + def write_message(self, message): + """ Write message to out file descriptor. + + Args: + message (JSONRPCRequest, JSONRPCResponse): body of the message to send + """ + with self.write_lock: + if self.wfile.closed: + return + elif isinstance(message, JSONRPC20Response) and message._id in self.pending_request: + batch_response = self.pending_request[message._id](message) + if batch_response is not None: + message = batch_response + + log.debug("Sending %s", message) + body = message.json + 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.encode('utf-8')) + self.wfile.flush() + + def _read_message(self): + """Reads the contents of a message. + + Returns: + body of message if parsable else None + """ + line = self.rfile.readline() + + if not line: + return None + + content_length = _content_length(line) + + # Blindly consume all header lines + while line and line.strip(): + line = self.rfile.readline() + + if not line: + return None + + # Grab the body + return self.rfile.read(content_length) + + def _add_batch_request(self, requests): + pending_requests = [request for request in requests if not request.is_notification] + if not pending_requests: + return + + batch_request = {'pending': len(pending_requests), 'resolved': []} + for request in pending_requests: + def cleanup_message(response): + batch_request['pending'] -= 1 + batch_request['resolved'].append(response) + del self.pending_request[request._id] + return JSONRPC20BatchResponse(batch_request['resolved']) if batch_request['pending'] == 0 else None + self.pending_request[request._id] = cleanup_message + + +def _content_length(line): + """Extract the content length from an input line.""" + if line.startswith(b'Content-Length: '): + _, value = line.split(b'Content-Length: ') + value = value.strip() + try: + return int(value) + except ValueError: + raise ValueError("Invalid Content-Length header: {}".format(value)) + + return None diff --git a/pyls/language_server.py b/pyls/language_server.py deleted file mode 100644 index f3101820..00000000 --- a/pyls/language_server.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright 2017 Palantir Technologies, Inc. -import logging -import socketserver -from . import dispatcher, uris -from .server import JSONRPCServer - -log = logging.getLogger(__name__) - - -class _StreamHandlerWrapper(socketserver.StreamRequestHandler, object): - """A wrapper class that is used to construct a custom handler class.""" - - delegate = None - - def setup(self): - super(_StreamHandlerWrapper, self).setup() - # pylint: disable=no-member - self.delegate = self.DELEGATE_CLASS(self.rfile, self.wfile) - - def handle(self): - self.delegate.handle() - - -def start_tcp_lang_server(bind_addr, port, handler_class): - if not issubclass(handler_class, JSONRPCServer): - raise ValueError("Handler class must be a subclass of JSONRPCServer") - - # Construct a custom wrapper class around the user's handler_class - wrapper_class = type( - handler_class.__name__ + "Handler", - (_StreamHandlerWrapper,), - {'DELEGATE_CLASS': handler_class} - ) - - server = socketserver.ThreadingTCPServer((bind_addr, port), wrapper_class) - try: - log.info("Serving %s on (%s, %s)", handler_class.__name__, bind_addr, port) - server.serve_forever() - finally: - log.info("Shutting down") - server.server_close() - - -def start_io_lang_server(rfile, wfile, handler_class): - if not issubclass(handler_class, JSONRPCServer): - raise ValueError("Handler class must be a subclass of JSONRPCServer") - log.info("Starting %s IO language server", handler_class.__name__) - server = handler_class(rfile, wfile) - server.handle() - - -class LanguageServer(dispatcher.JSONRPCMethodDispatcher, JSONRPCServer): - """ Implementation of the Microsoft VSCode Language Server Protocol - https://github.com/Microsoft/language-server-protocol/blob/master/versions/protocol-1-x.md - """ - - process_id = None - root_uri = None - init_opts = None - - def capabilities(self): # pylint: disable=no-self-use - return {} - - def initialize(self, root_uri, init_opts, process_id): - pass - - def m_initialize(self, **kwargs): - log.debug("Language server initialized with %s", kwargs) - if 'rootUri' in kwargs: - self.root_uri = kwargs['rootUri'] - elif 'rootPath' in kwargs: - root_path = kwargs['rootPath'] - self.root_uri = uris.from_fs_path(root_path) - else: - self.root_uri = '' - self.init_opts = kwargs.get('initializationOptions') - self.process_id = kwargs.get('processId') - - self.initialize(self.root_uri, self.init_opts, self.process_id) - - # Get our capabilities - return {'capabilities': self.capabilities()} - - def m___cancel_request(self, **kwargs): - # TODO: We could I suppose launch tasks in their own threads and kill - # them on cancel, but is it really worth the effort given most methods - # are reasonably quick? - # This tends to happen when cancelling a hover request - pass - - def m_shutdown(self, **_kwargs): - self.shutdown() - - def m_exit(self, **_kwargs): - self.exit() diff --git a/pyls/python_ls.py b/pyls/python_ls.py index a966156c..61de3569 100644 --- a/pyls/python_ls.py +++ b/pyls/python_ls.py @@ -1,44 +1,120 @@ # Copyright 2017 Palantir Technologies, Inc. import logging -from . import lsp, _utils +import socketserver +import re + +from . import lsp, _utils, uris from .config import config -from .language_server import LanguageServer +from .json_rpc_server import JSONRPCServer +from .rpc_manager import JSONRPCManager, MissingMethodException from .workspace import Workspace log = logging.getLogger(__name__) +_RE_FIRST_CAP = re.compile('(.)([A-Z][a-z]+)') +_RE_ALL_CAP = re.compile('([a-z0-9])([A-Z])') + LINT_DEBOUNCE_S = 0.5 # 500 ms -class PythonLanguageServer(LanguageServer): +class _StreamHandlerWrapper(socketserver.StreamRequestHandler, object): + """A wrapper class that is used to construct a custom handler class.""" + + delegate = None + + def setup(self): + super(_StreamHandlerWrapper, self).setup() + # pylint: disable=no-member + self.delegate = self.DELEGATE_CLASS(self.rfile, self.wfile) + + def handle(self): + self.delegate.handle() + + +def start_tcp_lang_server(bind_addr, port, handler_class): + if not isinstance(handler_class, PythonLanguageServer): + raise ValueError('Handler class must be an instance of PythonLanguageServer') + + # Construct a custom wrapper class around the user's handler_class + wrapper_class = type( + handler_class.__name__ + 'Handler', + (_StreamHandlerWrapper,), + {'DELEGATE_CLASS': handler_class} + ) + + server = socketserver.TCPServer((bind_addr, port), wrapper_class) + try: + log.info('Serving %s on (%s, %s)', handler_class.__name__, bind_addr, port) + server.serve_forever() + finally: + log.info('Shutting down') + server.server_close() + + +def start_io_lang_server(rfile, wfile, handler_class): + if not issubclass(handler_class, PythonLanguageServer): + raise ValueError('Handler class must be an instance of PythonLanguageServer') + log.info('Starting %s IO language server', handler_class.__name__) + server = handler_class(rfile, wfile) + server.start() + + +class PythonLanguageServer(object): + """ Implementation of the Microsoft VSCode Language Server Protocol + https://github.com/Microsoft/language-server-protocol/blob/master/versions/protocol-1-x.md + """ + # pylint: disable=too-many-public-methods,redefined-builtin - workspace = None - config = None + def __init__(self, rx, tx): + self.rpc_manager = JSONRPCManager(JSONRPCServer(rx, tx), self.handle_request) + self.workspace = None + self.config = None + self._dispatchers = [] - # Set of method dispatchers to query - _dispatchers = [] + def start(self): + """Entry point for the server""" + self.rpc_manager.start() - def __getitem__(self, item): - """Override the method dispatcher to farm out any unknown messages to our plugins.""" - try: - return super(PythonLanguageServer, self).__getitem__(item) - except KeyError: - log.debug("Checking dispatchers for %s: %s", item, self._dispatchers) + def handle_request(self, method, params): + """Provides callables to handle requests or responses to those reqeuests + + Args: + method (str): name of the message + params (dict): body of the message + + Returns: + Callable if method is to be handled + + Raises: + KeyError: Handler for method is not found + """ + + method_call = 'm_{}'.format(_method_to_string(method)) + if hasattr(self, method_call): + return getattr(self, method_call)(**params) + elif self._dispatchers: for dispatcher in self._dispatchers: - try: - return dispatcher.__getitem__(item) - except KeyError: - pass - raise KeyError("Unknown item %s" % item) + if method_call in dispatcher: + return dispatcher[method_call](**params) + + raise MissingMethodException('No handler for for method {}'.format(method)) + + def m__cancel_request(self, **kwargs): + self.rpc_manager.cancel(kwargs['id']) + + def m_shutdown(self, **_kwargs): + self.rpc_manager.shutdown() + return None - def _hook_caller(self, hook_name): - return self.config.plugin_manager.subset_hook_caller(hook_name, self.config.disabled_plugins) + def m_exit(self, **_kwargs): + self.rpc_manager.exit() def _hook(self, hook_name, doc_uri=None, **kwargs): + """Calls hook_name and returns a list of results from all registered handlers""" doc = self.workspace.get_document(doc_uri) if doc_uri else None - hook = self.config.plugin_manager.subset_hook_caller(hook_name, self.config.disabled_plugins) - return hook(config=self.config, workspace=self.workspace, document=doc, **kwargs) + hook_handlers = self.config.plugin_manager.subset_hook_caller(hook_name, self.config.disabled_plugins) + return hook_handlers(config=self.config, workspace=self.workspace, document=doc, **kwargs) def capabilities(self): server_capabilities = { @@ -66,15 +142,22 @@ def capabilities(self): 'textDocumentSync': lsp.TextDocumentSyncKind.INCREMENTAL, 'experimental': merge(self._hook('pyls_experimental_capabilities')) } - log.info("Server capabilities: %s", server_capabilities) + log.info('Server capabilities: %s', server_capabilities) return server_capabilities - def initialize(self, root_uri, init_opts, _process_id): - self.workspace = Workspace(root_uri, lang_server=self) - self.config = config.Config(root_uri, init_opts) + def m_initialize(self, processId=None, rootUri=None, rootPath=None, initializationOptions=None, **_kwargs): + log.debug('Language server initialized with %s %s %s %s', processId, rootUri, rootPath, initializationOptions) + if rootUri is None: + rootUri = uris.from_fs_path(rootPath) if rootPath is not None else '' + + self.workspace = Workspace(rootUri, self.rpc_manager) + self.config = config.Config(rootUri, initializationOptions or {}) self._dispatchers = self._hook('pyls_dispatchers') self._hook('pyls_initialize') + # Get our capabilities + return {'capabilities': self.capabilities()} + def code_actions(self, doc_uri, range, context): return flatten(self._hook('pyls_code_actions', doc_uri, range=range, context=context)) @@ -194,6 +277,15 @@ def m_workspace__execute_command(self, command=None, arguments=None): return self.execute_command(command, arguments) +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() + + def flatten(list_of_lists): return [item for lst in list_of_lists for item in lst] diff --git a/pyls/rpc_manager.py b/pyls/rpc_manager.py new file mode 100644 index 00000000..62211e7e --- /dev/null +++ b/pyls/rpc_manager.py @@ -0,0 +1,189 @@ +# Copyright 2017 Palantir Technologies, Inc. +import logging +from uuid import uuid4 + +from concurrent.futures import ThreadPoolExecutor, Future +from jsonrpc.base import JSONRPCBaseResponse +from jsonrpc.jsonrpc1 import JSONRPC10Response +from jsonrpc.jsonrpc2 import JSONRPC20Response, JSONRPC20Request +from jsonrpc.exceptions import JSONRPCMethodNotFound, JSONRPCDispatchException, JSONRPCServerError + +log = logging.getLogger(__name__) + +RESPONSE_CLASS_MAP = { + "1.0": JSONRPC10Response, + "2.0": JSONRPC20Response +} + + +class MissingMethodException(Exception): + pass + + +class JSONRPCManager(object): + + def __init__(self, message_manager, message_handler): + self._message_manager = message_manager + self._message_handler = message_handler + self._shutdown = False + self._sent_requests = {} + self._received_requests = {} + self._executor_service = ThreadPoolExecutor(max_workers=5) # arbitrary pool size + + def start(self): + """Start reading JSONRPC messages off of rx""" + self.consume_requests() + + def shutdown(self): + """Set flag to ignore all non exit messages""" + self._shutdown = True + + def exit(self): + """Stop listening for new message""" + self._executor_service.shutdown() + self._message_manager.close() + + def call(self, method, params=None): + """Send a JSONRPC request. + + Args: + method (str): The method name of the message to send + params (dict): The payload of the message + + Returns: + Future that will resolve once a response has been received + """ + log.debug('Calling %s %s', method, params) + request = JSONRPC20Request(_id=uuid4().int, method=method, params=params) + request_future = Future() + self._sent_requests[request._id] = request_future + self._message_manager.write_message(request) + return request_future + + def notify(self, method, params=None): + """Send a JSONRPC notification. + + Args: + method (str): The method name of the notification to send + params (dict): The payload of the notification + """ + log.debug('Notify %s %s', method, params) + notification = JSONRPC20Request(method=method, params=params, is_notification=True) + self._message_manager.write_message(notification) + + def cancel(self, request_id): + """Cancel pending request handler. + + Args: + request_id (string | number): The id of the original request + + Note: + Request will only be cancelled if it has not begun execution. + """ + log.debug('Cancel request %d', request_id) + try: + self._received_requests[request_id].cancel() + except KeyError: + log.debug('Received cancel for finished/nonexistent request %d', request_id) + + def consume_requests(self): + """ Infinite loop watching for messages from the client.""" + for message in self._message_manager.get_messages(): + if isinstance(message, JSONRPCBaseResponse): + self._handle_response(message) + else: + self._handle_request(message) + + def _handle_request(self, request): + """Execute corresponding handler for the recieved request + + Args: + request (JSONRPCBaseRequest): Request to act upon + + Note: + Requests are handled asynchronously if the handler returns a callable, otherwise they are handle + synchronously by the main thread + """ + if self._shutdown and request.method != 'exit': + return + + output = None + try: + maybe_handler = self._message_handler(request.method, request.params if request.params is not None else {}) + except MissingMethodException as e: + log.debug(e) + # Do not need to notify client of failure with notifications + output = JSONRPC20Response(_id=request._id, error=JSONRPCMethodNotFound()._data) + except JSONRPCDispatchException as e: + output = _make_response(request, error=e.error._data) + except Exception as e: # pylint: disable=broad-except + log.exception('synchronous method handler exception') + output = _make_response(request, error=JSONRPCServerError()._data) + else: + if request._id in self._received_requests: + log.error('Received request %s with duplicate id', request.data) + elif callable(maybe_handler): + log.debug('Async request %s', request._id) + self._handle_async_request(request, maybe_handler) + else: + output = _make_response(request, result=maybe_handler) + finally: + if not request.is_notification and output is not None: + log.debug('Sync request %s', request._id) + self._message_manager.write_message(output) + + def _handle_async_request(self, request, handler): + future = self._executor_service.submit(handler) + + if request.is_notification: + return + + def did_finish_callback(completed_future): + del self._received_requests[request._id] + if completed_future.cancelled(): + log.debug('Cleared cancelled request %d', request._id) + else: + try: + result = completed_future.result() + except JSONRPCDispatchException as e: + output = _make_response(request, error=e.error._data) + except Exception as e: # pylint: disable=broad-except + # TODO(forozco): add more descriptive error + log.exception('asynchronous method handler exception') + output = _make_response(request, error=JSONRPCServerError()._data) + else: + output = _make_response(request, result=result) + finally: + self._message_manager.write_message(output) + + self._received_requests[request._id] = future + future.add_done_callback(did_finish_callback) + + def _handle_response(self, response): + """Handle the response to requests sent from the server to the client. + + Args: + response: (JSONRPC20Response): Received response + + """ + try: + request = self._sent_requests[response._id] + except KeyError: + log.error('Received unexpected response %s', response.data) + else: + log.debug("Received response %s", response.data) + + def cleanup(_): + del self._sent_requests[response._id] + request.add_done_callback(cleanup) + + if 'result' in response.data: + request.set_result(response.result) + else: + request.set_exception(JSONRPCDispatchException(**response.error)) + + +def _make_response(request, **kwargs): + response = RESPONSE_CLASS_MAP[request.JSONRPC_VERSION](_id=request._id, **kwargs) + response.request = request + return response diff --git a/pyls/server.py b/pyls/server.py deleted file mode 100644 index fb7069c5..00000000 --- a/pyls/server.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright 2017 Palantir Technologies, Inc. -import json -import logging -import uuid - -from jsonrpc import jsonrpc2, JSONRPCResponseManager - -log = logging.getLogger(__name__) - - -class JSONRPCServer(object): - """ Read/Write JSON RPC messages """ - - def __init__(self, rfile, wfile): - self.rfile = rfile - self.wfile = wfile - - self._callbacks = {} - self._shutdown = False - - def exit(self): - # Exit causes a complete exit of the server - self.rfile.close() - self.wfile.close() - - def shutdown(self): - # Shutdown signals the server to stop, but not exit - self._shutdown = True - log.debug("Server shut down, awaiting exit notification") - - def handle(self): - while True: - try: - data = self._read_message() - log.debug("Got message: %s", data) - - if self._shutdown: - # Handle only the exit notification when we're shut down - JSONRPCResponseManager.handle(data, {'exit': self.exit}) - break - - if isinstance(data, bytes): - data = data.decode("utf-8") - - msg = json.loads(data) - if 'method' in msg: - # It's a notification or request - # Dispatch to the thread pool for handling - response = JSONRPCResponseManager.handle(data, self) - if response is not None: - self._write_message(response.data) - else: - # Otherwise, it's a response message - on_result, on_error = self._callbacks.pop(msg['id']) - if 'result' in msg and on_result: - on_result(msg['result']) - elif 'error' in msg and on_error: - on_error(msg['error']) - except: # pylint: disable=bare-except - log.exception("Language server exiting due to uncaught exception") - break - - def call(self, method, params=None, on_result=None, on_error=None): - """Call a method on the client.""" - msg_id = str(uuid.uuid4()) - log.debug("Sending request %s: %s: %s", msg_id, method, params) - req = jsonrpc2.JSONRPC20Request(method=method, params=params) - req._id = msg_id - - def _default_on_error(error): - log.error("Call to %s failed with %s", method, error) - - if not on_error: - on_error = _default_on_error - - self._callbacks[msg_id] = (on_result, on_error) - self._write_message(req.data) - - def notify(self, method, params=None): - """ Send a notification to the client, expects no response. """ - log.debug("Sending notification %s: %s", method, params) - req = jsonrpc2.JSONRPC20Request( - method=method, params=params, is_notification=True - ) - self._write_message(req.data) - - def _read_message(self): - line = self.rfile.readline() - - if not line: - raise EOFError() - - content_length = _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.encode('utf-8')) - self.wfile.flush() - - -def _content_length(line): - """Extract the content length from an input line.""" - if line.startswith(b'Content-Length: '): - _, value = line.split(b'Content-Length: ') - value = value.strip() - try: - return int(value) - except ValueError: - raise ValueError("Invalid Content-Length header: {}".format(value)) - - return None diff --git a/pyls/workspace.py b/pyls/workspace.py index fd9374da..992c9b63 100644 --- a/pyls/workspace.py +++ b/pyls/workspace.py @@ -74,12 +74,12 @@ class Workspace(object): M_SHOW_MESSAGE = 'window/showMessage' PRELOADED_MODULES = get_preferred_submodules() - def __init__(self, root_uri, lang_server=None): + def __init__(self, root_uri, rpc_manager): self._root_uri = root_uri + self._rpc_manager = rpc_manager self._root_uri_scheme = uris.urlparse(self._root_uri)[0] self._root_path = uris.to_fs_path(self._root_uri) self._docs = {} - self._lang_server = lang_server # Whilst incubating, keep private self.__rope = Project(self._root_path) @@ -124,18 +124,16 @@ def update_document(self, doc_uri, change, version=None): self._docs[doc_uri].version = version def apply_edit(self, edit, on_result=None, on_error=None): - return self._lang_server.call( + return self._rpc_manager.call( self.M_APPLY_EDIT, {'edit': edit}, on_result=on_result, on_error=on_error ) def publish_diagnostics(self, doc_uri, diagnostics): - params = {'uri': doc_uri, 'diagnostics': diagnostics} - self._lang_server.notify(self.M_PUBLISH_DIAGNOSTICS, params) + self._rpc_manager.notify(self.M_PUBLISH_DIAGNOSTICS, params={'uri': doc_uri, 'diagnostics': diagnostics}) def show_message(self, message, msg_type=lsp.MessageType.Info): - params = {'type': msg_type, 'message': message} - self._lang_server.notify(self.M_SHOW_MESSAGE, params) + self._rpc_manager.notify(self.M_SHOW_MESSAGE, params={'type': msg_type, 'message': message}) def source_roots(self, document_path): """Return the source roots for the given document.""" diff --git a/setup.py b/setup.py index dd2170af..e3860de6 100755 --- a/setup.py +++ b/setup.py @@ -34,6 +34,7 @@ install_requires=[ 'configparser', 'future>=0.14.0', + 'futures; python_version == "2.7"', 'jedi>=0.10', 'json-rpc', 'mccabe', @@ -50,7 +51,7 @@ # for example: # $ pip install -e .[test] extras_require={ - 'test': ['tox', 'versioneer', 'pytest', 'pytest-cov', 'coverage'], + 'test': ['tox', 'versioneer', 'pytest', 'mock', 'pytest-cov', 'coverage'], }, # To provide executable scripts, use entry points in preference to the diff --git a/test/fixtures.py b/test/fixtures.py index d25f191f..5a43de0d 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -1,18 +1,37 @@ # Copyright 2017 Palantir Technologies, Inc. +import os +import sys +from mock import Mock import pytest +from jsonrpc.jsonrpc2 import JSONRPC20Response, JSONRPC20Request + from pyls import uris from pyls.config.config import Config from pyls.python_ls import PythonLanguageServer -from pyls.workspace import Workspace -from io import StringIO +from pyls.rpc_manager import JSONRPCManager +from pyls.workspace import Workspace, Document +from pyls.json_rpc_server import JSONRPCServer + +if sys.version_info[0] < 3: + from StringIO import StringIO +else: + from io import StringIO + +BASE_HANDLED_RESPONSE_CONTENT = 'handled' +BASE_HANDLED_RESPONSE = JSONRPC20Response(_id=1, result=BASE_HANDLED_RESPONSE_CONTENT) + +DOC_URI = uris.from_fs_path(__file__) +DOC = """import sys + +def main(): + print sys.stdin.read() +""" @pytest.fixture def pyls(tmpdir): """ Return an initialized python LS """ - rfile = StringIO() - wfile = StringIO() - ls = PythonLanguageServer(rfile, wfile) + ls = PythonLanguageServer(StringIO, StringIO) ls.m_initialize( processId=1, @@ -26,10 +45,40 @@ def pyls(tmpdir): @pytest.fixture def workspace(tmpdir): """Return a workspace.""" - return Workspace(uris.from_fs_path(str(tmpdir))) + return Workspace(uris.from_fs_path(str(tmpdir)), Mock()) + + +@pytest.fixture +def rpc_management(): + message_manager = Mock(**{'get_messages.return_value': [JSONRPC20Request(_id=1, method='test', params={})]}) + message_handler = Mock(return_value=BASE_HANDLED_RESPONSE_CONTENT) + rpc_manager = JSONRPCManager(message_manager, message_handler) + + yield rpc_manager, message_manager, message_handler, + + rpc_manager.exit() @pytest.fixture -def config(workspace): +def json_rpc_server(): + manager_rx, tester_tx = os.pipe() + tester_rx, manager_tx = os.pipe() + + client = JSONRPCServer(os.fdopen(manager_rx, 'rb'), os.fdopen(manager_tx, 'wb')) + server = JSONRPCServer(os.fdopen(tester_rx, 'rb'), os.fdopen(tester_tx, 'wb')) + + yield client, server + + client.close() + server.close() + + +@pytest.fixture +def config(workspace): # pylint: disable=redefined-outer-name """Return a config object.""" return Config(workspace.root_uri, {}) + + +@pytest.fixture +def doc(): + return Document(DOC_URI, DOC) diff --git a/test/plugins/test_completion.py b/test/plugins/test_completion.py index eb716e96..2e5c764b 100644 --- a/test/plugins/test_completion.py +++ b/test/plugins/test_completion.py @@ -37,7 +37,7 @@ def test_jedi_completion(): doc = Document(DOC_URI, DOC) items = pyls_jedi_completions(doc, com_position) - assert len(items) > 0 + assert items assert items[0]['label'] == 'isabs(s)' # Test we don't throw with big character @@ -52,7 +52,7 @@ def test_rope_completion(): doc = Document(DOC_URI, DOC, rope=rope) items = pyls_rope_completions(doc, com_position) - assert len(items) > 0 + assert items assert items[0]['label'] == 'isabs' diff --git a/test/plugins/test_definitions.py b/test/plugins/test_definitions.py index 4c937ad2..c360ac44 100644 --- a/test/plugins/test_definitions.py +++ b/test/plugins/test_definitions.py @@ -24,13 +24,13 @@ def test_definitions(): cursor_pos = {'line': 3, 'character': 6} # The definition of 'a' - range = { + def_range = { 'start': {'line': 0, 'character': 4}, 'end': {'line': 0, 'character': 5} } doc = Document(DOC_URI, DOC) - assert [{'uri': DOC_URI, 'range': range}] == pyls_definitions(doc, cursor_pos) + assert [{'uri': DOC_URI, 'range': def_range}] == pyls_definitions(doc, cursor_pos) def test_builtin_definition(): @@ -47,10 +47,10 @@ def test_assignment(): cursor_pos = {'line': 11, 'character': 19} # The assignment of 'self.members' - range = { + def_range = { 'start': {'line': 8, 'character': 13}, 'end': {'line': 8, 'character': 20} } doc = Document(DOC_URI, DOC) - assert [{'uri': DOC_URI, 'range': range}] == pyls_definitions(doc, cursor_pos) + assert [{'uri': DOC_URI, 'range': def_range}] == pyls_definitions(doc, cursor_pos) diff --git a/test/plugins/test_format.py b/test/plugins/test_format.py index d944b512..b73953a2 100644 --- a/test/plugins/test_format.py +++ b/test/plugins/test_format.py @@ -30,11 +30,11 @@ def test_format(): def test_range_format(): doc = Document(DOC_URI, DOC) - range = { + def_range = { 'start': {'line': 0, 'character': 0}, 'end': {'line': 4, 'character': 10} } - res = pyls_format_range(doc, range) + res = pyls_format_range(doc, def_range) assert len(res) == 1 @@ -44,4 +44,4 @@ def test_range_format(): def test_no_change(): doc = Document(DOC_URI, GOOD_DOC) - assert len(pyls_format_document(doc)) == 0 + assert not pyls_format_document(doc) diff --git a/test/plugins/test_references.py b/test/plugins/test_references.py index afe9247e..c8cc2742 100644 --- a/test/plugins/test_references.py +++ b/test/plugins/test_references.py @@ -33,7 +33,7 @@ def create_file(name, content): return workspace -def test_references(tmp_workspace): +def test_references(tmp_workspace): # pylint: disable=redefined-outer-name # Over 'Test1' in class Test1(): position = {'line': 0, 'character': 8} DOC1_URI = uris.from_fs_path(os.path.join(tmp_workspace.root_path, DOC1_NAME)) diff --git a/test/plugins/test_signature.py b/test/plugins/test_signature.py index e0a5c74f..34cb77c5 100644 --- a/test/plugins/test_signature.py +++ b/test/plugins/test_signature.py @@ -25,7 +25,7 @@ def test_no_signature(): doc = Document(DOC_URI, DOC) sigs = signature.pyls_signature_help(doc, sig_position)['signatures'] - assert len(sigs) == 0 + assert not sigs def test_signature(): diff --git a/test/test_dispatcher.py b/test/test_dispatcher.py deleted file mode 100644 index 12bd1d73..00000000 --- a/test/test_dispatcher.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2017 Palantir Technologies, Inc. -import pytest -from pyls import dispatcher - - -class TestDispatcher(dispatcher.JSONRPCMethodDispatcher): - - def m_test__method(self, **params): - return params - - -def test_method_dispatcher(): - td = TestDispatcher() - params = {'hello': 'world'} - assert td['test/method'](**params) == params - - -def test_method_dispatcher_missing_method(): - td = TestDispatcher() - with pytest.raises(KeyError): - td['test/noMethod']('hello') diff --git a/test/test_document.py b/test/test_document.py index 5c6439c8..8565ca81 100644 --- a/test/test_document.py +++ b/test/test_document.py @@ -1,21 +1,8 @@ # Copyright 2017 Palantir Technologies, Inc. import sys -import pytest -from pyls import uris +from test.fixtures import DOC_URI, DOC from pyls.workspace import Document -DOC_URI = uris.from_fs_path(__file__) -DOC = """import sys - -def main(): - print sys.stdin.read() -""" - - -@pytest.fixture -def doc(): - return Document(DOC_URI, DOC) - def test_document_props(doc): assert doc.uri == DOC_URI diff --git a/test/test_json_rpc_server.py b/test/test_json_rpc_server.py new file mode 100644 index 00000000..587f43f9 --- /dev/null +++ b/test/test_json_rpc_server.py @@ -0,0 +1,77 @@ +# Copyright 2018 Palantir Technologies, Inc. +from jsonrpc.jsonrpc2 import JSONRPC20Request, JSONRPC20Response, JSONRPC20BatchRequest + + +def test_receive_request(json_rpc_server): + client, server = json_rpc_server + request = JSONRPC20Request(_id=0, method='initialize', params={}) + client.write_message(request) + message = next(server.get_messages()) + assert isinstance(message, JSONRPC20Request) + assert request.data == message.data + assert not message.is_notification + + +def test_receive_notification(json_rpc_server): + client, server = json_rpc_server + notification = JSONRPC20Request(method='initialize', params={}, is_notification=True) + client.write_message(notification) + message = next(server.get_messages()) + assert isinstance(message, JSONRPC20Request) + assert notification.data == message.data + assert message.is_notification + + +def test_receive_response(json_rpc_server): + client, server = json_rpc_server + response = JSONRPC20Response(_id=0, result={}) + client.write_message(response) + message = next(server.get_messages()) + assert isinstance(message, JSONRPC20Response) + assert response.data == message.data + + +def test_drop_bad_message(json_rpc_server): + client, server = json_rpc_server + response = JSONRPC20Response(_id=0, result={}) + client.write_message(response) + server.close() + try: + next(server.get_messages()) + except StopIteration: + pass + else: + assert False + + +def test_recieve_batch_request(json_rpc_server): + client, server = json_rpc_server + request_1 = JSONRPC20Request(_id=1, method='test_2', params={}) + request_2 = JSONRPC20Request(_id=2, method='test_2', params={}) + request = JSONRPC20BatchRequest(request_1, request_2) + client.write_message(request) + + messages = server.get_messages() + message_1 = next(messages) + message_2 = next(messages) + assert isinstance(message_1, JSONRPC20Request) + assert request_1.data == message_1.data + assert isinstance(message_2, JSONRPC20Request) + assert request_2.data == message_2.data + + +def test_send_batch_request_notification(json_rpc_server): + client, server = json_rpc_server + request_1 = JSONRPC20Request(_id=1, method='test_1', params={}) + request_2 = JSONRPC20Request(method='test_2', params={}) + request = JSONRPC20BatchRequest(request_1, request_2) + client.write_message(request) + + # load batch request + next(server.get_messages()) + + response_1 = JSONRPC20Response(_id=1, result='response_1') + server.write_message(response_1) + + response = next(client.get_messages()) + assert response.data == response_1.data diff --git a/test/test_language_server.py b/test/test_language_server.py index e1641dff..dffb9811 100644 --- a/test/test_language_server.py +++ b/test/test_language_server.py @@ -1,21 +1,15 @@ # Copyright 2017 Palantir Technologies, Inc. -import json import os from threading import Thread - -import jsonrpc +from jsonrpc.exceptions import JSONRPCMethodNotFound, JSONRPCDispatchException import pytest +from pyls.python_ls import start_io_lang_server, PythonLanguageServer -from pyls.server import JSONRPCServer -from pyls.language_server import start_io_lang_server -from pyls.python_ls import PythonLanguageServer +CALL_TIMEOUT = 2 -class JSONRPCClient(JSONRPCServer): - """ This is a weird way of testing.. but we're going to have two JSONRPCServers - talking to each other. One pretending to be a 'VSCode'-like client, the other is - our language server """ - pass +def start_client(client): + client.start() @pytest.fixture @@ -27,71 +21,37 @@ def client_server(): # Server to client pipe scr, scw = os.pipe() - server = Thread(target=start_io_lang_server, args=( + server_thread = Thread(target=start_io_lang_server, args=( os.fdopen(csr, 'rb'), os.fdopen(scw, 'wb'), PythonLanguageServer )) - server.daemon = True - server.start() - - client = JSONRPCClient(os.fdopen(scr, 'rb'), os.fdopen(csw, 'wb')) + server_thread.daemon = True + server_thread.start() - yield client, server + client = PythonLanguageServer(os.fdopen(scr, 'rb'), os.fdopen(csw, 'wb')) + client_thread = Thread(target=start_client, args=[client]) + client_thread.daemon = True + client_thread.start() - client.call('shutdown') - response = _get_response(client) - assert response['result'] is None - client.notify('exit') + yield client + shutdown_response = client.rpc_manager.call('shutdown').result(timeout=CALL_TIMEOUT) + assert shutdown_response is None + client.rpc_manager.notify('exit') -def test_initialize(client_server): - client, server = client_server - client.call('initialize', { +def test_initialize(client_server): # pylint: disable=redefined-outer-name + response = client_server.rpc_manager.call('initialize', { 'processId': 1234, 'rootPath': os.path.dirname(__file__), 'initializationOptions': {} - }) - response = _get_response(client) - - assert 'capabilities' in response['result'] - - -def test_missing_message(client_server): - client, server = client_server - - client.call('unknown_method') - response = _get_response(client) - assert response['error']['code'] == -32601 # Method not implemented error - - -def test_linting(client_server): - client, server = client_server - - # Initialize - client.call('initialize', { - 'processId': 1234, - 'rootPath': os.path.dirname(__file__), - 'initializationOptions': {} - }) - response = _get_response(client) - - assert 'capabilities' in response['result'] - - # didOpen - client.notify('textDocument/didOpen', { - 'textDocument': {'uri': 'file:///test', 'text': 'import sys'} - }) - 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().decode('utf-8')) - assert request.is_notification - return request.data + }).result(timeout=CALL_TIMEOUT) + assert 'capabilities' in response -def _get_response(client): - return json.loads(client._read_message().decode('utf-8')) +def test_missing_message(client_server): # pylint: disable=redefined-outer-name + try: + client_server.rpc_manager.call('unknown_method').result(timeout=CALL_TIMEOUT) + except JSONRPCDispatchException as e: + assert e.error.code == JSONRPCMethodNotFound.CODE + else: + assert False, "expected JSONRPCDispatchException" diff --git a/test/test_rpc_manager.py b/test/test_rpc_manager.py new file mode 100644 index 00000000..f0b9b4a7 --- /dev/null +++ b/test/test_rpc_manager.py @@ -0,0 +1,178 @@ +# Copyright 2018 Palantir Technologies, Inc. +from time import sleep +from test.fixtures import BASE_HANDLED_RESPONSE +from jsonrpc.jsonrpc2 import JSONRPC20Request, JSONRPC20Response +from jsonrpc.exceptions import JSONRPCMethodNotFound, JSONRPCServerError, JSONRPCDispatchException +from pyls.rpc_manager import MissingMethodException + + +def test_handle_request_sync(rpc_management): + rpc_manager, message_manager, message_handler = rpc_management + + rpc_manager.start() + message_manager.write_message.assert_called_once() + message_handler.assert_called_once_with('test', {}) + (sent_message, ), _ = message_manager.write_message.call_args + assert sent_message.data == BASE_HANDLED_RESPONSE.data + + +def test_handle_request_async(rpc_management): + rpc_manager, message_manager, message_handler = rpc_management + + def wrapper(): + return 'async' + message_handler.configure_mock(return_value=wrapper) + + rpc_manager.start() + message_manager.get_messages.assert_any_call() + message_handler.assert_called_once_with('test', {}) + + # block until request has been handled + sleep(0.25) + if rpc_manager._sent_requests: + rpc_manager._sent_requests.values()[0].result(timeout=1) + message_manager.write_message.assert_called_once() + (sent_message, ), _ = message_manager.write_message.call_args + assert sent_message.data == JSONRPC20Response(_id=1, result="async").data + + +def test_handle_request_async_exception(rpc_management): + rpc_manager, message_manager, message_handler = rpc_management + + def wrapper(): + raise RuntimeError("something bad happened") + message_handler.configure_mock(return_value=wrapper) + + rpc_manager.start() + message_manager.get_messages.assert_any_call() + message_handler.assert_called_once_with('test', {}) + + # block until request has been handled + sleep(0.25) + if rpc_manager._sent_requests: + rpc_manager._sent_requests.values()[0].result(timeout=1) + message_manager.write_message.assert_called_once() + (sent_message, ), _ = message_manager.write_message.call_args + assert sent_message.data == JSONRPC20Response(_id=1, error=JSONRPCServerError()._data).data + + +def test_handle_request_async_error(rpc_management): + rpc_manager, message_manager, message_handler = rpc_management + error_response = JSONRPCDispatchException(code=123, message="something bad happened", data={}) + + def wrapper(): + raise error_response + message_handler.configure_mock(return_value=wrapper) + + rpc_manager.start() + message_manager.get_messages.assert_any_call() + message_handler.assert_called_once_with('test', {}) + + # block until request has been handled + sleep(0.25) + if rpc_manager._sent_requests: + rpc_manager._sent_requests.values()[0].result(timeout=1) + message_manager.write_message.assert_called_once() + (sent_message, ), _ = message_manager.write_message.call_args + assert sent_message.error == error_response.error._data + + +def test_handle_request_unknown_method(rpc_management): + rpc_manager, message_manager, message_handler = rpc_management + message_handler.configure_mock(side_effect=MissingMethodException) + + rpc_manager.start() + message_manager.get_messages.assert_any_call() + message_handler.assert_called_once_with('test', {}) + (sent_message, ), _ = message_manager.write_message.call_args + assert sent_message.data == JSONRPC20Response(_id=1, error=JSONRPCMethodNotFound()._data).data + + +def test_handle_notification_sync(rpc_management): + rpc_manager, message_manager, message_handler = rpc_management + notification = JSONRPC20Request(method='notification', params={}, is_notification=True) + message_manager.get_messages.configure_mock(return_value=[notification]) + + rpc_manager.start() + message_manager.get_messages.assert_any_call() + message_handler.assert_called_once_with('notification', {}) + message_manager.write_message.assert_not_called() + + +def test_handle_notification_sync_empty(rpc_management): + rpc_manager, message_manager, message_handler = rpc_management + notification = JSONRPC20Request(method='notification', params=None, is_notification=True) + message_manager.get_messages.configure_mock(return_value=[notification]) + + rpc_manager.start() + message_manager.get_messages.assert_any_call() + message_handler.assert_called_once_with('notification', {}) + message_manager.write_message.assert_not_called() + + +def test_handle_notification_async(rpc_management): + rpc_manager, message_manager, message_handler = rpc_management + notification = JSONRPC20Request(method='notification', params={}, is_notification=True) + + def wrapper(): + pass + message_handler.configure_mock(return_value=wrapper) + message_manager.get_messages.configure_mock(return_value=[notification]) + + rpc_manager.start() + message_manager.get_messages.assert_any_call() + message_handler.assert_called_once_with('notification', {}) + message_manager.write_message.assert_not_called() + + +def test_handle_notification_async_empty(rpc_management): + rpc_manager, message_manager, message_handler = rpc_management + notification = JSONRPC20Request(method='notification', params=None, is_notification=True) + + def wrapper(): + pass + message_handler.configure_mock(return_value=wrapper) + message_manager.get_messages.configure_mock(return_value=[notification]) + + rpc_manager.start() + message_manager.get_messages.assert_any_call() + message_handler.assert_called_once_with('notification', {}) + message_manager.write_message.assert_not_called() + + +def test_handle_notification_unknown_method(rpc_management): + rpc_manager, message_manager, message_handler = rpc_management + notification = JSONRPC20Request(method='notification', params=None, is_notification=True) + message_manager.get_messages.configure_mock(return_value=[notification]) + message_handler.configure_mock(side_effect=KeyError) + + rpc_manager.start() + message_manager.get_messages.assert_any_call() + message_handler.assert_called_once_with('notification', {}) + message_manager.write_message.assert_not_called() + + +def test_send_request(rpc_management): + rpc_manager, message_manager, _ = rpc_management + + response_future = rpc_manager.call('request', {}) + message_manager.write_message.assert_called_once() + assert len(rpc_manager._sent_requests) == 1 + request_id = list(rpc_manager._sent_requests.keys())[0] + + response = JSONRPC20Response(_id=request_id, result={}) + message_manager.get_messages.configure_mock(return_value=[response]) + + rpc_manager.start() + message_manager.get_messages.assert_any_call() + assert not rpc_manager._sent_requests + assert response_future.result() == {} + + +def test_send_notification(rpc_management): + rpc_manager, message_manager, _ = rpc_management + + rpc_manager.notify('notify', {}) + message_manager.write_message.assert_called_once() + (sent_message, ), _ = message_manager.write_message.call_args + assert sent_message.data == (JSONRPC20Request(method='notify', params={}, is_notification=True)).data diff --git a/test/test_uris.py b/test/test_uris.py index 37dae33e..d4e177e6 100644 --- a/test/test_uris.py +++ b/test/test_uris.py @@ -1,7 +1,7 @@ # Copyright 2017 Palantir Technologies, Inc. +from test import unix_only, windows_only import pytest from pyls import uris -from test import unix_only, windows_only @unix_only diff --git a/tox.ini b/tox.ini index 28a64c69..8bf29a6e 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,7 @@ envlist = py27,py34,lint [pycodestyle] ignore = E226, E722 max-line-length = 120 +exclude = test/plugins/.ropeproject,test/.ropeproject [pytest] testpaths = test @@ -22,11 +23,12 @@ commands = deps = pytest coverage + mock pytest-cov pylint [testenv:lint] commands = - pylint pyls - pycodestyle pyls - pyflakes pyls + pylint pyls test + pycodestyle pyls test + pyflakes pyls test diff --git a/vscode-client/src/extension.ts b/vscode-client/src/extension.ts index 005299d1..3eea77e3 100644 --- a/vscode-client/src/extension.ts +++ b/vscode-client/src/extension.ts @@ -44,7 +44,7 @@ function startLangServerTCP(addr: number, documentSelector: string[]): Disposabl export function activate(context: ExtensionContext) { context.subscriptions.push(startLangServer("pyls", ["-vv"], ["python"])); - // For TCP + // For TCP server needs to be started seperately // context.subscriptions.push(startLangServerTCP(2087, ["python"])); }