diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index f60bb1b..6811279 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -18,19 +18,16 @@ jobs: OS: 'linux' timeout-minutes: 2 steps: - - uses: actions/cache@v1 + - uses: actions/cache@v4 with: path: ~/.cache/pip key: static-pip-${{ hashFiles('setup.py') }} restore-keys: static-pip- - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: - # TODO: check with Python 3, but need to fix the - # errors first - python-version: '3.6' + 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 diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index 00299b5..846c70f 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -20,20 +20,19 @@ jobs: strategy: fail-fast: false matrix: - PYTHON_VERSION: ['3.9', '3.8', '3.7', '3.6'] + PYTHON_VERSION: ['3.10', '3.9', '3.8'] timeout-minutes: 10 steps: - - uses: actions/cache@v1 + - uses: actions/cache@v4 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 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 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 diff --git a/.github/workflows/test-mac.yml b/.github/workflows/test-mac.yml index c362753..a0fcbce 100644 --- a/.github/workflows/test-mac.yml +++ b/.github/workflows/test-mac.yml @@ -12,7 +12,7 @@ on: jobs: build: name: Mac Py${{ matrix.PYTHON_VERSION }} - runs-on: macos-latest + runs-on: macos-13 env: CI: 'true' OS: 'macos' @@ -20,20 +20,19 @@ jobs: strategy: fail-fast: false matrix: - PYTHON_VERSION: ['3.9', '3.8', '3.7', '3.6'] + PYTHON_VERSION: ['3.10', '3.9', '3.8'] timeout-minutes: 10 steps: - - uses: actions/cache@v1 + - uses: actions/cache@v4 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 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 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 diff --git a/.github/workflows/test-win.yml b/.github/workflows/test-win.yml index 0902f55..a089f1b 100644 --- a/.github/workflows/test-win.yml +++ b/.github/workflows/test-win.yml @@ -20,19 +20,18 @@ jobs: strategy: fail-fast: false matrix: - PYTHON_VERSION: ['3.9', '3.8', '3.7', '3.6'] + PYTHON_VERSION: ['3.10', '3.9', '3.8'] timeout-minutes: 10 steps: - - uses: actions/cache@v1 + - uses: actions/cache@v4 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 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 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/ diff --git a/.gitignore b/.gitignore index 3fc908a..95df41c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# autogenerated version file +/pylsp_jsonrpc/_version.py + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c592da..45bc81a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,61 @@ +# 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 diff --git a/README.md b/README.md index c3c1b3a..db5f20e 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ # Python JSON RPC Server -A Python 3.6+ 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. +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-jsonrpc-server + 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-language-server` +Start by installing `tornado` and `python-lsp-server` pip install python-lsp-server[all] tornado diff --git a/RELEASE.md b/RELEASE.md index e63591d..e0c9525 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,17 +1,22 @@ -To release a new version of python-lsp-jsonrpc: +## 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. Update CHANGELOG.md with loghub -5. git add -A && git commit -m "Update Changelog" -6. Update release version in ``_version.py`` (set release version, remove 'dev0') -7. git add -A && git commit -m "Release vX.X.X" -8. python setup.py sdist -9. python setup.py bdist_wheel -10. twine check -11. twine upload -12. git tag -a vX.X.X -m "Release vX.X.X" -13. Update development version in ``_version.py`` (add 'dev0' and increment minor) -14. git add -A && git commit -m "Back to work" -15. git push upstream master -16. git push upstream --tags +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 diff --git a/pylsp_jsonrpc/__init__.py b/pylsp_jsonrpc/__init__.py index 8f327dd..03314c6 100644 --- a/pylsp_jsonrpc/__init__.py +++ b/pylsp_jsonrpc/__init__.py @@ -1,6 +1,23 @@ # Copyright 2017-2020 Palantir Technologies, Inc. # Copyright 2021- Python Language Server Contributors. +from . import _version from ._version import __version__ -__all__ = [__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__",) diff --git a/pylsp_jsonrpc/_version.py b/pylsp_jsonrpc/_version.py deleted file mode 100644 index 7d546ba..0000000 --- a/pylsp_jsonrpc/_version.py +++ /dev/null @@ -1,5 +0,0 @@ -# Copyright 2017-2020 Palantir Technologies, Inc. -# Copyright 2021- Python Language Server Contributors. - -VERSION_INFO = (1, 0, 0) -__version__ = '.'.join(map(str, VERSION_INFO)) diff --git a/pylsp_jsonrpc/dispatchers.py b/pylsp_jsonrpc/dispatchers.py index 2dc81e1..03e10f9 100644 --- a/pylsp_jsonrpc/dispatchers.py +++ b/pylsp_jsonrpc/dispatchers.py @@ -16,7 +16,7 @@ class MethodDispatcher: """ def __getitem__(self, item): - method_name = 'm_{}'.format(_method_to_string(item)) + method_name = f'm_{_method_to_string(item)}' if hasattr(self, method_name): method = getattr(self, method_name) diff --git a/pylsp_jsonrpc/endpoint.py b/pylsp_jsonrpc/endpoint.py index 9bbe4c8..0c9e62a 100644 --- a/pylsp_jsonrpc/endpoint.py +++ b/pylsp_jsonrpc/endpoint.py @@ -4,6 +4,7 @@ import logging import uuid import sys +from typing import Any, Dict, Mapping from concurrent import futures from .exceptions import (JsonRpcException, JsonRpcRequestCancelled, @@ -175,6 +176,17 @@ def _handle_cancel_notification(self, msg_id): 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: @@ -195,11 +207,14 @@ def _handle_request(self, msg_id, method, params): handler_result.add_done_callback(self._request_callback(msg_id)) else: log.debug("Got result from synchronous request handler: %s", handler_result) - self._consumer({ - 'jsonrpc': JSONRPC_VERSION, - 'id': msg_id, - 'result': 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.""" @@ -216,7 +231,8 @@ def callback(future): } try: - message['result'] = future.result() + 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() diff --git a/pylsp_jsonrpc/streams.py b/pylsp_jsonrpc/streams.py index ac7a005..40048a9 100644 --- a/pylsp_jsonrpc/streams.py +++ b/pylsp_jsonrpc/streams.py @@ -74,7 +74,7 @@ def _content_length(line): try: return int(value) except ValueError as e: - raise ValueError("Invalid Content-Length header: {}".format(value)) from e + raise ValueError(f"Invalid Content-Length header: {value}") from e return None @@ -100,9 +100,9 @@ def write(self, message): content_length = len(body) if isinstance(body, bytes) else len(body.encode('utf-8')) response = ( - "Content-Length: {}\r\n" - "Content-Type: application/vscode-jsonrpc; charset=utf8\r\n\r\n" - "{}".format(content_length, body) + 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')) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..376785c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,51 @@ +# 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" diff --git a/setup.cfg b/setup.cfg index 3c9d096..933ff6a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,11 +1,7 @@ +# 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 - -[tool:pytest] -testpaths = test -addopts = - --cov-report html --cov-report term --junitxml=pytest.xml - --cov pylsp_jsonrpc --cov test diff --git a/setup.py b/setup.py deleted file mode 100755 index 7950d7e..0000000 --- a/setup.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2017-2020 Palantir Technologies, Inc. -# Copyright 2021- Python Language Server Contributors. - -import ast -import os -from setuptools import find_packages, setup - -HERE = os.path.abspath(os.path.dirname(__file__)) - - -def get_version(module='pylsp_jsonrpc'): - """Get version.""" - with open(os.path.join(HERE, module, '_version.py'), 'r') as f: - data = f.read() - lines = data.split('\n') - for line in lines: - if line.startswith('VERSION_INFO'): - version_tuple = ast.literal_eval(line.split('=')[-1].strip()) - version = '.'.join(map(str, version_tuple)) - break - return version - - -README = open('README.md', 'r').read() - - -setup( - name='python-lsp-jsonrpc', - version=get_version(), - description='JSON RPC 2.0 server library', - long_description=README, - long_description_content_type='text/markdown', - url='https://github.com/python-lsp/python-lsp-jsonrpc', - author='Python Language Server Contributors', - packages=find_packages(exclude=['contrib', 'docs', 'test']), - install_requires=[ - 'ujson>=3.0.0', - ], - extras_require={ - 'test': ['pylint', 'pycodestyle', 'pyflakes', 'pytest', - 'pytest-cov', 'coverage'], - }, -) diff --git a/test/test_endpoint.py b/test/test_endpoint.py index 350aaed..08fb62d 100644 --- a/test/test_endpoint.py +++ b/test/test_endpoint.py @@ -332,7 +332,7 @@ def assert_consumer_error(consumer_mock, exception): def await_assertion(condition, timeout=3.0, interval=0.1, exc=None): if timeout <= 0: - raise exc if exc else AssertionError("Failed to wait for condition %s" % condition) + raise exc if exc else AssertionError(f"Failed to wait for condition {condition}") try: condition() except AssertionError as e: diff --git a/test/test_streams.py b/test/test_streams.py index bce3ffa..14fe1bb 100644 --- a/test/test_streams.py +++ b/test/test_streams.py @@ -82,13 +82,20 @@ def test_writer(wfile, writer): 'method': 'method', 'params': {} }) - - 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":{}}' - ) + 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): @@ -98,7 +105,7 @@ def __json__(self): dif = int(self.timestamp()) else: dif = int((self - datetime.datetime(1970, 1, 1)).total_seconds()) - return '{0}'.format(dif) + return f'{dif}' def test_writer_bad_message(wfile, writer):