pax_global_header00006660000000000000000000000064145036176750014530gustar00rootroot0000000000000052 comment=786d8dd8f830dbd83a17962c0167183a6609e72f python-lsp-jsonrpc-1.1.2/000077500000000000000000000000001450361767500153225ustar00rootroot00000000000000python-lsp-jsonrpc-1.1.2/.coveragerc000066400000000000000000000000471450361767500174440ustar00rootroot00000000000000[run] omit = pylsp_jsonrpc/_version.py python-lsp-jsonrpc-1.1.2/.gitattributes000066400000000000000000000000411450361767500202100ustar00rootroot00000000000000jsonrpc/_version.py export-subst python-lsp-jsonrpc-1.1.2/.github/000077500000000000000000000000001450361767500166625ustar00rootroot00000000000000python-lsp-jsonrpc-1.1.2/.github/workflows/000077500000000000000000000000001450361767500207175ustar00rootroot00000000000000python-lsp-jsonrpc-1.1.2/.github/workflows/static.yml000066400000000000000000000017141450361767500227340ustar00rootroot00000000000000name: Static code analysis on: push: branches: - develop pull_request: branches: - '*' jobs: build: name: Static code analysis runs-on: ubuntu-latest env: CI: 'true' OS: 'linux' timeout-minutes: 2 steps: - uses: actions/cache@v1 with: path: ~/.cache/pip key: static-pip-${{ hashFiles('setup.py') }} restore-keys: static-pip- - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: # TODO: check with Python 3, but need to fix the # errors first python-version: '3.8' architecture: 'x64' - run: python -m pip install --upgrade pip setuptools - run: pip install -e .[test] - name: Pylint checks run: pylint pylsp_jsonrpc test - name: Code style checks run: pycodestyle pylsp_jsonrpc test - name: Pyflakes checks run: pyflakes pylsp_jsonrpc test python-lsp-jsonrpc-1.1.2/.github/workflows/test-linux.yml000066400000000000000000000021431450361767500235560ustar00rootroot00000000000000name: Linux tests on: push: branches: - develop pull_request: branches: - '*' jobs: build: name: Linux Py${{ matrix.PYTHON_VERSION }} runs-on: ubuntu-latest env: CI: 'true' OS: 'linux' PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} strategy: fail-fast: false matrix: PYTHON_VERSION: ['3.10', '3.9', '3.8'] timeout-minutes: 10 steps: - uses: actions/cache@v1 with: path: ~/.cache/pip key: ${{ runner.os }}-${{ matrix.PYTHON_VERSION }}-pip-${{ hashFiles('setup.py') }} restore-keys: ${{ runner.os }}-${{ matrix.PYTHON_VERSION }}-pip- - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: ${{ matrix.PYTHON_VERSION }} architecture: 'x64' - run: python -m pip install --upgrade pip setuptools - run: pip install -e .[all,test] - run: pytest -v test/ # Enable this if SSH debugging is required # - name: Setup tmate session # uses: mxschmitt/action-tmate@v3 # if: ${{ failure() }} python-lsp-jsonrpc-1.1.2/.github/workflows/test-mac.yml000066400000000000000000000021461450361767500231620ustar00rootroot00000000000000name: Mac tests on: push: branches: - develop pull_request: branches: - '*' jobs: build: name: Mac Py${{ matrix.PYTHON_VERSION }} runs-on: macos-latest env: CI: 'true' OS: 'macos' PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} strategy: fail-fast: false matrix: PYTHON_VERSION: ['3.10', '3.9', '3.8'] timeout-minutes: 10 steps: - uses: actions/cache@v1 with: path: ~/Library/Caches/pip key: ${{ runner.os }}-${{ matrix.PYTHON_VERSION }}-pip-${{ hashFiles('setup.py') }} restore-keys: ${{ runner.os }}-${{ matrix.PYTHON_VERSION }}-pip- - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: ${{ matrix.PYTHON_VERSION }} architecture: 'x64' - run: python -m pip install --upgrade pip setuptools - run: pip install -e .[all,test] - run: pytest -v test/ # Enable this if SSH debugging is required # - name: Setup tmate session # uses: mxschmitt/action-tmate@v3 # if: ${{ failure() }} python-lsp-jsonrpc-1.1.2/.github/workflows/test-win.yml000066400000000000000000000017211450361767500232150ustar00rootroot00000000000000name: Windows tests on: push: branches: - develop pull_request: branches: - '*' jobs: build: name: Win Py${{ matrix.PYTHON_VERSION }} runs-on: windows-latest env: CI: 'true' OS: 'win' PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }} strategy: fail-fast: false matrix: PYTHON_VERSION: ['3.10', '3.9', '3.8'] timeout-minutes: 10 steps: - uses: actions/cache@v1 with: path: ~\AppData\Local\pip\Cache key: ${{ runner.os }}-${{ matrix.PYTHON_VERSION }}-pip-${{ hashFiles('setup.py') }} restore-keys: ${{ runner.os }}-${{ matrix.PYTHON_VERSION }}-pip- - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: ${{ matrix.PYTHON_VERSION }} architecture: 'x64' - run: python -m pip install --upgrade pip setuptools - run: pip install -e .[all,test] - run: pytest -v test/ python-lsp-jsonrpc-1.1.2/.gitignore000066400000000000000000000023621450361767500173150ustar00rootroot00000000000000# autogenerated version file /pylsp_jsonrpc/_version.py # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # IntelliJ *.iml *.ipr *.iws .idea/ out/ # C extensions *.so # Distribution / packaging .Python env/ env3/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ pytest.xml .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject # JavaScript **/*.vscode/ # vim *.sw[mnopqrs] # Idea .idea/ # Merge orig files *.orig python-lsp-jsonrpc-1.1.2/.pylintrc000066400000000000000000000004551450361767500171730ustar00rootroot00000000000000[FORMAT] max-line-length = 120 [MESSAGES CONTROL] enable = useless-suppression disable = duplicate-code, invalid-name, fixme, missing-docstring, protected-access, too-few-public-methods, too-many-arguments, too-many-instance-attributes [REPORTS] reports = no python-lsp-jsonrpc-1.1.2/CHANGELOG.md000066400000000000000000000067451450361767500171470ustar00rootroot00000000000000# History of changes ## Version 1.1.2 (2023/09/23) ### Pull Requests Merged * [PR 26](https://github.com/python-lsp/python-lsp-jsonrpc/pull/26) - Fix tests so they're compatible with both ujson and pure json library, by [@ajohnston9](https://github.com/ajohnston9) In this release 1 pull request was closed. ---- ## Version 1.1.1 (2023/09/09) ### Issues Closed * [Issue 18](https://github.com/python-lsp/python-lsp-jsonrpc/issues/18) - No license included in package metadata ([PR 19](https://github.com/python-lsp/python-lsp-jsonrpc/pull/19) by [@thejcannon](https://github.com/thejcannon)) In this release 1 issue was closed. ### Pull Requests Merged * [PR 23](https://github.com/python-lsp/python-lsp-jsonrpc/pull/23) - Remove redundant wheel dep from pyproject.toml, by [@mgorny](https://github.com/mgorny) * [PR 19](https://github.com/python-lsp/python-lsp-jsonrpc/pull/19) - Add license trove classifier, by [@thejcannon](https://github.com/thejcannon) ([18](https://github.com/python-lsp/python-lsp-jsonrpc/issues/18)) In this release 2 pull requests were closed. ---- ## Version 1.1.0 (2023/09/07) ## New features * Allow method handlers to return json rpc errors * Drop support for Python 3.7 and 3.6 ### Issues Closed * [Issue 11](https://github.com/python-lsp/python-lsp-jsonrpc/issues/11) - Drop support for Python 3.6 ([PR 16](https://github.com/python-lsp/python-lsp-jsonrpc/pull/16) by [@ccordoba12](https://github.com/ccordoba12)) In this release 1 issue was closed. ### Pull Requests Merged * [PR 21](https://github.com/python-lsp/python-lsp-jsonrpc/pull/21) - Drop support for Python 3.7, by [@ccordoba12](https://github.com/ccordoba12) * [PR 20](https://github.com/python-lsp/python-lsp-jsonrpc/pull/20) - Allow method handlers to return json rpc errors, by [@smacke](https://github.com/smacke) * [PR 16](https://github.com/python-lsp/python-lsp-jsonrpc/pull/16) - Make necessary changes to drop support for Python 3.6, by [@ccordoba12](https://github.com/ccordoba12) ([11](https://github.com/python-lsp/python-lsp-jsonrpc/issues/11)) * [PR 15](https://github.com/python-lsp/python-lsp-jsonrpc/pull/15) - Drop Python 3.6 on CIs and start testing with Python 3.10, by [@ccordoba12](https://github.com/ccordoba12) * [PR 14](https://github.com/python-lsp/python-lsp-jsonrpc/pull/14) - Migrate metadata from `setup.cfg` to PEP 621-compliant `pyproject.toml`, by [@KOLANICH](https://github.com/KOLANICH) * [PR 10](https://github.com/python-lsp/python-lsp-jsonrpc/pull/10) - Use f-strings wherever possible, by [@ccordoba12](https://github.com/ccordoba12) * [PR 9](https://github.com/python-lsp/python-lsp-jsonrpc/pull/9) - Add license info to setup.py so it will show up in wheel installs., by [@itsbenweeks](https://github.com/itsbenweeks) * [PR 8](https://github.com/python-lsp/python-lsp-jsonrpc/pull/8) - Move the package metadata from setup.py to setup.cfg, by [@KOLANICH](https://github.com/KOLANICH) * [PR 4](https://github.com/python-lsp/python-lsp-jsonrpc/pull/4) - PR: Fix typos in readme, by [@yaegassy](https://github.com/yaegassy) In this release 9 pull requests were closed. ---- ## Version 1.0.0 (2021/04/14) ### Issues Closed * [Issue 3](https://github.com/python-lsp/python-lsp-jsonrpc/issues/3) - Release v1.0.0 In this release 1 issue was closed. ### Pull Requests Merged * [PR 2](https://github.com/python-lsp/python-lsp-jsonrpc/pull/2) - PR: Update package name, remove Python 2 compatibility and update CIs, by [@andfoy](https://github.com/andfoy) In this release 1 pull request was closed. python-lsp-jsonrpc-1.1.2/LICENSE000066400000000000000000000021731450361767500163320ustar00rootroot00000000000000The MIT License (MIT) Copyright 2017-2020 Palantir Technologies, Inc. Copyright 2021 Python Language Server Contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. python-lsp-jsonrpc-1.1.2/MANIFEST.in000066400000000000000000000001631450361767500170600ustar00rootroot00000000000000include README.rst include LICENSE include .pylintrc include pylsp_jsonrpc/_version.py recursive-include test *.py python-lsp-jsonrpc-1.1.2/README.md000066400000000000000000000015061450361767500166030ustar00rootroot00000000000000# Python JSON RPC Server A Python 3.8+ server implementation of the [JSON RPC 2.0](http://www.jsonrpc.org/specification) protocol. This library has been pulled out of the [Python LSP Server](https://github.com/python-lsp/python-lsp-server) project. ## Installation pip install -U python-lsp-jsonrpc ## Examples The examples directory contains two examples of running language servers over websockets. `examples/langserver.py` shows how to run a language server in-memory. `examples/langserver_ext.py` shows how to run a subprocess language server, in this case the Python LSP Server. Start by installing `tornado` and `python-lsp-server` pip install python-lsp-server[all] tornado Then running `python examples/langserver.py` or `python examples/langserver_ext.py` will host a websocket on ``ws://localhost:3000/python``. python-lsp-jsonrpc-1.1.2/RELEASE.md000066400000000000000000000013741450361767500167310ustar00rootroot00000000000000## Before the release: 1. Create pull request to update CHANGELOG.md with * `loghub python-lsp/python-lsp-jsonrpc -m vX.X.X` * git add -A && git commit -m "Update changelog for X.X.X" This is necessary to run our tests before the release, so we can be sure everything is in order. ## To release a new version of python-lsp-jsonrpc: 1. git fetch upstream && git checkout upstream/master 2. Close milestone on GitHub 3. git clean -xfdi 4. git tag -a vX.X.X -m "Release vX.X.X" 5. python -m pip install --upgrade pip 6. pip install --upgrade --upgrade-strategy eager build setuptools twine wheel 7. python -bb -X dev -W error -m build 8. twine check --strict dist/* 9. twine upload dist/* 10. git push upstream --tags 11. Create release on Github python-lsp-jsonrpc-1.1.2/examples/000077500000000000000000000000001450361767500171405ustar00rootroot00000000000000python-lsp-jsonrpc-1.1.2/examples/langserver.py000066400000000000000000000046431450361767500216710ustar00rootroot00000000000000import logging from tornado import web, ioloop, websocket from pylsp_jsonrpc import dispatchers, endpoint try: import ujson as json except Exception: # pylint: disable=broad-except import json log = logging.getLogger(__name__) class LanguageServer(dispatchers.MethodDispatcher): """Implement a JSON RPC method dispatcher for the language server protocol.""" def __init__(self): # Endpoint is lazily set after construction self.endpoint = None def m_initialize(self, rootUri=None, **kwargs): log.info("Got initialize params: %s", kwargs) return {"capabilities": { "textDocumentSync": { "openClose": True, } }} def m_text_document__did_open(self, textDocument=None, **_kwargs): log.info("Opened text document %s", textDocument) self.endpoint.notify('textDocument/publishDiagnostics', { 'uri': textDocument['uri'], 'diagnostics': [{ 'range': { 'start': {'line': 0, 'character': 0}, 'end': {'line': 1, 'character': 0}, }, 'message': 'Some very bad Python code', 'severity': 1 # DiagnosticSeverity.Error }] }) class LanguageServerWebSocketHandler(websocket.WebSocketHandler): """Setup tornado websocket handler to host language server.""" def __init__(self, *args, **kwargs): # Create an instance of the language server used to dispatch JSON RPC methods langserver = LanguageServer() # Setup an endpoint that dispatches to the ls, and writes server->client messages # back to the client websocket self.endpoint = endpoint.Endpoint(langserver, lambda msg: self.write_message(json.dumps(msg))) # Give the language server a handle to the endpoint so it can send JSON RPC # notifications and requests. langserver.endpoint = self.endpoint super(LanguageServerWebSocketHandler, self).__init__(*args, **kwargs) def on_message(self, message): """Forward client->server messages to the endpoint.""" self.endpoint.consume(json.loads(message)) def check_origin(self, origin): return True if __name__ == "__main__": app = web.Application([ (r"/python", LanguageServerWebSocketHandler), ]) app.listen(3000, address='127.0.0.1') ioloop.IOLoop.current().start() python-lsp-jsonrpc-1.1.2/examples/langserver_ext.py000066400000000000000000000033431450361767500225450ustar00rootroot00000000000000import logging import subprocess import threading from tornado import ioloop, process, web, websocket from pylsp_jsonrpc import streams try: import ujson as json except Exception: # pylint: disable=broad-except import json log = logging.getLogger(__name__) class LanguageServerWebSocketHandler(websocket.WebSocketHandler): """Setup tornado websocket handler to host an external language server.""" writer = None def open(self, *args, **kwargs): log.info("Spawning pylsp subprocess") # Create an instance of the language server proc = process.Subprocess( ['pylsp', '-v'], stdin=subprocess.PIPE, stdout=subprocess.PIPE ) # Create a writer that formats json messages with the correct LSP headers self.writer = streams.JsonRpcStreamWriter(proc.stdin) # Create a reader for consuming stdout of the language server. We need to # consume this in another thread def consume(): # Start a tornado IOLoop for reading/writing to the process in this thread ioloop.IOLoop() reader = streams.JsonRpcStreamReader(proc.stdout) reader.listen(lambda msg: self.write_message(json.dumps(msg))) thread = threading.Thread(target=consume) thread.daemon = True thread.start() def on_message(self, message): """Forward client->server messages to the endpoint.""" self.writer.write(json.loads(message)) def check_origin(self, origin): return True if __name__ == "__main__": app = web.Application([ (r"/python", LanguageServerWebSocketHandler), ]) app.listen(3000, address='127.0.0.1') ioloop.IOLoop.current().start() python-lsp-jsonrpc-1.1.2/pylsp_jsonrpc/000077500000000000000000000000001450361767500202275ustar00rootroot00000000000000python-lsp-jsonrpc-1.1.2/pylsp_jsonrpc/__init__.py000066400000000000000000000012521450361767500223400ustar00rootroot00000000000000# Copyright 2017-2020 Palantir Technologies, Inc. # Copyright 2021- Python Language Server Contributors. from . import _version from ._version import __version__ def convert_version_info(version: str) -> (int, ..., str): version_info = version.split(".") for i in range(len(version_info)): # pylint:disable=consider-using-enumerate try: version_info[i] = int(version_info[i]) except ValueError: version_info[i] = version_info[i].split("+")[0] version_info = version_info[: i + 1] break return tuple(version_info) _version.VERSION_INFO = convert_version_info(__version__) __all__ = ("__version__",) python-lsp-jsonrpc-1.1.2/pylsp_jsonrpc/dispatchers.py000066400000000000000000000020161450361767500231110ustar00rootroot00000000000000# Copyright 2017-2020 Palantir Technologies, Inc. # Copyright 2021- Python Language Server Contributors. import functools import re _RE_FIRST_CAP = re.compile('(.)([A-Z][a-z]+)') _RE_ALL_CAP = re.compile('([a-z0-9])([A-Z])') class MethodDispatcher: """JSON RPC dispatcher that calls methods on itself. Method names are computed by converting camel case to snake case, slashes with double underscores, and removing dollar signs. """ def __getitem__(self, item): method_name = f'm_{_method_to_string(item)}' if hasattr(self, method_name): method = getattr(self, method_name) @functools.wraps(method) def handler(params): return method(**(params or {})) return handler raise KeyError() 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() python-lsp-jsonrpc-1.1.2/pylsp_jsonrpc/endpoint.py000066400000000000000000000237551450361767500224350ustar00rootroot00000000000000# Copyright 2017-2020 Palantir Technologies, Inc. # Copyright 2021- Python Language Server Contributors. import logging import uuid import sys from typing import Any, Dict, Mapping from concurrent import futures from .exceptions import (JsonRpcException, JsonRpcRequestCancelled, JsonRpcInternalError, JsonRpcMethodNotFound) log = logging.getLogger(__name__) JSONRPC_VERSION = '2.0' CANCEL_METHOD = '$/cancelRequest' class Endpoint: def __init__(self, dispatcher, consumer, id_generator=lambda: str(uuid.uuid4()), max_workers=5): """A JSON RPC endpoint for managing messages sent to/from the client. Args: dispatcher (dict): A dictionary of method name to handler function. The handler functions should return either the result or a callable that will be used to asynchronously compute the result. consumer (fn): A function that consumes JSON RPC message dicts and sends them to the client. id_generator (fn, optional): A function used to generate request IDs. Defaults to the string value of :func:`uuid.uuid4`. max_workers (int, optional): The number of workers in the asynchronous executor pool. """ self._dispatcher = dispatcher self._consumer = consumer self._id_generator = id_generator self._client_request_futures = {} self._server_request_futures = {} self._executor_service = futures.ThreadPoolExecutor(max_workers=max_workers) def shutdown(self): self._executor_service.shutdown() def notify(self, method, params=None): """Send a JSON RPC notification to the client. Args: method (str): The method name of the notification to send params (any): The payload of the notification """ log.debug('Sending notification: %s %s', method, params) message = { 'jsonrpc': JSONRPC_VERSION, 'method': method, } if params is not None: message['params'] = params self._consumer(message) def request(self, method, params=None): """Send a JSON RPC request to the client. Args: method (str): The method name of the message to send params (any): The payload of the message Returns: Future that will resolve once a response has been received """ msg_id = self._id_generator() log.debug('Sending request with id %s: %s %s', msg_id, method, params) message = { 'jsonrpc': JSONRPC_VERSION, 'id': msg_id, 'method': method, } if params is not None: message['params'] = params request_future = futures.Future() request_future.add_done_callback(self._cancel_callback(msg_id)) self._server_request_futures[msg_id] = request_future self._consumer(message) return request_future def _cancel_callback(self, request_id): """Construct a cancellation callback for the given request ID.""" def callback(future): if future.cancelled(): self.notify(CANCEL_METHOD, {'id': request_id}) future.set_exception(JsonRpcRequestCancelled()) return callback def consume(self, message): """Consume a JSON RPC message from the client. Args: message (dict): The JSON RPC message sent by the client """ if 'jsonrpc' not in message or message['jsonrpc'] != JSONRPC_VERSION: log.warning("Unknown message type %s", message) return if 'id' not in message: log.debug("Handling notification from client %s", message) self._handle_notification(message['method'], message.get('params')) elif 'method' not in message: log.debug("Handling response from client %s", message) self._handle_response(message['id'], message.get('result'), message.get('error')) else: try: log.debug("Handling request from client %s", message) self._handle_request(message['id'], message['method'], message.get('params')) except JsonRpcException as e: log.exception("Failed to handle request %s", message['id']) self._consumer({ 'jsonrpc': JSONRPC_VERSION, 'id': message['id'], 'error': e.to_dict() }) except Exception: # pylint: disable=broad-except log.exception("Failed to handle request %s", message['id']) self._consumer({ 'jsonrpc': JSONRPC_VERSION, 'id': message['id'], 'error': JsonRpcInternalError.of(sys.exc_info()).to_dict() }) def _handle_notification(self, method, params): """Handle a notification from the client.""" if method == CANCEL_METHOD: self._handle_cancel_notification(params['id']) return try: handler = self._dispatcher[method] except KeyError: log.warning("Ignoring notification for unknown method %s", method) return try: handler_result = handler(params) except Exception: # pylint: disable=broad-except log.exception("Failed to handle notification %s: %s", method, params) return if callable(handler_result): log.debug("Executing async notification handler %s", handler_result) notification_future = self._executor_service.submit(handler_result) notification_future.add_done_callback(self._notification_callback(method, params)) @staticmethod def _notification_callback(method, params): """Construct a notification callback for the given request ID.""" def callback(future): try: future.result() log.debug("Successfully handled async notification %s %s", method, params) except Exception: # pylint: disable=broad-except log.exception("Failed to handle async notification %s %s", method, params) return callback def _handle_cancel_notification(self, msg_id): """Handle a cancel notification from the client.""" request_future = self._client_request_futures.pop(msg_id, None) if not request_future: log.warning("Received cancel notification for unknown message id %s", msg_id) return # Will only work if the request hasn't started executing if request_future.cancel(): log.debug("Cancelled request with id %s", msg_id) @staticmethod def _make_response_payload(header: Dict[str, Any], result: Any) -> Mapping[str, Any]: # return type of 'Mapping' because it should not be mutated # further from here response = dict(header) if isinstance(result, dict) and ('result' in result or 'error' in result): response.update(result) else: response['result'] = result return response def _handle_request(self, msg_id, method, params): """Handle a request from the client.""" try: handler = self._dispatcher[method] except KeyError as e: raise JsonRpcMethodNotFound.of(method) from e handler_result = handler(params) if callable(handler_result): log.debug("Executing async request handler %s", handler_result) request_future = self._executor_service.submit(handler_result) self._client_request_futures[msg_id] = request_future request_future.add_done_callback(self._request_callback(msg_id)) elif isinstance(handler_result, futures.Future): log.debug("Request handler is already a future %s", handler_result) self._client_request_futures[msg_id] = handler_result handler_result.add_done_callback(self._request_callback(msg_id)) else: log.debug("Got result from synchronous request handler: %s", handler_result) response = self._make_response_payload( { 'jsonrpc': JSONRPC_VERSION, 'id': msg_id, }, handler_result, ) self._consumer(response) def _request_callback(self, request_id): """Construct a request callback for the given request ID.""" def callback(future): # Remove the future from the client requests map self._client_request_futures.pop(request_id, None) if future.cancelled(): future.set_exception(JsonRpcRequestCancelled()) message = { 'jsonrpc': JSONRPC_VERSION, 'id': request_id, } try: result = future.result() message = self._make_response_payload(message, result) except JsonRpcException as e: log.exception("Failed to handle request %s", request_id) message['error'] = e.to_dict() except Exception: # pylint: disable=broad-except log.exception("Failed to handle request %s", request_id) message['error'] = JsonRpcInternalError.of(sys.exc_info()).to_dict() self._consumer(message) return callback def _handle_response(self, msg_id, result=None, error=None): """Handle a response from the client.""" request_future = self._server_request_futures.pop(msg_id, None) if not request_future: log.warning("Received response to unknown message id %s", msg_id) return if error is not None: log.debug("Received error response to message %s: %s", msg_id, error) request_future.set_exception(JsonRpcException.from_dict(error)) return log.debug("Received result for message %s: %s", msg_id, result) request_future.set_result(result) python-lsp-jsonrpc-1.1.2/pylsp_jsonrpc/exceptions.py000066400000000000000000000053651450361767500227730ustar00rootroot00000000000000# Copyright 2017-2020 Palantir Technologies, Inc. # Copyright 2021- Python Language Server Contributors. import traceback class JsonRpcException(Exception): def __init__(self, message=None, code=None, data=None): super().__init__(message) self.message = message or getattr(self.__class__, 'MESSAGE') self.code = code or getattr(self.__class__, 'CODE') self.data = data def to_dict(self): exception_dict = { 'code': self.code, 'message': self.message, } if self.data is not None: exception_dict['data'] = self.data return exception_dict def __eq__(self, other): return ( isinstance(other, self.__class__) and self.code == other.code and self.message == other.message ) def __hash__(self): return hash((self.code, self.message)) @staticmethod def from_dict(error): for exc_class in _EXCEPTIONS: if exc_class.supports_code(error['code']): return exc_class(**error) return JsonRpcException(**error) @classmethod def supports_code(cls, code): # Defaults to UnknownErrorCode return getattr(cls, 'CODE', -32001) == code class JsonRpcParseError(JsonRpcException): CODE = -32700 MESSAGE = 'Parse Error' class JsonRpcInvalidRequest(JsonRpcException): CODE = -32600 MESSAGE = 'Invalid Request' class JsonRpcMethodNotFound(JsonRpcException): CODE = -32601 MESSAGE = 'Method Not Found' @classmethod def of(cls, method): return cls(message=cls.MESSAGE + ': ' + method) class JsonRpcInvalidParams(JsonRpcException): CODE = -32602 MESSAGE = 'Invalid Params' class JsonRpcInternalError(JsonRpcException): CODE = -32602 MESSAGE = 'Internal Error' @classmethod def of(cls, exc_info): exc_type, exc_value, exc_tb = exc_info return cls( message=''.join(traceback.format_exception_only(exc_type, exc_value)).strip(), data={'traceback': traceback.format_tb(exc_tb)} ) class JsonRpcRequestCancelled(JsonRpcException): CODE = -32800 MESSAGE = 'Request Cancelled' class JsonRpcServerError(JsonRpcException): def __init__(self, message, code, data=None): assert _is_server_error_code(code) super().__init__(message=message, code=code, data=data) @classmethod def supports_code(cls, code): return _is_server_error_code(code) def _is_server_error_code(code): return -32099 <= code <= -32000 _EXCEPTIONS = ( JsonRpcParseError, JsonRpcInvalidRequest, JsonRpcMethodNotFound, JsonRpcInvalidParams, JsonRpcInternalError, JsonRpcRequestCancelled, JsonRpcServerError, ) python-lsp-jsonrpc-1.1.2/pylsp_jsonrpc/streams.py000066400000000000000000000063341450361767500222650ustar00rootroot00000000000000# Copyright 2017-2020 Palantir Technologies, Inc. # Copyright 2021- Python Language Server Contributors. import logging import threading try: import ujson as json except Exception: # pylint: disable=broad-except import json log = logging.getLogger(__name__) class JsonRpcStreamReader: def __init__(self, rfile): self._rfile = rfile def close(self): self._rfile.close() def listen(self, message_consumer): """Blocking call to listen for messages on the rfile. Args: message_consumer (fn): function that is passed each message as it is read off the socket. """ while not self._rfile.closed: try: request_str = self._read_message() except ValueError: if self._rfile.closed: return log.exception("Failed to read from rfile") if request_str is None: break try: message_consumer(json.loads(request_str.decode('utf-8'))) except ValueError: log.exception("Failed to parse JSON message %s", request_str) continue 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 = self._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) @staticmethod 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 as e: raise ValueError(f"Invalid Content-Length header: {value}") from e return None class JsonRpcStreamWriter: def __init__(self, wfile, **json_dumps_args): self._wfile = wfile self._wfile_lock = threading.Lock() self._json_dumps_args = json_dumps_args def close(self): with self._wfile_lock: self._wfile.close() def write(self, message): with self._wfile_lock: if self._wfile.closed: return try: body = json.dumps(message, **self._json_dumps_args) # Ensure we get the byte length, not the character length content_length = len(body) if isinstance(body, bytes) else len(body.encode('utf-8')) response = ( f"Content-Length: {content_length}\r\n" f"Content-Type: application/vscode-jsonrpc; charset=utf8\r\n\r\n" f"{body}" ) self._wfile.write(response.encode('utf-8')) self._wfile.flush() except Exception: # pylint: disable=broad-except log.exception("Failed to write message to output file %s", message) python-lsp-jsonrpc-1.1.2/pyproject.toml000066400000000000000000000024301450361767500202350ustar00rootroot00000000000000# Copyright 2017-2020 Palantir Technologies, Inc. # Copyright 2021- Python Language Server Contributors. [build-system] requires = ["setuptools>=61.2.0", "setuptools_scm[toml]>=3.4.3"] build-backend = "setuptools.build_meta" [project] name = "python-lsp-jsonrpc" authors = [{name = "Python Language Server Contributors"}] description = "JSON RPC 2.0 server library" license = {text = "MIT"} requires-python = ">=3.8" dependencies = ["ujson>=3.0.0"] dynamic = ["version"] classifiers = [ "License :: OSI Approved :: MIT License", ] [project.readme] file = "README.md" content-type = "text/markdown" [project.urls] Homepage = "https://github.com/python-lsp/python-lsp-jsonrpc" [project.optional-dependencies] test = [ "pylint", "pycodestyle", "pyflakes", "pytest", "pytest-cov", "coverage", ] [tool.setuptools] license-files = ["LICENSE"] include-package-data = false [tool.setuptools.packages.find] exclude = ["contrib", "docs", "test", "test.*"] namespaces = false [tool.setuptools_scm] write_to = "pylsp_jsonrpc/_version.py" write_to_template = "__version__ = \"{version}\"\n" # VERSION_INFO is populated in __main__ [tool.pytest.ini_options] testpaths = ["test"] addopts = "--cov-report html --cov-report term --junitxml=pytest.xml --cov pylsp_jsonrpc --cov test" python-lsp-jsonrpc-1.1.2/scripts/000077500000000000000000000000001450361767500170115ustar00rootroot00000000000000python-lsp-jsonrpc-1.1.2/scripts/circle/000077500000000000000000000000001450361767500202525ustar00rootroot00000000000000python-lsp-jsonrpc-1.1.2/scripts/circle/pypi.sh000077500000000000000000000007201450361767500215710ustar00rootroot00000000000000#!/bin/bash -e if [ -z "$CI" ]; then echo "Will only continue on CI" exit fi # build package and upload to private pypi index rm -f ~/.pypirc echo "[distutils]" >> ~/.pypirc echo "index-servers = pypi-private" >> ~/.pypirc echo "[pypi-private]" >> ~/.pypirc echo "repository=https://$PYPI_HOST" >> ~/.pypirc echo "username=$PYPI_USERNAME" >> ~/.pypirc echo "password=$PYPI_PASSWORD" >> ~/.pypirc python setup.py sdist bdist_wheel upload -r pypi-private python-lsp-jsonrpc-1.1.2/setup.cfg000066400000000000000000000003361450361767500171450ustar00rootroot00000000000000# Copyright 2017-2020 Palantir Technologies, Inc. # Copyright 2021- Python Language Server Contributors. [pycodestyle] ignore = E226, E722, W504 max-line-length = 120 exclude = test/plugins/.ropeproject,test/.ropeproject python-lsp-jsonrpc-1.1.2/test/000077500000000000000000000000001450361767500163015ustar00rootroot00000000000000python-lsp-jsonrpc-1.1.2/test/__init__.py000066400000000000000000000000001450361767500204000ustar00rootroot00000000000000python-lsp-jsonrpc-1.1.2/test/test_endpoint.py000066400000000000000000000226061450361767500215400ustar00rootroot00000000000000# Copyright 2018 Palantir Technologies, Inc. # pylint: disable=redefined-outer-name from concurrent import futures import time from unittest import mock import pytest from pylsp_jsonrpc import exceptions from pylsp_jsonrpc.endpoint import Endpoint MSG_ID = 'id' @pytest.fixture() def dispatcher(): return {} @pytest.fixture() def consumer(): return mock.MagicMock() @pytest.fixture() def endpoint(dispatcher, consumer): return Endpoint(dispatcher, consumer, id_generator=lambda: MSG_ID) def test_bad_message(endpoint): # Ensure doesn't raise for a bad message endpoint.consume({'key': 'value'}) def test_notify(endpoint, consumer): endpoint.notify('methodName', {'key': 'value'}) consumer.assert_called_once_with({ 'jsonrpc': '2.0', 'method': 'methodName', 'params': {'key': 'value'} }) def test_notify_none_params(endpoint, consumer): endpoint.notify('methodName', None) consumer.assert_called_once_with({ 'jsonrpc': '2.0', 'method': 'methodName', }) def test_request(endpoint, consumer): future = endpoint.request('methodName', {'key': 'value'}) assert not future.done() consumer.assert_called_once_with({ 'jsonrpc': '2.0', 'id': MSG_ID, 'method': 'methodName', 'params': {'key': 'value'} }) # Send the response back to the endpoint result = 1234 endpoint.consume({ 'jsonrpc': '2.0', 'id': MSG_ID, 'result': result }) assert future.result(timeout=2) == result def test_request_error(endpoint, consumer): future = endpoint.request('methodName', {'key': 'value'}) assert not future.done() consumer.assert_called_once_with({ 'jsonrpc': '2.0', 'id': MSG_ID, 'method': 'methodName', 'params': {'key': 'value'} }) # Send an error back from the client error = exceptions.JsonRpcInvalidRequest(data=1234) endpoint.consume({ 'jsonrpc': '2.0', 'id': MSG_ID, 'error': error.to_dict() }) # Verify the exception raised by the future is the same as the error the client serialized with pytest.raises(exceptions.JsonRpcException) as exc_info: assert future.result(timeout=2) assert exc_info.type == exceptions.JsonRpcInvalidRequest assert exc_info.value == error def test_request_cancel(endpoint, consumer): future = endpoint.request('methodName', {'key': 'value'}) assert not future.done() consumer.assert_called_once_with({ 'jsonrpc': '2.0', 'id': MSG_ID, 'method': 'methodName', 'params': {'key': 'value'} }) # Cancel the request future.cancel() consumer.assert_any_call({ 'jsonrpc': '2.0', 'method': '$/cancelRequest', 'params': {'id': MSG_ID} }) with pytest.raises((exceptions.JsonRpcException, futures.CancelledError)) as exc_info: assert future.result(timeout=2) assert exc_info.type in (exceptions.JsonRpcRequestCancelled, futures.CancelledError) def test_consume_notification(endpoint, dispatcher): handler = mock.Mock() dispatcher['methodName'] = handler endpoint.consume({ 'jsonrpc': '2.0', 'method': 'methodName', 'params': {'key': 'value'} }) handler.assert_called_once_with({'key': 'value'}) def test_consume_notification_error(endpoint, dispatcher): handler = mock.Mock(side_effect=ValueError) dispatcher['methodName'] = handler # Verify the consume doesn't throw endpoint.consume({ 'jsonrpc': '2.0', 'method': 'methodName', 'params': {'key': 'value'} }) handler.assert_called_once_with({'key': 'value'}) def test_consume_notification_method_not_found(endpoint): # Verify consume doesn't throw for method not found endpoint.consume({ 'jsonrpc': '2.0', 'method': 'methodName', 'params': {'key': 'value'} }) def test_consume_async_notification_error(endpoint, dispatcher): def _async_handler(): raise ValueError() handler = mock.Mock(return_value=_async_handler) dispatcher['methodName'] = handler # Verify the consume doesn't throw endpoint.consume({ 'jsonrpc': '2.0', 'method': 'methodName', 'params': {'key': 'value'} }) handler.assert_called_once_with({'key': 'value'}) def test_consume_request(endpoint, consumer, dispatcher): result = 1234 handler = mock.Mock(return_value=result) dispatcher['methodName'] = handler endpoint.consume({ 'jsonrpc': '2.0', 'id': MSG_ID, 'method': 'methodName', 'params': {'key': 'value'} }) handler.assert_called_once_with({'key': 'value'}) consumer.assert_called_once_with({ 'jsonrpc': '2.0', 'id': MSG_ID, 'result': result }) def test_consume_future_request(endpoint, consumer, dispatcher): future_response = futures.ThreadPoolExecutor().submit(lambda: 1234) handler = mock.Mock(return_value=future_response) dispatcher['methodName'] = handler endpoint.consume({ 'jsonrpc': '2.0', 'id': MSG_ID, 'method': 'methodName', 'params': {'key': 'value'} }) handler.assert_called_once_with({'key': 'value'}) await_assertion(lambda: consumer.assert_called_once_with({ 'jsonrpc': '2.0', 'id': MSG_ID, 'result': 1234 })) def test_consume_async_request(endpoint, consumer, dispatcher): def _async_handler(): return 1234 handler = mock.Mock(return_value=_async_handler) dispatcher['methodName'] = handler endpoint.consume({ 'jsonrpc': '2.0', 'id': MSG_ID, 'method': 'methodName', 'params': {'key': 'value'} }) handler.assert_called_once_with({'key': 'value'}) await_assertion(lambda: consumer.assert_called_once_with({ 'jsonrpc': '2.0', 'id': MSG_ID, 'result': 1234 })) @pytest.mark.parametrize('exc_type, error', [ (ValueError, exceptions.JsonRpcInternalError(message='ValueError')), (KeyError, exceptions.JsonRpcInternalError(message='KeyError')), (exceptions.JsonRpcMethodNotFound, exceptions.JsonRpcMethodNotFound()), ]) def test_consume_async_request_error(exc_type, error, endpoint, consumer, dispatcher): def _async_handler(): raise exc_type() handler = mock.Mock(return_value=_async_handler) dispatcher['methodName'] = handler endpoint.consume({ 'jsonrpc': '2.0', 'id': MSG_ID, 'method': 'methodName', 'params': {'key': 'value'} }) handler.assert_called_once_with({'key': 'value'}) await_assertion(lambda: assert_consumer_error(consumer, error)) def test_consume_request_method_not_found(endpoint, consumer): endpoint.consume({ 'jsonrpc': '2.0', 'id': MSG_ID, 'method': 'methodName', 'params': {'key': 'value'} }) assert_consumer_error(consumer, exceptions.JsonRpcMethodNotFound.of('methodName')) @pytest.mark.parametrize('exc_type, error', [ (ValueError, exceptions.JsonRpcInternalError(message='ValueError')), (KeyError, exceptions.JsonRpcInternalError(message='KeyError')), (exceptions.JsonRpcMethodNotFound, exceptions.JsonRpcMethodNotFound()), ]) def test_consume_request_error(exc_type, error, endpoint, consumer, dispatcher): handler = mock.Mock(side_effect=exc_type) dispatcher['methodName'] = handler endpoint.consume({ 'jsonrpc': '2.0', 'id': MSG_ID, 'method': 'methodName', 'params': {'key': 'value'} }) handler.assert_called_once_with({'key': 'value'}) await_assertion(lambda: assert_consumer_error(consumer, error)) def test_consume_request_cancel(endpoint, dispatcher): def async_handler(): time.sleep(3) handler = mock.Mock(return_value=async_handler) dispatcher['methodName'] = handler endpoint.consume({ 'jsonrpc': '2.0', 'id': MSG_ID, 'method': 'methodName', 'params': {'key': 'value'} }) handler.assert_called_once_with({'key': 'value'}) endpoint.consume({ 'jsonrpc': '2.0', 'method': '$/cancelRequest', 'params': {'id': MSG_ID} }) # Because Python's Future cannot be cancelled once it's started, the request is never actually cancelled # consumer.assert_called_once_with({ # 'jsonrpc': '2.0', # 'id': MSG_ID, # 'error': exceptions.JsonRpcRequestCancelled().to_dict() # }) def test_consume_request_cancel_unknown(endpoint): # Verify consume doesn't throw endpoint.consume({ 'jsonrpc': '2.0', 'method': '$/cancelRequest', 'params': {'id': 'unknown identifier'} }) def assert_consumer_error(consumer_mock, exception): """Assert that the consumer mock has had once call with the given error message and code. The error's data part is not compared since it contains the traceback. """ assert len(consumer_mock.mock_calls) == 1 _name, args, _kwargs = consumer_mock.mock_calls[0] assert args[0]['error']['message'] == exception.message assert args[0]['error']['code'] == exception.code def await_assertion(condition, timeout=3.0, interval=0.1, exc=None): if timeout <= 0: raise exc if exc else AssertionError(f"Failed to wait for condition {condition}") try: condition() except AssertionError as e: time.sleep(interval) await_assertion(condition, timeout=(timeout - interval), interval=interval, exc=e) python-lsp-jsonrpc-1.1.2/test/test_streams.py000066400000000000000000000063251450361767500213760ustar00rootroot00000000000000# Copyright 2017-2020 Palantir Technologies, Inc. # Copyright 2021- Python Language Server Contributors. # pylint: disable=redefined-outer-name from io import BytesIO import datetime import sys from unittest import mock import pytest from pylsp_jsonrpc.streams import JsonRpcStreamReader, JsonRpcStreamWriter @pytest.fixture() def rfile(): return BytesIO() @pytest.fixture() def wfile(): return BytesIO() @pytest.fixture() def reader(rfile): return JsonRpcStreamReader(rfile) @pytest.fixture() def writer(wfile): return JsonRpcStreamWriter(wfile, sort_keys=True) def test_reader(rfile, reader): rfile.write( b'Content-Length: 49\r\n' b'Content-Type: application/vscode-jsonrpc; charset=utf8\r\n' b'\r\n' b'{"id": "hello", "method": "method", "params": {}}' ) rfile.seek(0) consumer = mock.Mock() reader.listen(consumer) consumer.assert_called_once_with({ 'id': 'hello', 'method': 'method', 'params': {} }) def test_reader_bad_message(rfile, reader): rfile.write(b'Hello world') rfile.seek(0) # Ensure the listener doesn't throw consumer = mock.Mock() reader.listen(consumer) consumer.assert_not_called() def test_reader_bad_json(rfile, reader): rfile.write( b'Content-Length: 8\r\n' b'Content-Type: application/vscode-jsonrpc; charset=utf8\r\n' b'\r\n' b'{hello}}' ) rfile.seek(0) # Ensure the listener doesn't throw consumer = mock.Mock() reader.listen(consumer) consumer.assert_not_called() def test_writer(wfile, writer): writer.write({ 'id': 'hello', 'method': 'method', 'params': {} }) if 'ujson' in sys.modules: assert wfile.getvalue() == ( b'Content-Length: 44\r\n' b'Content-Type: application/vscode-jsonrpc; charset=utf8\r\n' b'\r\n' b'{"id":"hello","method":"method","params":{}}' ) else: assert wfile.getvalue() == ( b'Content-Length: 49\r\n' b'Content-Type: application/vscode-jsonrpc; charset=utf8\r\n' b'\r\n' b'{"id": "hello", "method": "method", "params": {}}' ) class JsonDatetime(datetime.datetime): """Monkey path json datetime.""" def __json__(self): if sys.version_info.major == 3: dif = int(self.timestamp()) else: dif = int((self - datetime.datetime(1970, 1, 1)).total_seconds()) return f'{dif}' def test_writer_bad_message(wfile, writer): # A datetime isn't serializable(or poorly serializable), # ensure the write method doesn't throw, but the result could be empty # or the correct datetime datetime.datetime = JsonDatetime writer.write(datetime.datetime( year=2019, month=1, day=1, hour=1, minute=1, second=1, )) assert wfile.getvalue() in [ b'', b'Content-Length: 10\r\n' b'Content-Type: application/vscode-jsonrpc; charset=utf8\r\n' b'\r\n' b'1546304461', b'Content-Length: 10\r\n' b'Content-Type: application/vscode-jsonrpc; charset=utf8\r\n' b'\r\n' b'1546322461' ]