From 0072ccdd9fb29a2b57fd6114cf96f8368fb4fd42 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 18:49:21 +0000 Subject: [PATCH 01/20] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0) - [github.com/astral-sh/ruff-pre-commit: v0.5.2 → v0.6.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.2...v0.6.9) - [github.com/pre-commit/mirrors-mypy: v1.10.1 → v1.11.2](https://github.com/pre-commit/mirrors-mypy/compare/v1.10.1...v1.11.2) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 573e1f3..fc82045 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ exclude: '.bumpversion.cfg$' repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-yaml - id: end-of-file-fixer @@ -10,7 +10,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.2 + rev: v0.6.9 hooks: - id: ruff args: [--fix] @@ -20,7 +20,7 @@ repos: files: 'lib/.*\.py' - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v1.10.1' + rev: 'v1.11.2' hooks: - id: mypy name: mypy (pytest-lsp) From a10ffe6069f9fcbc5823ee63aff016e5429ed28e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 18:50:07 +0000 Subject: [PATCH 02/20] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- lib/pytest-lsp/pytest_lsp/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pytest-lsp/pytest_lsp/plugin.py b/lib/pytest-lsp/pytest_lsp/plugin.py index 95f222a..814e85f 100644 --- a/lib/pytest-lsp/pytest_lsp/plugin.py +++ b/lib/pytest-lsp/pytest_lsp/plugin.py @@ -132,7 +132,7 @@ def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo): # anext() was added in 3.10 if sys.version_info < (3, 10): - async def anext(it): # noqa: A001 + async def anext(it): return await it.__anext__() From 2a04bfae1f254006910e9e43891f4e081149d5d9 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Mon, 4 Nov 2024 20:13:36 +0000 Subject: [PATCH 03/20] pytest-lsp: Bump pygls version --- lib/pytest-lsp/changes/188.misc.md | 1 + lib/pytest-lsp/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 lib/pytest-lsp/changes/188.misc.md diff --git a/lib/pytest-lsp/changes/188.misc.md b/lib/pytest-lsp/changes/188.misc.md new file mode 100644 index 0000000..c975a77 --- /dev/null +++ b/lib/pytest-lsp/changes/188.misc.md @@ -0,0 +1 @@ +Update `pygls` to `v2.0a2` diff --git a/lib/pytest-lsp/pyproject.toml b/lib/pytest-lsp/pyproject.toml index 5076714..c4c8495 100644 --- a/lib/pytest-lsp/pyproject.toml +++ b/lib/pytest-lsp/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ ] dependencies = [ "packaging", - "pygls>=2.0.0a1", + "pygls>=2.0.0a2", "pytest", "pytest-asyncio>=0.24", ] From ad7b7629ba0642fdc6b599f3a00da4fbcb422130 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Mon, 4 Nov 2024 20:12:27 +0000 Subject: [PATCH 04/20] pytest-lsp: Stop cancelling requests on server exit The upstream client from pygls now handles this --- lib/pytest-lsp/pytest_lsp/client.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/pytest-lsp/pytest_lsp/client.py b/lib/pytest-lsp/pytest_lsp/client.py index f573685..ddba624 100644 --- a/lib/pytest-lsp/pytest_lsp/client.py +++ b/lib/pytest-lsp/pytest_lsp/client.py @@ -99,15 +99,6 @@ async def server_exit(self, server: asyncio.subprocess.Process): if self._stop_event.is_set(): return - # TODO: Should the upstream base client be doing this? - # Cancel any pending futures. - reason = f"Server process {server.pid} exited with code: {server.returncode}" - - for id_, fut in self.protocol._request_futures.items(): - if not fut.done(): - fut.set_exception(RuntimeError(reason)) - logger.debug("Cancelled pending request '%s': %s", id_, reason) - def report_server_error( self, error: Exception, source: PyglsError | JsonRpcException ): From a37bd191ba28ed8848e40e894bff7615609c6bab Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Mon, 4 Nov 2024 20:14:09 +0000 Subject: [PATCH 05/20] pytest-lsp: Cancel requests and kill server process on error --- lib/pytest-lsp/pytest_lsp/client.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/pytest-lsp/pytest_lsp/client.py b/lib/pytest-lsp/pytest_lsp/client.py index ddba624..ba7e745 100644 --- a/lib/pytest-lsp/pytest_lsp/client.py +++ b/lib/pytest-lsp/pytest_lsp/client.py @@ -108,12 +108,13 @@ def report_server_error( tb = "".join(traceback.format_exc()) message = f"{source.__name__}: {error}\n{tb}" # type: ignore + for id_, fut in self.protocol._request_futures.items(): + if not fut.done(): + fut.set_exception(RuntimeError(message)) + logger.debug("Cancelled pending request '%s': %s", id_, message) - loop = asyncio.get_running_loop() - loop.call_soon(cancel_all_tasks, message) - - if self._stop_event: - self._stop_event.set() + if self._server: + self._server.terminate() def get_configuration( self, *, section: str | None = None, scope_uri: str | None = None From e6b1854b9c589e8411f9afa78d8450cc34748a5e Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Mon, 4 Nov 2024 20:15:01 +0000 Subject: [PATCH 06/20] pytest-lsp: Align test cases to changes in upstream pygls --- .../tests/examples/generic-rpc/server.py | 7 +++++++ .../tests/examples/generic-rpc/t_server.py | 3 ++- lib/pytest-lsp/tests/servers/invalid_json.py | 6 +++++- lib/pytest-lsp/tests/test_examples.py | 2 +- lib/pytest-lsp/tests/test_plugin.py | 14 +++++--------- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/pytest-lsp/tests/examples/generic-rpc/server.py b/lib/pytest-lsp/tests/examples/generic-rpc/server.py index 20cc043..d946d2a 100644 --- a/lib/pytest-lsp/tests/examples/generic-rpc/server.py +++ b/lib/pytest-lsp/tests/examples/generic-rpc/server.py @@ -1,3 +1,5 @@ +import sys + from pygls.protocol import JsonRPCProtocol, default_converter from pygls.server import JsonRPCServer @@ -28,5 +30,10 @@ def subtraction(ls: JsonRPCServer, params): return dict(total=b - a) +@server.feature("server/exit") +def server_exit(ls: JsonRPCServer, params): + sys.exit(0) + + if __name__ == "__main__": server.start_io() diff --git a/lib/pytest-lsp/tests/examples/generic-rpc/t_server.py b/lib/pytest-lsp/tests/examples/generic-rpc/t_server.py index 38b6e3c..519c2c2 100644 --- a/lib/pytest-lsp/tests/examples/generic-rpc/t_server.py +++ b/lib/pytest-lsp/tests/examples/generic-rpc/t_server.py @@ -27,7 +27,8 @@ async def client(rpc_client: JsonRPCClient): yield - # Teardown code here (if any) + # Teardown code here + rpc_client.protocol.notify("server/exit", {}) @pytest.mark.asyncio diff --git a/lib/pytest-lsp/tests/servers/invalid_json.py b/lib/pytest-lsp/tests/servers/invalid_json.py index 149394c..857ee6c 100644 --- a/lib/pytest-lsp/tests/servers/invalid_json.py +++ b/lib/pytest-lsp/tests/servers/invalid_json.py @@ -1,7 +1,9 @@ # A server that returns a message that cannot be parsed as JSON. import json +import sys from lsprotocol import types +from pygls.io_ import StdoutWriter from pygls.lsp.server import LanguageServer server = LanguageServer(name="completion-exit-server", version="v1.0") @@ -20,12 +22,14 @@ def bad_send_data(data): f"Content-Type: {self.CONTENT_TYPE}; charset={self.CHARSET}\r\n\r\n" ).encode(self.CHARSET) - self.transport.write(header + body) + self.writer.write(header + body) @server.feature(types.TEXT_DOCUMENT_COMPLETION) def on_complete(server: LanguageServer, params: types.CompletionParams): server.protocol._send_data = bad_send_data + server.protocol.set_writer(StdoutWriter(sys.stdout.buffer)) + return [types.CompletionItem(label="item-one")] diff --git a/lib/pytest-lsp/tests/test_examples.py b/lib/pytest-lsp/tests/test_examples.py index 5e5055d..21ee1d1 100644 --- a/lib/pytest-lsp/tests/test_examples.py +++ b/lib/pytest-lsp/tests/test_examples.py @@ -116,7 +116,7 @@ def test_getting_started_fail(pytester: pytest.Pytester): results = pytester.runpytest() results.assert_outcomes(errors=1) - message = r"E\s+RuntimeError: Server process \d+ exited with code: 0" + message = r"E\s+RuntimeError: Server process \d+ exited with return code: 0" results.stdout.re_match_lines(message) diff --git a/lib/pytest-lsp/tests/test_plugin.py b/lib/pytest-lsp/tests/test_plugin.py index 69a1f8e..b3960ee 100644 --- a/lib/pytest-lsp/tests/test_plugin.py +++ b/lib/pytest-lsp/tests/test_plugin.py @@ -130,7 +130,7 @@ async def test_capabilities(client): results.assert_outcomes(errors=1) - message = r"E\s+RuntimeError: Server process \d+ exited with code: 0" + message = r"E\s+RuntimeError: Server process \d+ exited with return code: 0" results.stdout.re_match_lines(message) @@ -162,7 +162,7 @@ async def test_capabilities(client): results.assert_outcomes(failed=1, errors=1) - message = r"E\s+RuntimeError: Server process \d+ exited with code: 0" + message = r"E\s+RuntimeError: Server process \d+ exited with return code: 0" results.stdout.re_match_lines(message) results.stdout.fnmatch_lines("E*RuntimeError: Client has been stopped.") @@ -183,7 +183,7 @@ async def test_capabilities(client): results.assert_outcomes(errors=1) - message = r"E\s+RuntimeError: Server process \d+ exited with code: 1" + message = r"E\s+RuntimeError: Server process \d+ exited with return code: 1" results.stdout.re_match_lines(message) results.stdout.fnmatch_lines("ZeroDivisionError: division by zero") @@ -215,11 +215,7 @@ async def test_capabilities(client): setup_test(pytester, "invalid_json.py", test_code) results = pytester.runpytest("-vv") - results.assert_outcomes(errors=1, failed=1) - - if sys.version_info < (3, 9): - message = "E*CancelledError" - else: - message = "E*asyncio.exceptions.CancelledError: JsonRpcInternalError: *" + results.assert_outcomes(failed=1) + message = "E*json.decoder.JSONDecodeError: *" results.stdout.fnmatch_lines(message) From 5ce668a7f201369accb6e55c7588abf6ca5789a1 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Mon, 4 Nov 2024 20:20:21 +0000 Subject: [PATCH 07/20] Fix link on develop --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 05d20da..5a9c36a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,7 +20,7 @@ Client Capability Index capabilities/* -Inspired by `caniuse.com `__ this provides information on which clients support which features of the `LSP Specification `__. +Inspired by `caniuse.com `__ this provides information on which clients support which features of the `LSP Specification `__. .. grid:: 2 :gutter: 2 From 42403fbba210df67f615e3b678f4bb64c7b797a5 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Mon, 4 Nov 2024 20:24:25 +0000 Subject: [PATCH 08/20] docs: Update generic rpc server guide --- docs/pytest-lsp/howto/testing-json-rpc-servers.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/pytest-lsp/howto/testing-json-rpc-servers.rst b/docs/pytest-lsp/howto/testing-json-rpc-servers.rst index f6e238e..0fb81aa 100644 --- a/docs/pytest-lsp/howto/testing-json-rpc-servers.rst +++ b/docs/pytest-lsp/howto/testing-json-rpc-servers.rst @@ -10,6 +10,7 @@ As an example we'll reuse some of the `pygls`_ internals to write a simple JSON- - client to server request ``math/add``, returns the sum of two numbers ``a`` and ``b`` - client to server request ``math/sub``, returns the difference of two numbers ``a`` and ``b`` +- client to server notification ``server/exit`` that instructs the server to exit - server to client notification ``log/message``, allows the server to send debug messages to the client. .. note:: @@ -40,7 +41,7 @@ Once you have your factory function defined you can pass it to the :class:`~pyte .. literalinclude:: ../../../lib/pytest-lsp/tests/examples/generic-rpc/t_server.py :language: python :start-at: @pytest_lsp.fixture( - :end-at: # Teardown code + :end-at: rpc_client.protocol.notify Writing Test Cases ------------------ From fbe8d71b4ac93b3ac3e43bb0441035ef562bf012 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Mon, 4 Nov 2024 20:48:27 +0000 Subject: [PATCH 09/20] pytest-lsp: Cancel pendining notification futures on server exit --- lib/pytest-lsp/changes/186.fix.md | 1 + lib/pytest-lsp/pytest_lsp/client.py | 8 +++++ lib/pytest-lsp/tests/servers/notify_exit.py | 30 +++++++++++++++++ lib/pytest-lsp/tests/test_plugin.py | 37 +++++++++++++++++++++ 4 files changed, 76 insertions(+) create mode 100644 lib/pytest-lsp/changes/186.fix.md create mode 100644 lib/pytest-lsp/tests/servers/notify_exit.py diff --git a/lib/pytest-lsp/changes/186.fix.md b/lib/pytest-lsp/changes/186.fix.md new file mode 100644 index 0000000..40a9df9 --- /dev/null +++ b/lib/pytest-lsp/changes/186.fix.md @@ -0,0 +1 @@ +`pytest-lsp` is now able to detect the situation where the server process exits while the client is waiting on a notification message and fail the test accordingly diff --git a/lib/pytest-lsp/pytest_lsp/client.py b/lib/pytest-lsp/pytest_lsp/client.py index ba7e745..0df91e5 100644 --- a/lib/pytest-lsp/pytest_lsp/client.py +++ b/lib/pytest-lsp/pytest_lsp/client.py @@ -99,6 +99,14 @@ async def server_exit(self, server: asyncio.subprocess.Process): if self._stop_event.is_set(): return + reason = ( + f"Server process {server.pid} exited with return code: {server.returncode}" + ) + for id_, fut in self.protocol._notification_futures.items(): + if not fut.done(): + fut.set_exception(RuntimeError(reason)) + logger.debug("Cancelled pending request '%s': %s", id_, reason) + def report_server_error( self, error: Exception, source: PyglsError | JsonRpcException ): diff --git a/lib/pytest-lsp/tests/servers/notify_exit.py b/lib/pytest-lsp/tests/servers/notify_exit.py new file mode 100644 index 0000000..d85dff4 --- /dev/null +++ b/lib/pytest-lsp/tests/servers/notify_exit.py @@ -0,0 +1,30 @@ +# A server that exits mid request. +import sys + +from lsprotocol import types +from pygls.lsp.server import LanguageServer + + +class CountingLanguageServer(LanguageServer): + count: int = 0 + + +server = CountingLanguageServer(name="completion-exit-server", version="v1.0") + + +@server.feature("server/exit") +def server_exit(*args): + sys.exit(0) + + +@server.feature(types.TEXT_DOCUMENT_COMPLETION) +def on_complete(server: CountingLanguageServer, params: types.CompletionParams): + server.count += 1 + if server.count == 5: + sys.exit(0) + + return [types.CompletionItem(label=f"{server.count}")] + + +if __name__ == "__main__": + server.start_io() diff --git a/lib/pytest-lsp/tests/test_plugin.py b/lib/pytest-lsp/tests/test_plugin.py index b3960ee..06439d4 100644 --- a/lib/pytest-lsp/tests/test_plugin.py +++ b/lib/pytest-lsp/tests/test_plugin.py @@ -167,6 +167,43 @@ async def test_capabilities(client): results.stdout.fnmatch_lines("E*RuntimeError: Client has been stopped.") +def test_detect_server_exit_pending_notification(pytester: pytest.Pytester): + """Ensure that the plugin can detect when the server process exits while the client + is waiting for a notification to arrive.""" + + test_code = """\ +import pytest +from lsprotocol.types import CompletionParams +from lsprotocol.types import Position +from lsprotocol.types import TextDocumentIdentifier + + +@pytest.mark.asyncio +async def test_capabilities(client): + expected = {str(i) for i in range(10)} + + for i in range(10): + client.protocol.notify("server/exit") + await client.wait_for_notification("never/happening") + + params = CompletionParams( + text_document=TextDocumentIdentifier(uri="file:///test.txt"), + position=Position(line=0, character=0) + ) + items = await client.text_document_completion_async(params) + assert len({i.label for i in items} & expected) == len(items) +""" + + setup_test(pytester, "notify_exit.py", test_code) + results = pytester.runpytest("-vv") + + results.assert_outcomes(failed=1, errors=1) + + message = r"E\s+RuntimeError: Server process \d+ exited with return code: 0" + results.stdout.re_match_lines(message) + results.stdout.fnmatch_lines("E*RuntimeError: Client has been stopped.") + + def test_detect_server_crash(pytester: pytest.Pytester): """Ensure the plugin can detect when the server process crashes on boot.""" From a7abb52ce464f8458350c741bb65b7eaa5ced5bb Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Wed, 6 Nov 2024 18:57:38 +0000 Subject: [PATCH 10/20] lsp-devtools: Drop Python 3.8 support --- .github/workflows/lsp-devtools-pr.yml | 2 +- lib/lsp-devtools/changes/190.misc.md | 1 + lib/lsp-devtools/pyproject.toml | 7 ++----- 3 files changed, 4 insertions(+), 6 deletions(-) create mode 100644 lib/lsp-devtools/changes/190.misc.md diff --git a/.github/workflows/lsp-devtools-pr.yml b/.github/workflows/lsp-devtools-pr.yml index 87542ed..c640450 100644 --- a/.github/workflows/lsp-devtools-pr.yml +++ b/.github/workflows/lsp-devtools-pr.yml @@ -41,7 +41,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12"] os: [ubuntu-latest] steps: diff --git a/lib/lsp-devtools/changes/190.misc.md b/lib/lsp-devtools/changes/190.misc.md new file mode 100644 index 0000000..a7d42d9 --- /dev/null +++ b/lib/lsp-devtools/changes/190.misc.md @@ -0,0 +1 @@ +Drop Python 3.8 support diff --git a/lib/lsp-devtools/pyproject.toml b/lib/lsp-devtools/pyproject.toml index edf3b62..531c50f 100644 --- a/lib/lsp-devtools/pyproject.toml +++ b/lib/lsp-devtools/pyproject.toml @@ -7,7 +7,7 @@ name = "lsp-devtools" dynamic = ["version"] description = "Developer tooling for language servers" readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" license = { text = "MIT" } authors = [{ name = "Alex Carney", email = "alcarneyme@gmail.com" }] classifiers = [ @@ -16,7 +16,6 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -24,12 +23,10 @@ classifiers = [ ] dependencies = [ "aiosqlite", - "importlib-resources; python_version<\"3.9\"", "platformdirs", - "pygls>=1.1.0", + "pygls>=1.1.0,<2", "stamina", "textual>=0.41.0", - "typing-extensions; python_version<\"3.8\"", ] [project.urls] From f4558ad38e27f45f8a39388c99267dccf2a762ee Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Wed, 6 Nov 2024 19:20:31 +0000 Subject: [PATCH 11/20] lsp-devtools: pyupgrade --py39-plus --- .../lsp_devtools/agent/__init__.py | 7 ++-- lib/lsp-devtools/lsp_devtools/agent/agent.py | 25 ++++++--------- lib/lsp-devtools/lsp_devtools/agent/client.py | 6 ++-- lib/lsp-devtools/lsp_devtools/agent/server.py | 16 ++++------ .../lsp_devtools/client/__init__.py | 9 +++--- .../lsp_devtools/client/editor/text_editor.py | 3 +- lib/lsp-devtools/lsp_devtools/database.py | 18 +++-------- .../lsp_devtools/handlers/__init__.py | 13 ++++---- lib/lsp-devtools/lsp_devtools/handlers/sql.py | 7 +--- .../lsp_devtools/inspector/__init__.py | 21 ++++++------ .../lsp_devtools/record/__init__.py | 18 +++++------ .../lsp_devtools/record/filters.py | 28 ++++++++-------- .../lsp_devtools/record/formatters.py | 32 ++++++++----------- .../lsp_devtools/record/visualize.py | 11 +++---- lib/lsp-devtools/pyproject.toml | 3 +- lib/lsp-devtools/tests/record/test_filters.py | 14 ++++---- lib/lsp-devtools/tests/record/test_record.py | 6 ++-- 17 files changed, 102 insertions(+), 135 deletions(-) diff --git a/lib/lsp-devtools/lsp_devtools/agent/__init__.py b/lib/lsp-devtools/lsp_devtools/agent/__init__.py index 14556dd..82c0cd7 100644 --- a/lib/lsp-devtools/lsp_devtools/agent/__init__.py +++ b/lib/lsp-devtools/lsp_devtools/agent/__init__.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import argparse import asyncio import subprocess import sys -from typing import List from .agent import Agent from .agent import RPCMessage @@ -31,7 +32,7 @@ async def forward_stderr(server: asyncio.subprocess.Process): sys.stderr.buffer.write(line) -async def main(args, extra: List[str]): +async def main(args, extra: list[str]): if extra is None: print("Missing server start command", file=sys.stderr) return 1 @@ -54,7 +55,7 @@ async def main(args, extra: List[str]): ) -def run_agent(args, extra: List[str]): +def run_agent(args, extra: list[str]): asyncio.run(main(args, extra)) diff --git a/lib/lsp-devtools/lsp_devtools/agent/agent.py b/lib/lsp-devtools/lsp_devtools/agent/agent.py index 2c9ea1d..4d0e26f 100644 --- a/lib/lsp-devtools/lsp_devtools/agent/agent.py +++ b/lib/lsp-devtools/lsp_devtools/agent/agent.py @@ -15,14 +15,10 @@ import attrs if typing.TYPE_CHECKING: + from collections.abc import Coroutine from typing import Any from typing import BinaryIO from typing import Callable - from typing import Coroutine - from typing import Dict - from typing import Optional - from typing import Set - from typing import Tuple from typing import Union MessageHandler = Callable[[bytes], Union[None, Coroutine[Any, Any, None]]] @@ -35,9 +31,9 @@ class RPCMessage: """A Json-RPC message.""" - headers: Dict[str, str] + headers: dict[str, str] - body: Dict[str, Any] + body: dict[str, Any] def __getitem__(self, key: str): return self.headers[key] @@ -46,8 +42,8 @@ def __getitem__(self, key: str): def parse_rpc_message(data: bytes) -> RPCMessage: """Parse a JSON-RPC message from the given set of bytes.""" - headers: Dict[str, str] = {} - body: Optional[Dict[str, Any]] = None + headers: dict[str, str] = {} + body: dict[str, Any] | None = None headers_complete = False for line in data.split(b"\r\n"): @@ -118,7 +114,7 @@ async def aio_readline(reader: asyncio.StreamReader, message_handler: MessageHan async def get_streams( stdin, stdout -) -> Tuple[asyncio.StreamReader, asyncio.StreamWriter]: +) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]: """Convert blocking stdin/stdout streams into async streams.""" loop = asyncio.get_running_loop() @@ -150,9 +146,9 @@ def __init__( self.handler = handler self.session_id = str(uuid4()) - self._tasks: Set[asyncio.Task] = set() - self.reader: Optional[asyncio.StreamReader] = None - self.writer: Optional[asyncio.StreamWriter] = None + self._tasks: set[asyncio.Task] = set() + self.reader: asyncio.StreamReader | None = None + self.writer: asyncio.StreamWriter | None = None async def start(self): # Get async versions of stdin/stdout @@ -227,8 +223,7 @@ async def stop(self): self.server.kill() args = {} - if sys.version_info >= (3, 9): - args["msg"] = "lsp-devtools agent is stopping." + args["msg"] = "lsp-devtools agent is stopping." # Cancel the tasks connecting client to server for task in self._tasks: diff --git a/lib/lsp-devtools/lsp_devtools/agent/client.py b/lib/lsp-devtools/lsp_devtools/agent/client.py index 1b65cfc..87e9d4c 100644 --- a/lib/lsp-devtools/lsp_devtools/agent/client.py +++ b/lib/lsp-devtools/lsp_devtools/agent/client.py @@ -12,8 +12,6 @@ if typing.TYPE_CHECKING: from typing import Any - from typing import List - from typing import Optional # from websockets.client import WebSocketClientProtocol @@ -45,14 +43,14 @@ def __init__(self): protocol_cls=AgentProtocol, converter_factory=default_converter ) self.connected = False - self._buffer: List[bytes] = [] + self._buffer: list[bytes] = [] def _report_server_error(self, error, source): # Bail on error # TODO: Report the actual error somehow self._stop_event.set() - def feature(self, feature_name: str, options: Optional[Any] = None): + def feature(self, feature_name: str, options: Any | None = None): return self.protocol.fm.feature(feature_name, options) # TODO: Upstream this... or at least something equivalent. diff --git a/lib/lsp-devtools/lsp_devtools/agent/server.py b/lib/lsp-devtools/lsp_devtools/agent/server.py index 17805d7..f74a47a 100644 --- a/lib/lsp-devtools/lsp_devtools/agent/server.py +++ b/lib/lsp-devtools/lsp_devtools/agent/server.py @@ -14,8 +14,6 @@ if typing.TYPE_CHECKING: from typing import Any - from typing import List - from typing import Optional from lsp_devtools.agent.agent import MessageHandler @@ -29,8 +27,8 @@ class AgentServer(Server): def __init__( self, *args, - logger: Optional[logging.Logger] = None, - handler: Optional[MessageHandler] = None, + logger: logging.Logger | None = None, + handler: MessageHandler | None = None, **kwargs, ): if "protocol_cls" not in kwargs: @@ -43,11 +41,11 @@ def __init__( self.logger = logger or logging.getLogger(__name__) self.handler = handler or self.lsp.data_received - self.db: Optional[Database] = None + self.db: Database | None = None - self._client_buffer: List[str] = [] - self._server_buffer: List[str] = [] - self._tcp_server: Optional[asyncio.Task] = None + self._client_buffer: list[str] = [] + self._server_buffer: list[str] = [] + self._tcp_server: asyncio.Task | None = None def _report_server_error(self, exc: Exception, source): """Report internal server errors.""" @@ -55,7 +53,7 @@ def _report_server_error(self, exc: Exception, source): self.logger.error("%s: %s", type(exc).__name__, exc) self.logger.debug("%s", tb) - def feature(self, feature_name: str, options: Optional[Any] = None): + def feature(self, feature_name: str, options: Any | None = None): return self.lsp.fm.feature(feature_name, options) async def start_tcp(self, host: str, port: int) -> None: # type: ignore[override] diff --git a/lib/lsp-devtools/lsp_devtools/client/__init__.py b/lib/lsp-devtools/lsp_devtools/client/__init__.py index 42ba9c0..ff7d951 100644 --- a/lib/lsp-devtools/lsp_devtools/client/__init__.py +++ b/lib/lsp-devtools/lsp_devtools/client/__init__.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import argparse import asyncio import logging import os import pathlib -from typing import List from uuid import uuid4 import platformdirs @@ -54,7 +55,7 @@ class LSPClient(App): ] def __init__( - self, db: Database, server_command: List[str], session: str, *args, **kwargs + self, db: Database, server_command: list[str], session: str, *args, **kwargs ): super().__init__(*args, **kwargs) @@ -65,7 +66,7 @@ def __init__( self.server_command = server_command self.lsp_client = LanguageClient() - self._async_tasks: List[asyncio.Task] = [] + self._async_tasks: list[asyncio.Task] = [] def compose(self) -> ComposeResult: message_viewer = MessageViewer("") @@ -140,7 +141,7 @@ async def action_quit(self): await super().action_quit() -def client(args, extra: List[str]): +def client(args, extra: list[str]): if len(extra) == 0: raise ValueError("Missing server command.") diff --git a/lib/lsp-devtools/lsp_devtools/client/editor/text_editor.py b/lib/lsp-devtools/lsp_devtools/client/editor/text_editor.py index 74b256c..ee051c0 100644 --- a/lib/lsp-devtools/lsp_devtools/client/editor/text_editor.py +++ b/lib/lsp-devtools/lsp_devtools/client/editor/text_editor.py @@ -3,7 +3,6 @@ import contextlib import pathlib import typing -from typing import List from typing import Union from lsprotocol import types @@ -15,7 +14,7 @@ if typing.TYPE_CHECKING: from lsp_devtools.client.lsp import LanguageClient -CompletionResult = Union[List[types.CompletionItem], types.CompletionList, None] +CompletionResult = Union[list[types.CompletionItem], types.CompletionList, None] # TODO: Refactor to diff --git a/lib/lsp-devtools/lsp_devtools/database.py b/lib/lsp-devtools/lsp_devtools/database.py index d1aef89..14e6641 100644 --- a/lib/lsp-devtools/lsp_devtools/database.py +++ b/lib/lsp-devtools/lsp_devtools/database.py @@ -2,13 +2,10 @@ import json import logging import pathlib -import sys from contextlib import asynccontextmanager +from importlib import resources from typing import Any -from typing import Dict -from typing import List from typing import Optional -from typing import Set import aiosqlite from textual.app import App @@ -16,11 +13,6 @@ from lsp_devtools.handlers import LspMessage -if sys.version_info < (3, 9): - import importlib_resources as resources -else: - from importlib import resources # type: ignore[no-redef] - class Database: """Controls access to the backing sqlite database.""" @@ -32,7 +24,7 @@ def __init__(self, dbpath: Optional[pathlib.Path] = None): self.dbpath = dbpath or ":memory:" self.db: Optional[aiosqlite.Connection] = None self.app: Optional[App] = None - self._handlers: Dict[str, set] = {} + self._handlers: dict[str, set] = {} async def close(self): if self.db: @@ -106,8 +98,8 @@ async def get_messages( """ base_query = "SELECT rowid, * FROM protocol" - where: List[str] = [] - parameters: List[Any] = [] + where: list[str] = [] + parameters: list[Any] = [] if session: where.append("session = ?") @@ -151,7 +143,7 @@ class DatabaseLogHandler(logging.Handler): def __init__(self, db: Database, *args, **kwargs): super().__init__(*args, **kwargs) self.db = db - self._tasks: Set[asyncio.Task] = set() + self._tasks: set[asyncio.Task] = set() def emit(self, record: logging.LogRecord): body = json.loads(record.args[0]) # type: ignore diff --git a/lib/lsp-devtools/lsp_devtools/handlers/__init__.py b/lib/lsp-devtools/lsp_devtools/handlers/__init__.py index 3df7596..0380de3 100644 --- a/lib/lsp-devtools/lsp_devtools/handlers/__init__.py +++ b/lib/lsp-devtools/lsp_devtools/handlers/__init__.py @@ -8,10 +8,9 @@ import attrs if typing.TYPE_CHECKING: + from collections.abc import Mapping from typing import Any from typing import Literal - from typing import Mapping - from typing import Optional MessageSource = Literal["client", "server"] @@ -37,19 +36,19 @@ class LspMessage: source: MessageSource """Indicates if the message was sent by the client or the server.""" - id: Optional[str] + id: str | None """The ``id`` field, if it exists.""" - method: Optional[str] + method: str | None """The ``method`` field, if it exists.""" - params: Optional[Any] = attrs.field(converter=maybe_json) + params: Any | None = attrs.field(converter=maybe_json) """The ``params`` field, if it exists.""" - result: Optional[Any] = attrs.field(converter=maybe_json) + result: Any | None = attrs.field(converter=maybe_json) """The ``result`` field, if it exists.""" - error: Optional[Any] = attrs.field(converter=maybe_json) + error: Any | None = attrs.field(converter=maybe_json) """The ``error`` field, if it exists.""" @classmethod diff --git a/lib/lsp-devtools/lsp_devtools/handlers/sql.py b/lib/lsp-devtools/lsp_devtools/handlers/sql.py index b06b26a..5fdad0f 100644 --- a/lib/lsp-devtools/lsp_devtools/handlers/sql.py +++ b/lib/lsp-devtools/lsp_devtools/handlers/sql.py @@ -1,17 +1,12 @@ import json import pathlib import sqlite3 -import sys from contextlib import closing +from importlib import resources from lsp_devtools.handlers import LspHandler from lsp_devtools.handlers import LspMessage -if sys.version_info < (3, 9): - import importlib_resources as resources -else: - from importlib import resources # type: ignore[no-redef] - class SqlHandler(LspHandler): """A logging handler that sends log records to a SQL database""" diff --git a/lib/lsp-devtools/lsp_devtools/inspector/__init__.py b/lib/lsp-devtools/lsp_devtools/inspector/__init__.py index 15ba47e..414f662 100644 --- a/lib/lsp-devtools/lsp_devtools/inspector/__init__.py +++ b/lib/lsp-devtools/lsp_devtools/inspector/__init__.py @@ -1,12 +1,11 @@ +from __future__ import annotations + import argparse import asyncio import logging import pathlib +import typing from functools import partial -from typing import Any -from typing import Dict -from typing import List -from typing import Optional import platformdirs from rich.highlighter import ReprHighlighter @@ -28,6 +27,10 @@ from lsp_devtools.database import Database from lsp_devtools.handlers import LspMessage +if typing.TYPE_CHECKING: + from typing import Any + + logger = logging.getLogger(__name__) @@ -70,9 +73,9 @@ def __init__(self, db: Database, viewer: MessageViewer, session=None): self.db = db - self.rpcdata: Dict[int, LspMessage] = {} + self.rpcdata: dict[int, LspMessage] = {} self.max_row = 0 - self.session: Optional[str] = session + self.session: str | None = session self.viewer = viewer @@ -113,7 +116,7 @@ def show_object(self, event: DataTable.RowHighlighted): def _get_query_params(self): """Return the set of query parameters to use when populating the table.""" - query: Dict[str, Any] = dict(max_row=self.max_row) + query: dict[str, Any] = dict(max_row=self.max_row) if self.session is not None: query["session"] = self.session @@ -156,7 +159,7 @@ def __init__(self, db: Database, server: AgentServer, *args, **kwargs): self.server = server """Server used to manage connections to lsp servers.""" - self._async_tasks: List[asyncio.Task] = [] + self._async_tasks: list[asyncio.Task] = [] def compose(self) -> ComposeResult: yield Header() @@ -215,7 +218,7 @@ async def handle_message(db: Database, data: bytes): await db.add_message(session, timestamp, source, rpc.body) -def inspector(args, extra: List[str]): +def inspector(args, extra: list[str]): db = Database(args.dbpath) server = AgentServer(handler=partial(handle_message, db)) diff --git a/lib/lsp-devtools/lsp_devtools/record/__init__.py b/lib/lsp-devtools/lsp_devtools/record/__init__.py index 7537e8b..79e13ac 100644 --- a/lib/lsp-devtools/lsp_devtools/record/__init__.py +++ b/lib/lsp-devtools/lsp_devtools/record/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import argparse import asyncio import json @@ -5,8 +7,6 @@ import pathlib from functools import partial from logging import LogRecord -from typing import List -from typing import Optional from rich.console import Console from rich.console import ConsoleRenderable @@ -41,9 +41,9 @@ def render( self, *, record: logging.LogRecord, - traceback: Optional[Traceback], - message_renderable: "ConsoleRenderable", - ) -> "ConsoleRenderable": + traceback: Traceback | None, + message_renderable: ConsoleRenderable, + ) -> ConsoleRenderable: # Delegate most of the rendering to the base RichHandler class. res = super().render( record=record, traceback=traceback, message_renderable=message_renderable @@ -97,7 +97,7 @@ def setup_stdout_output(args, logger: logging.Logger, console: Console): logger.propagate = False -def setup_file_output(args, logger: logging.Logger, console: Optional[Console] = None): +def setup_file_output(args, logger: logging.Logger, console: Console | None = None): """Log messages to a file.""" handler = logging.FileHandler(filename=str(args.to_file)) handler.setLevel(logging.INFO) @@ -122,9 +122,7 @@ def setup_file_output(args, logger: logging.Logger, console: Optional[Console] = logger.propagate = False -def setup_sqlite_output( - args, logger: logging.Logger, console: Optional[Console] = None -): +def setup_sqlite_output(args, logger: logging.Logger, console: Console | None = None): """Log messages to SQLite.""" handler = SqlHandler(args.to_sqlite) handler.setLevel(logging.INFO) @@ -158,7 +156,7 @@ def log_message(logger: logging.Logger, message: bytes): logger.info("%s", rpc.body, extra=rpc.headers) -def start_recording(args, extra: List[str]): +def start_recording(args, extra: list[str]): logger = logging.getLogger("lsp_devtools") rpc_logger = logging.getLogger(__name__) diff --git a/lib/lsp-devtools/lsp_devtools/record/filters.py b/lib/lsp-devtools/lsp_devtools/record/filters.py index 83a0e2f..6cd7e3a 100644 --- a/lib/lsp-devtools/lsp_devtools/record/filters.py +++ b/lib/lsp-devtools/lsp_devtools/record/filters.py @@ -1,17 +1,19 @@ +from __future__ import annotations + import logging -from typing import Dict -from typing import Literal -from typing import Set -from typing import Union +import typing import attrs from .formatters import FormatString -logger = logging.getLogger(__name__) +if typing.TYPE_CHECKING: + from typing import Literal -MessageSource = Literal["client", "server", "both"] -MessageType = Literal["request", "response", "result", "error", "notification"] + MessageSource = Literal["client", "server", "both"] + MessageType = Literal["request", "response", "result", "error", "notification"] + +logger = logging.getLogger(__name__) @attrs.define @@ -21,16 +23,16 @@ class LSPFilter(logging.Filter): message_source: MessageSource = attrs.field(default="both") """Only include messages from the given source.""" - include_message_types: Set[MessageType] = attrs.field(factory=set, converter=set) + include_message_types: set[MessageType] = attrs.field(factory=set, converter=set) """Only include the given message types.""" - exclude_message_types: Set[MessageType] = attrs.field(factory=set, converter=set) + exclude_message_types: set[MessageType] = attrs.field(factory=set, converter=set) """Exclude the given message types.""" - include_methods: Set[str] = attrs.field(factory=set, converter=set) + include_methods: set[str] = attrs.field(factory=set, converter=set) """Only include messages associated with the given method.""" - exclude_methods: Set[str] = attrs.field(factory=set, converter=set) + exclude_methods: set[str] = attrs.field(factory=set, converter=set) """Exclude messages associated with the given method.""" formatter: FormatString = attrs.field( @@ -39,7 +41,7 @@ class LSPFilter(logging.Filter): ) # type: ignore """Format messages according to the given string""" - _response_method_map: Dict[Union[int, str], str] = attrs.field(factory=dict) + _response_method_map: dict[int | str, str] = attrs.field(factory=dict) """Used to determine the method for response messages""" def filter(self, record: logging.LogRecord) -> bool: @@ -95,7 +97,7 @@ def _get_message_method(self, message_type: str, message: dict) -> str: return self._response_method_map[message["id"]] -def message_matches_type(message_type: str, types: Set[MessageType]) -> bool: +def message_matches_type(message_type: str, types: set[MessageType]) -> bool: """Determine if the type of message is included in the given set of types""" if message_type == "result": diff --git a/lib/lsp-devtools/lsp_devtools/record/formatters.py b/lib/lsp-devtools/lsp_devtools/record/formatters.py index 1a628c4..ce42e8a 100644 --- a/lib/lsp-devtools/lsp_devtools/record/formatters.py +++ b/lib/lsp-devtools/lsp_devtools/record/formatters.py @@ -1,25 +1,19 @@ +from __future__ import annotations + import json import re +import typing +from functools import cache from functools import partial -from typing import Any -from typing import Callable -from typing import Dict -from typing import List -from typing import Optional -from typing import Tuple -from typing import Union import lsprotocol.types -try: - from functools import cache -except ImportError: - from functools import lru_cache - - cache = lru_cache(None) +if typing.TYPE_CHECKING: + from typing import Any + from typing import Callable -def format_json(obj: dict, *, indent: Union[str, int, None] = 2) -> str: +def format_json(obj: dict, *, indent: str | int | None = 2) -> str: if isinstance(obj, str): return obj @@ -34,7 +28,7 @@ def format_range(range_: dict) -> str: return f"{format_position(range_['start'])}-{format_position(range_['end'])}" -FORMATTERS: Dict[str, Callable[[Any], str]] = { +FORMATTERS: dict[str, Callable[[Any], str]] = { "position": format_position, "range": format_range, "json": format_json, @@ -73,7 +67,7 @@ def __init__(self, accessor: str, formatter: Callable[[Any], str]): def __repr__(self): return f'Value(accessor="{self.accessor}", formatter={self.formatter})' - def format(self, message: dict, accessor: Optional[str] = None) -> str: + def format(self, message: dict, accessor: str | None = None) -> str: """Convert a message to a string according to the current accessor and formatter.""" @@ -110,7 +104,7 @@ def format(self, message: dict, accessor: Optional[str] = None) -> str: @cache -def get_separator_index(separator: str) -> Tuple[str, Union[int, slice, None]]: +def get_separator_index(separator: str) -> tuple[str, int | slice | None]: if not separator: return "\n", None @@ -134,7 +128,7 @@ def get_separator(sep: str) -> str: return sep.replace("\\n", "\n").replace("\\t", "\t") -def get_index(idx: str) -> Union[int, slice, None]: +def get_index(idx: str) -> int | slice | None: try: return int(idx) except ValueError: @@ -165,7 +159,7 @@ def __init__(self, pattern: str): def _parse(self): idx = 0 - parts: List[Union[str, Value]] = [] + parts: list[str | Value] = [] for match in self.VARIABLE.finditer(self.pattern): start, end = match.span() diff --git a/lib/lsp-devtools/lsp_devtools/record/visualize.py b/lib/lsp-devtools/lsp_devtools/record/visualize.py index 4fc4daa..cfba069 100644 --- a/lib/lsp-devtools/lsp_devtools/record/visualize.py +++ b/lib/lsp-devtools/lsp_devtools/record/visualize.py @@ -9,9 +9,6 @@ from rich.style import Style if typing.TYPE_CHECKING: - from typing import List - from typing import Optional - from rich.console import Console from rich.console import ConsoleOptions from rich.console import RenderResult @@ -59,19 +56,19 @@ class PacketPipeColumn(progress.ProgressColumn): """Visualizes messages sent between client and server as "packets".""" def __init__( - self, duration: float = 1.0, table_column: Optional[Column] = None + self, duration: float = 1.0, table_column: Column | None = None ) -> None: self.client_count = 0 self.server_count = 0 - self.server_times: List[float] = [] - self.client_times: List[float] = [] + self.server_times: list[float] = [] + self.client_times: list[float] = [] # How long it should take for a packet to propogate. self.duration = duration super().__init__(table_column) - def _update_packets(self, task: progress.Task, source: str) -> List[float]: + def _update_packets(self, task: progress.Task, source: str) -> list[float]: """Update the packet positions for the given message source. Parameters diff --git a/lib/lsp-devtools/pyproject.toml b/lib/lsp-devtools/pyproject.toml index 531c50f..ed0c787 100644 --- a/lib/lsp-devtools/pyproject.toml +++ b/lib/lsp-devtools/pyproject.toml @@ -46,9 +46,8 @@ skip_covered = true sort = "Cover" [tool.pyright] -venv = ".env" include = ["lsp_devtools"] -pythonVersion = "3.8" +pythonVersion = "3.9" [tool.towncrier] filename = "CHANGES.md" diff --git a/lib/lsp-devtools/tests/record/test_filters.py b/lib/lsp-devtools/tests/record/test_filters.py index 6978f6b..7ceb8bc 100644 --- a/lib/lsp-devtools/tests/record/test_filters.py +++ b/lib/lsp-devtools/tests/record/test_filters.py @@ -1,7 +1,5 @@ import itertools import logging -from typing import List -from typing import Tuple import pytest @@ -91,7 +89,7 @@ def test_filter_message_source(filter_source: str, message_source: str, expected ), ], ) -def test_filter_included_message_types(message: dict, setup: Tuple[List[str], bool]): +def test_filter_included_message_types(message: dict, setup: tuple[list[str], bool]): """Ensure that we can filter messages by listing the types we DO want to see.""" message_types, expected = setup @@ -164,7 +162,7 @@ def test_filter_included_message_types(message: dict, setup: Tuple[List[str], bo ), ], ) -def test_filter_excluded_message_types(message: dict, setup: Tuple[List[str], bool]): +def test_filter_excluded_message_types(message: dict, setup: tuple[list[str], bool]): """Ensure that we can filter messages by listing the types we DO NOT want to see.""" message_types, expected = setup @@ -202,7 +200,7 @@ def test_filter_excluded_message_types(message: dict, setup: Tuple[List[str], bo ), ], ) -def test_filter_included_method(message: dict, setup: Tuple[List[str], bool]): +def test_filter_included_method(message: dict, setup: tuple[list[str], bool]): """Ensure that we can filter messages by listing the methods we wish to see.""" methods, expected = setup @@ -247,7 +245,7 @@ def test_filter_included_method(message: dict, setup: Tuple[List[str], bool]): ], ) def test_filter_included_method_response_message( - response: dict, setup: Tuple[List[str], str, bool] + response: dict, setup: tuple[list[str], str, bool] ): """Ensure that we can filter response message by listing the methods we wish to see.""" @@ -292,7 +290,7 @@ def test_filter_included_method_response_message( ), ], ) -def test_filter_excluded_method(message: dict, setup: Tuple[List[str], bool]): +def test_filter_excluded_method(message: dict, setup: tuple[list[str], bool]): """Ensure that we can filter messages by listing the methods we don't wish to see.""" @@ -338,7 +336,7 @@ def test_filter_excluded_method(message: dict, setup: Tuple[List[str], bool]): ], ) def test_filter_excluded_method_response_message( - response: dict, setup: Tuple[List[str], str, bool] + response: dict, setup: tuple[list[str], str, bool] ): """Ensure that we can filter response message by listing the methods we dont' wish to see.""" diff --git a/lib/lsp-devtools/tests/record/test_record.py b/lib/lsp-devtools/tests/record/test_record.py index 36078df..ecca10f 100644 --- a/lib/lsp-devtools/tests/record/test_record.py +++ b/lib/lsp-devtools/tests/record/test_record.py @@ -12,8 +12,6 @@ if typing.TYPE_CHECKING: import pathlib from typing import Any - from typing import Dict - from typing import List @pytest.fixture(scope="module") @@ -89,8 +87,8 @@ def test_file_output( tmp_path: pathlib.Path, record: argparse.ArgumentParser, logger: logging.Logger, - args: List[str], - messages: List[Dict[str, Any]], + args: list[str], + messages: list[dict[str, Any]], expected: str, ): """Ensure that we can log to files correctly. From 012f8c3ed9a319709ede8fc395e10abeb7c891f8 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Mon, 18 Nov 2024 20:00:49 +0000 Subject: [PATCH 12/20] lsp-devtools: upgrade to pygls 2.0a2 --- lib/lsp-devtools/changes/xxx.misc.rst | 1 + lib/lsp-devtools/hatch.toml | 3 + lib/lsp-devtools/lsp_devtools/agent/agent.py | 10 +-- lib/lsp-devtools/lsp_devtools/agent/client.py | 89 +++---------------- lib/lsp-devtools/lsp_devtools/agent/server.py | 25 ++++-- .../lsp_devtools/client/editor/text_editor.py | 2 +- lib/lsp-devtools/pyproject.toml | 2 +- lib/lsp-devtools/tests/servers/simple.py | 9 +- 8 files changed, 45 insertions(+), 96 deletions(-) create mode 100644 lib/lsp-devtools/changes/xxx.misc.rst diff --git a/lib/lsp-devtools/changes/xxx.misc.rst b/lib/lsp-devtools/changes/xxx.misc.rst new file mode 100644 index 0000000..36caf8c --- /dev/null +++ b/lib/lsp-devtools/changes/xxx.misc.rst @@ -0,0 +1 @@ +Migrate to pygls `v2.0a2` diff --git a/lib/lsp-devtools/hatch.toml b/lib/lsp-devtools/hatch.toml index 29ea2b4..bd5ec2b 100644 --- a/lib/lsp-devtools/hatch.toml +++ b/lib/lsp-devtools/hatch.toml @@ -11,6 +11,9 @@ packages = ["lsp_devtools"] [envs.hatch-test] extra-dependencies = ["pytest-asyncio"] +[envs.hatch-test.env-vars] +UV_PRERELEASE="allow" + [envs.hatch-static-analysis] config-path = "ruff_defaults.toml" dependencies = ["ruff==0.5.2"] diff --git a/lib/lsp-devtools/lsp_devtools/agent/agent.py b/lib/lsp-devtools/lsp_devtools/agent/agent.py index 4d0e26f..15a5c2d 100644 --- a/lib/lsp-devtools/lsp_devtools/agent/agent.py +++ b/lib/lsp-devtools/lsp_devtools/agent/agent.py @@ -21,6 +21,8 @@ from typing import Callable from typing import Union + from pygls.io_ import AsyncReader + MessageHandler = Callable[[bytes], Union[None, Coroutine[Any, Any, None]]] UTC = timezone.utc @@ -74,7 +76,7 @@ def parse_rpc_message(data: bytes) -> RPCMessage: return RPCMessage(headers, body) -async def aio_readline(reader: asyncio.StreamReader, message_handler: MessageHandler): +async def aio_readline(reader: AsyncReader, message_handler: MessageHandler): CONTENT_LENGTH_PATTERN = re.compile(rb"^Content-Length: (\d+)\r\n$") # Initialize message buffer @@ -222,12 +224,10 @@ async def stop(self): except TimeoutError: self.server.kill() - args = {} - args["msg"] = "lsp-devtools agent is stopping." - # Cancel the tasks connecting client to server for task in self._tasks: - task.cancel(**args) + logger.debug("cancelling: %s", task) + task.cancel(msg="lsp-devtools agent is stopping.") if self.writer: self.writer.close() diff --git a/lib/lsp-devtools/lsp_devtools/agent/client.py b/lib/lsp-devtools/lsp_devtools/agent/client.py index 87e9d4c..571819f 100644 --- a/lib/lsp-devtools/lsp_devtools/agent/client.py +++ b/lib/lsp-devtools/lsp_devtools/agent/client.py @@ -1,11 +1,11 @@ from __future__ import annotations import asyncio +import inspect import typing import stamina from pygls.client import JsonRPCClient -from pygls.client import aio_readline from pygls.protocol import default_converter from lsp_devtools.agent.protocol import AgentProtocol @@ -13,25 +13,6 @@ if typing.TYPE_CHECKING: from typing import Any -# from websockets.client import WebSocketClientProtocol - - -# class WebSocketClientTransportAdapter: -# """Protocol adapter for the WebSocket client interface.""" - -# def __init__(self, ws: WebSocketClientProtocol, loop: asyncio.AbstractEventLoop): -# self._ws = ws -# self._loop = loop - -# def close(self) -> None: -# """Stop the WebSocket server.""" -# print("-- CLOSING --") -# self._loop.create_task(self._ws.close()) - -# def write(self, data: Any) -> None: -# """Create a task to write specified data into a WebSocket.""" -# asyncio.ensure_future(self._ws.send(data)) - class AgentClient(JsonRPCClient): """Client for connecting to an AgentServer instance.""" @@ -53,7 +34,6 @@ def _report_server_error(self, error, source): def feature(self, feature_name: str, options: Any | None = None): return self.protocol.fm.feature(feature_name, options) - # TODO: Upstream this... or at least something equivalent. async def start_tcp(self, host: str, port: int): # The user might not have started the server app immediately and since the # agent will live as long as the wrapper language server we may as well @@ -67,71 +47,22 @@ async def start_tcp(self, host: str, port: int): ) async for attempt in retries: with attempt: - reader, writer = await asyncio.open_connection(host, port) - - self.protocol.connection_made(writer) # type: ignore[arg-type] - connection = asyncio.create_task( - aio_readline(self._stop_event, reader, self.protocol.data_received) - ) - self.connected = True - self._async_tasks.append(connection) + await super().start_tcp(host, port) + self.connected = True def forward_message(self, message: bytes): """Forward the given message to the server instance.""" - if not self.connected: + if not self.connected or self.protocol.writer is None: self._buffer.append(message) return - if self.protocol.transport is None: - return - # Send any buffered messages while len(self._buffer) > 0: - self.protocol.transport.write(self._buffer.pop(0)) - - self.protocol.transport.write(message) - - # TODO: Upstream this... or at least something equivalent. - # def start_ws(self, host: str, port: int): - # self.protocol._send_only_body = True # Don't send headers within the payload - - # async def client_connection(host: str, port: int): - # """Create and run a client connection.""" - - # self._client = await websockets.connect( # type: ignore - # f"ws://{host}:{port}" - # ) - # loop = asyncio.get_running_loop() - # self.protocol.transport = WebSocketClientTransportAdapter( - # self._client, loop - # ) - # message = None - - # try: - # while not self._stop_event.is_set(): - # try: - # message = await asyncio.wait_for( - # self._client.recv(), timeout=0.5 - # ) - # self.protocol._procedure_handler( - # json.loads( - # message, - # object_hook=self.protocol._deserialize_message - # ) - # ) - # except JSONDecodeError: - # print(message or "-- message not found --") - # raise - # except TimeoutError: - # pass - # except Exception: - # raise - - # finally: - # await self._client.close() + res = self.protocol.writer.write(self._buffer.pop(0)) + if inspect.isawaitable(res): + asyncio.ensure_future(res) - # try: - # asyncio.run(client_connection(host, port)) - # except KeyboardInterrupt: - # pass + res = self.protocol.writer.write(message) + if inspect.isawaitable(res): + asyncio.ensure_future(res) diff --git a/lib/lsp-devtools/lsp_devtools/agent/server.py b/lib/lsp-devtools/lsp_devtools/agent/server.py index f74a47a..df4893a 100644 --- a/lib/lsp-devtools/lsp_devtools/agent/server.py +++ b/lib/lsp-devtools/lsp_devtools/agent/server.py @@ -1,12 +1,13 @@ from __future__ import annotations import asyncio +import json import logging import traceback import typing from pygls.protocol import default_converter -from pygls.server import Server +from pygls.server import JsonRPCServer from lsp_devtools.agent.agent import aio_readline from lsp_devtools.agent.protocol import AgentProtocol @@ -18,7 +19,7 @@ from lsp_devtools.agent.agent import MessageHandler -class AgentServer(Server): +class AgentServer(JsonRPCServer): """A pygls server that accepts connections from agents allowing them to send their collected messages.""" @@ -40,25 +41,33 @@ def __init__( super().__init__(*args, **kwargs) self.logger = logger or logging.getLogger(__name__) - self.handler = handler or self.lsp.data_received + self.handler = handler or self._default_handler self.db: Database | None = None self._client_buffer: list[str] = [] self._server_buffer: list[str] = [] self._tcp_server: asyncio.Task | None = None - def _report_server_error(self, exc: Exception, source): + def _default_handler(self, data: bytes): + message = self.protocol.structure_message(json.loads(data)) + self.protocol.handle_message(message) + + def _report_server_error(self, error: Exception, source): """Report internal server errors.""" - tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) - self.logger.error("%s: %s", type(exc).__name__, exc) + tb = "".join( + traceback.format_exception(type(error), error, error.__traceback__) + ) + self.logger.error("%s: %s", type(error).__name__, error) self.logger.debug("%s", tb) def feature(self, feature_name: str, options: Any | None = None): return self.lsp.fm.feature(feature_name, options) async def start_tcp(self, host: str, port: int) -> None: # type: ignore[override] - async def handle_client(reader, writer): - self.lsp.connection_made(writer) + async def handle_client( + reader: asyncio.StreamReader, writer: asyncio.StreamWriter + ): + self.protocol.set_writer(writer) try: await aio_readline(reader, self.handler) diff --git a/lib/lsp-devtools/lsp_devtools/client/editor/text_editor.py b/lib/lsp-devtools/lsp_devtools/client/editor/text_editor.py index ee051c0..38c32ff 100644 --- a/lib/lsp-devtools/lsp_devtools/client/editor/text_editor.py +++ b/lib/lsp-devtools/lsp_devtools/client/editor/text_editor.py @@ -103,7 +103,7 @@ def edit(self, edit): version=self.version, uri=self.uri ), content_changes=[ - types.TextDocumentContentChangeEvent_Type1( + types.TextDocumentContentChangePartial( text=edit.text, range=types.Range( start=types.Position(line=start_line, character=start_col), diff --git a/lib/lsp-devtools/pyproject.toml b/lib/lsp-devtools/pyproject.toml index ed0c787..2452915 100644 --- a/lib/lsp-devtools/pyproject.toml +++ b/lib/lsp-devtools/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ dependencies = [ "aiosqlite", "platformdirs", - "pygls>=1.1.0,<2", + "pygls>=2.0a2", "stamina", "textual>=0.41.0", ] diff --git a/lib/lsp-devtools/tests/servers/simple.py b/lib/lsp-devtools/tests/servers/simple.py index 6bda60d..582d9cc 100644 --- a/lib/lsp-devtools/tests/servers/simple.py +++ b/lib/lsp-devtools/tests/servers/simple.py @@ -1,14 +1,19 @@ """A very simple language server.""" from lsprotocol import types -from pygls.server import LanguageServer +from pygls.lsp.server import LanguageServer server = LanguageServer("simple-server", "v1") @server.feature(types.INITIALIZED) def _(ls: LanguageServer, params: types.InitializedParams): - ls.show_message("Hello, world!") + ls.window_show_message( + types.ShowMessageParams( + message="Hello, world!", + type=types.MessageType.Log, + ) + ) if __name__ == "__main__": From 0efa2e772f175324f5eafa17b375966c20c9a37e Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sat, 23 Nov 2024 16:04:37 +0000 Subject: [PATCH 13/20] Upgrade ruff to 0.8.0 --- .pre-commit-config.yaml | 2 +- lib/lsp-devtools/hatch.toml | 2 +- lib/lsp-devtools/ruff.toml | 6 +++--- lib/lsp-devtools/ruff_defaults.toml | 17 +++++++---------- lib/pytest-lsp/hatch.toml | 2 +- lib/pytest-lsp/ruff.toml | 6 +++--- lib/pytest-lsp/ruff_defaults.toml | 15 +++++++-------- 7 files changed, 23 insertions(+), 27 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fc82045..5a7f349 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.9 + rev: v0.8.0 hooks: - id: ruff args: [--fix] diff --git a/lib/lsp-devtools/hatch.toml b/lib/lsp-devtools/hatch.toml index bd5ec2b..7f81b10 100644 --- a/lib/lsp-devtools/hatch.toml +++ b/lib/lsp-devtools/hatch.toml @@ -16,4 +16,4 @@ UV_PRERELEASE="allow" [envs.hatch-static-analysis] config-path = "ruff_defaults.toml" -dependencies = ["ruff==0.5.2"] +dependencies = ["ruff==0.8.0"] diff --git a/lib/lsp-devtools/ruff.toml b/lib/lsp-devtools/ruff.toml index 42a3240..c47c4f8 100644 --- a/lib/lsp-devtools/ruff.toml +++ b/lib/lsp-devtools/ruff.toml @@ -56,9 +56,9 @@ ignore = [ "SIM115", # Use key in dict "SIM118", # Use key in dict "SLF001", # private member access - "TCH001", # move import to type checking block - "TCH002", # move import to type checking block - "TCH003", # move import to type checking block + "TC001", # move import to type checking block + "TC002", # move import to type checking block + "TC003", # move import to type checking block "TID252", # Absolute vs relative imports "TRY300", # Move statement to else block ] diff --git a/lib/lsp-devtools/ruff_defaults.toml b/lib/lsp-devtools/ruff_defaults.toml index 9b62f5a..21a72a6 100644 --- a/lib/lsp-devtools/ruff_defaults.toml +++ b/lib/lsp-devtools/ruff_defaults.toml @@ -100,7 +100,6 @@ select = [ "E742", "E743", "E902", - "E999", "EM101", "EM102", "EM103", @@ -244,7 +243,6 @@ select = [ "PLR0133", "PLR0206", "PLR0402", - "PLR1701", "PLR1711", "PLR1714", "PLR1722", @@ -452,12 +450,12 @@ select = [ "T100", "T201", "T203", - "TCH001", - "TCH002", - "TCH003", - "TCH004", - "TCH005", - "TCH010", + "TC001", + "TC002", + "TC003", + "TC004", + "TC005", + "TC010", "TD004", "TD005", "TD006", @@ -469,9 +467,9 @@ select = [ "TRY003", "TRY004", "TRY201", + "TRY203", "TRY300", "TRY301", - "TRY302", "TRY400", "TRY401", "UP001", @@ -498,7 +496,6 @@ select = [ "UP024", "UP025", "UP026", - "UP027", "UP028", "UP029", "UP030", diff --git a/lib/pytest-lsp/hatch.toml b/lib/pytest-lsp/hatch.toml index ff88486..e235c75 100644 --- a/lib/pytest-lsp/hatch.toml +++ b/lib/pytest-lsp/hatch.toml @@ -25,4 +25,4 @@ matrix.pytest.dependencies = [ [envs.hatch-static-analysis] config-path = "ruff_defaults.toml" -dependencies = ["ruff==0.5.2"] +dependencies = ["ruff==0.8.0"] diff --git a/lib/pytest-lsp/ruff.toml b/lib/pytest-lsp/ruff.toml index 42a3240..c47c4f8 100644 --- a/lib/pytest-lsp/ruff.toml +++ b/lib/pytest-lsp/ruff.toml @@ -56,9 +56,9 @@ ignore = [ "SIM115", # Use key in dict "SIM118", # Use key in dict "SLF001", # private member access - "TCH001", # move import to type checking block - "TCH002", # move import to type checking block - "TCH003", # move import to type checking block + "TC001", # move import to type checking block + "TC002", # move import to type checking block + "TC003", # move import to type checking block "TID252", # Absolute vs relative imports "TRY300", # Move statement to else block ] diff --git a/lib/pytest-lsp/ruff_defaults.toml b/lib/pytest-lsp/ruff_defaults.toml index 301a10e..21a72a6 100644 --- a/lib/pytest-lsp/ruff_defaults.toml +++ b/lib/pytest-lsp/ruff_defaults.toml @@ -450,12 +450,12 @@ select = [ "T100", "T201", "T203", - "TCH001", - "TCH002", - "TCH003", - "TCH004", - "TCH005", - "TCH010", + "TC001", + "TC002", + "TC003", + "TC004", + "TC005", + "TC010", "TD004", "TD005", "TD006", @@ -467,9 +467,9 @@ select = [ "TRY003", "TRY004", "TRY201", + "TRY203", "TRY300", "TRY301", - "TRY302", "TRY400", "TRY401", "UP001", @@ -496,7 +496,6 @@ select = [ "UP024", "UP025", "UP026", - "UP027", "UP028", "UP029", "UP030", From 5694bcc99a705054c76671df6415e1745faf1538 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sat, 23 Nov 2024 16:07:41 +0000 Subject: [PATCH 14/20] lsp-devtools: Apply ruff fixes --- lib/lsp-devtools/lsp_devtools/agent/client.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/lsp-devtools/lsp_devtools/agent/client.py b/lib/lsp-devtools/lsp_devtools/agent/client.py index 571819f..43d062d 100644 --- a/lib/lsp-devtools/lsp_devtools/agent/client.py +++ b/lib/lsp-devtools/lsp_devtools/agent/client.py @@ -25,6 +25,7 @@ def __init__(self): ) self.connected = False self._buffer: list[bytes] = [] + self._tasks: set[asyncio.Task[Any]] = set() def _report_server_error(self, error, source): # Bail on error @@ -61,8 +62,12 @@ def forward_message(self, message: bytes): while len(self._buffer) > 0: res = self.protocol.writer.write(self._buffer.pop(0)) if inspect.isawaitable(res): - asyncio.ensure_future(res) + task = asyncio.ensure_future(res) + task.add_done_callback(self._tasks.discard) + self._tasks.add(task) res = self.protocol.writer.write(message) if inspect.isawaitable(res): - asyncio.ensure_future(res) + task = asyncio.ensure_future(res) + task.add_done_callback(self._tasks.discard) + self._tasks.add(task) From fbb296f734f0696ff8a7485f51a55a2541860c80 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sat, 23 Nov 2024 16:08:17 +0000 Subject: [PATCH 15/20] Update mypy to 1.13.0 --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5a7f349..976cf21 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: files: 'lib/.*\.py' - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v1.11.2' + rev: 'v1.13.0' hooks: - id: mypy name: mypy (pytest-lsp) @@ -28,7 +28,7 @@ repos: additional_dependencies: - importlib-resources - platformdirs - - pygls + - 'pygls>=2.0a2' - pytest - pytest-asyncio - websockets @@ -42,7 +42,7 @@ repos: - attrs - importlib-resources - platformdirs - - pygls + - 'pygls>=2.0a2' - stamina - textual - websockets From 5a6398d17808d677e13201e59b2f7e9a2a1b4f16 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sat, 23 Nov 2024 16:14:55 +0000 Subject: [PATCH 16/20] lsp-devtools: Remove screenshot action and binding --- lib/lsp-devtools/lsp_devtools/inspector/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/lsp-devtools/lsp_devtools/inspector/__init__.py b/lib/lsp-devtools/lsp_devtools/inspector/__init__.py index 414f662..9af740b 100644 --- a/lib/lsp-devtools/lsp_devtools/inspector/__init__.py +++ b/lib/lsp-devtools/lsp_devtools/inspector/__init__.py @@ -147,7 +147,6 @@ class LSPInspector(App): BINDINGS = [ ("ctrl+b", "toggle_sidebar", "Sidebar"), ("ctrl+c", "quit", "Quit"), - ("ctrl+s", "screenshot", "Take Screenshot"), ] def __init__(self, db: Database, server: AgentServer, *args, **kwargs): @@ -169,10 +168,6 @@ def compose(self) -> ComposeResult: yield Container(ScrollableContainer(messages), Sidebar(viewer)) yield Footer() - def action_screenshot(self): - self.bell() - self.save_screenshot(None, "./") - def action_toggle_sidebar(self) -> None: sidebar = self.query_one(Sidebar) self.set_focus(None) From ccd535a7c963430582da4a4d6f20fc7aa8eed233 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sat, 23 Nov 2024 16:23:19 +0000 Subject: [PATCH 17/20] pytest-lsp: Ruff fixes --- lib/pytest-lsp/tests/test_client.py | 13 ++++++++----- lib/pytest-lsp/tests/test_plugin.py | 11 +++++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/pytest-lsp/tests/test_client.py b/lib/pytest-lsp/tests/test_client.py index 300d53c..9fbb582 100644 --- a/lib/pytest-lsp/tests/test_client.py +++ b/lib/pytest-lsp/tests/test_client.py @@ -1,10 +1,10 @@ +from __future__ import annotations + import itertools import json import pathlib import sys -from typing import Any -from typing import Dict -from typing import Optional +import typing import pygls.uris as uri import pytest @@ -12,6 +12,9 @@ import pytest_lsp from pytest_lsp import LanguageClient +if typing.TYPE_CHECKING: + from typing import Any + @pytest.mark.parametrize( "client_spec,capabilities", @@ -156,7 +159,7 @@ async def test_capabilities(client): ], ) def test_get_configuration( - config: Dict[str, Any], section: Optional[str], scope: Optional[str], expected: Any + config: dict[str, Any], section: str | None, scope: str | None, expected: Any ): """Ensure that we can get a client's configuration correctly. @@ -199,7 +202,7 @@ def test_get_configuration( ], ) def test_set_configuration( - item: Any, section: Optional[str], scope: Optional[str], expected: Dict[str, Any] + item: Any, section: str | None, scope: str | None, expected: dict[str, Any] ): """Ensure that we can set the client's configuration correctly. diff --git a/lib/pytest-lsp/tests/test_plugin.py b/lib/pytest-lsp/tests/test_plugin.py index 06439d4..d8d4d9d 100644 --- a/lib/pytest-lsp/tests/test_plugin.py +++ b/lib/pytest-lsp/tests/test_plugin.py @@ -1,14 +1,17 @@ +from __future__ import annotations + import pathlib import sys -from typing import Any -from typing import Dict -from typing import List +import typing import pygls.uris as uri import pytest from pytest_lsp.plugin import ClientServerConfig +if typing.TYPE_CHECKING: + from typing import Any + @pytest.mark.parametrize( "config, kwargs, expected", @@ -59,7 +62,7 @@ ], ) def test_get_server_command( - config: ClientServerConfig, kwargs: Dict[str, Any], expected: List[str] + config: ClientServerConfig, kwargs: dict[str, Any], expected: list[str] ): """Ensure that we can build the server start command correctly.""" actual = config.get_server_command(**kwargs) From 1b9d33ea330f98340c06cb58e9f33527fbd473b5 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sat, 23 Nov 2024 16:41:13 +0000 Subject: [PATCH 18/20] pytest-lsp: mypy fixes --- lib/pytest-lsp/pytest_lsp/client.py | 5 +++-- lib/pytest-lsp/pytest_lsp/protocol.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/pytest-lsp/pytest_lsp/client.py b/lib/pytest-lsp/pytest_lsp/client.py index 0df91e5..caeeca2 100644 --- a/lib/pytest-lsp/pytest_lsp/client.py +++ b/lib/pytest-lsp/pytest_lsp/client.py @@ -23,6 +23,7 @@ from .protocol import LanguageClientProtocol if typing.TYPE_CHECKING: + from collections.abc import Sequence from typing import Any @@ -53,7 +54,7 @@ def __init__(self, *args, configuration: dict[str, Any] | None = None, **kwargs) self.log_messages: list[types.LogMessageParams] = [] """Holds any received ``window/logMessage`` requests.""" - self.diagnostics: dict[str, list[types.Diagnostic]] = {} + self.diagnostics: dict[str, Sequence[types.Diagnostic]] = {} """Holds any recieved diagnostics.""" self.progress_reports: dict[ @@ -108,7 +109,7 @@ async def server_exit(self, server: asyncio.subprocess.Process): logger.debug("Cancelled pending request '%s': %s", id_, reason) def report_server_error( - self, error: Exception, source: PyglsError | JsonRpcException + self, error: Exception, source: type[PyglsError | JsonRpcException] ): """Called when the server does something unexpected, e.g. sending malformed JSON.""" diff --git a/lib/pytest-lsp/pytest_lsp/protocol.py b/lib/pytest-lsp/pytest_lsp/protocol.py index 23efe5f..8578747 100644 --- a/lib/pytest-lsp/pytest_lsp/protocol.py +++ b/lib/pytest-lsp/pytest_lsp/protocol.py @@ -54,7 +54,7 @@ def _handle_notification(self, method_name, params): super()._handle_notification(method_name, params) - async def send_request_async(self, method, params=None): + async def send_request_async(self, method, params=None, msg_id=None): """Wrap pygls' ``send_request_async`` implementation. This will - Check the params to see if they're compatible with the client's stated @@ -78,7 +78,7 @@ async def send_request_async(self, method, params=None): check_params_against_client_capabilities( self._server.capabilities, method, params ) - result = await super().send_request_async(method, params) + result = await super().send_request_async(method, params, msg_id) check_result_against_client_capabilities( self._server.capabilities, method, From c24938da818e5a1ccd7f76300f893e5a5dbcfbea Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sat, 23 Nov 2024 18:41:53 +0000 Subject: [PATCH 19/20] lsp-devtools: Update changelog --- lib/lsp-devtools/changes/{xxx.misc.rst => 192.misc.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/lsp-devtools/changes/{xxx.misc.rst => 192.misc.md} (100%) diff --git a/lib/lsp-devtools/changes/xxx.misc.rst b/lib/lsp-devtools/changes/192.misc.md similarity index 100% rename from lib/lsp-devtools/changes/xxx.misc.rst rename to lib/lsp-devtools/changes/192.misc.md From df1ffe34e90710eb1769900f54c5f6661a4db9d4 Mon Sep 17 00:00:00 2001 From: Alex Carney Date: Sat, 23 Nov 2024 18:43:22 +0000 Subject: [PATCH 20/20] lsp-devtools: Suppress `asyncio.CancelledError` --- lib/lsp-devtools/changes/191.fix.md | 1 + lib/lsp-devtools/lsp_devtools/agent/__init__.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 lib/lsp-devtools/changes/191.fix.md diff --git a/lib/lsp-devtools/changes/191.fix.md b/lib/lsp-devtools/changes/191.fix.md new file mode 100644 index 0000000..af307dc --- /dev/null +++ b/lib/lsp-devtools/changes/191.fix.md @@ -0,0 +1 @@ +The `lsp-devtools agent` should now suppress `asyncio.CancelledError` exceptions allowing the agent to process to terminate gracefully diff --git a/lib/lsp-devtools/lsp_devtools/agent/__init__.py b/lib/lsp-devtools/lsp_devtools/agent/__init__.py index 82c0cd7..ef82460 100644 --- a/lib/lsp-devtools/lsp_devtools/agent/__init__.py +++ b/lib/lsp-devtools/lsp_devtools/agent/__init__.py @@ -56,7 +56,10 @@ async def main(args, extra: list[str]): def run_agent(args, extra: list[str]): - asyncio.run(main(args, extra)) + try: + asyncio.run(main(args, extra)) + except asyncio.CancelledError: + pass def cli(commands: argparse._SubParsersAction):