Skip to content

Commit

Permalink
pytest-lsp: Add support for window/workDoneProgress/create
Browse files Browse the repository at this point in the history
  • Loading branch information
alcarney committed Nov 12, 2023
1 parent 021b9a5 commit e82a8e6
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 13 deletions.
21 changes: 21 additions & 0 deletions docs/pytest-lsp/guide/language-client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,27 @@ Similar to ``window/logMessage`` above, the client records any :lsp:`window/show
:start-at: @server.feature
:end-at: return items

``window/workDoneProgress/create``
----------------------------------

The client can respond to :lsp:`window/workDoneProgress/create` requests and handle associated :lsp:`$/progress`
notifications

.. card:: test_server.py

.. literalinclude:: ../../../lib/pytest-lsp/tests/examples/window-create-progress/t_server.py
:language: python
:start-at: @pytest.mark.asyncio
:end-before: @pytest.mark.asyncio

.. card:: server.py

.. literalinclude:: ../../../lib/pytest-lsp/tests/examples/window-create-progress/server.py
:language: python
:start-at: @server.command
:end-at: return


``workspace/configuration``
---------------------------

Expand Down
1 change: 1 addition & 0 deletions lib/pytest-lsp/changes/91.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pytest-lsp's ``LanguageClient`` is now able to handle ``window/workDoneProgress/create`` requests.
49 changes: 46 additions & 3 deletions lib/pytest-lsp/pytest_lsp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
import sys
import traceback
import typing
import warnings
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Type
from typing import Union

from lsprotocol import types
Expand All @@ -20,6 +22,7 @@
from pygls.lsp.client import BaseLanguageClient
from pygls.protocol import default_converter

from .checks import LspSpecificationWarning
from .protocol import LanguageClientProtocol

if sys.version_info.minor < 9:
Expand Down Expand Up @@ -47,8 +50,7 @@ def __init__(self, *args, configuration: Optional[Dict[str, Any]] = None, **kwar
"""The client's capabilities."""

self.shown_documents: List[types.ShowDocumentParams] = []
"""Used to keep track of the documents requested to be shown via a
``window/showDocument`` request."""
"""Holds any received show document requests."""

self.messages: List[types.ShowMessageParams] = []
"""Holds any received ``window/showMessage`` requests."""
Expand All @@ -57,7 +59,12 @@ def __init__(self, *args, configuration: Optional[Dict[str, Any]] = None, **kwar
"""Holds any received ``window/logMessage`` requests."""

self.diagnostics: Dict[str, List[types.Diagnostic]] = {}
"""Used to hold any recieved diagnostics."""
"""Holds any recieved diagnostics."""

self.progress_reports: Dict[
types.ProgressToken, List[types.ProgressParams]
] = {}
"""Holds any received progress updates."""

self.error: Optional[Exception] = None
"""Indicates if the client encountered an error."""
Expand Down Expand Up @@ -283,6 +290,42 @@ def publish_diagnostics(
):
client.diagnostics[params.uri] = params.diagnostics

@client.feature(types.WINDOW_WORK_DONE_PROGRESS_CREATE)
def create_work_done_progress(
client: LanguageClient, params: types.WorkDoneProgressCreateParams
):
if params.token in client.progress_reports:
# TODO: Send an error reponse to the client - might require changes
# to pygls...
warnings.warn(
f"Duplicate progress token: {params.token!r}", LspSpecificationWarning
)

client.progress_reports.setdefault(params.token, [])
return None

@client.feature(types.PROGRESS)
def progress(client: LanguageClient, params: types.ProgressParams):
if params.token not in client.progress_reports:
warnings.warn(
f"Unknown progress token: {params.token!r}", LspSpecificationWarning
)

if not params.value:
return

if (kind := params.value.get("kind", None)) == "begin":
type_: Type[Any] = types.WorkDoneProgressBegin
elif kind == "report":
type_ = types.WorkDoneProgressReport
elif kind == "end":
type_ = types.WorkDoneProgressEnd
else:
raise TypeError(f"Unknown progress kind: {kind!r}")

value = client.protocol._converter.structure(params.value, type_)
client.progress_reports.setdefault(params.token, []).append(value)

@client.feature(types.WINDOW_LOG_MESSAGE)
def log_message(client: LanguageClient, params: types.LogMessageParams):
client.log_messages.append(params)
Expand Down
7 changes: 6 additions & 1 deletion lib/pytest-lsp/pytest_lsp/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ def _handle_notification(self, method_name, params):
async def send_request_async(self, method, params=None):
"""Wrap pygls' ``send_request_async`` implementation. This will
- Check the params to see if they're compatible with the client's stated
capabilities
- Check the result to see if it's compatible with the client's stated
capabilities
Expand All @@ -71,8 +73,11 @@ async def send_request_async(self, method, params=None):
Returns
-------
Any
The response's result
The result
"""
check_params_against_client_capabilities(
self._server.capabilities, method, params
)
result = await super().send_request_async(method, params)
check_result_against_client_capabilities(
self._server.capabilities, method, result # type: ignore
Expand Down
64 changes: 64 additions & 0 deletions lib/pytest-lsp/tests/examples/window-create-progress/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from unittest.mock import Mock

from lsprotocol import types
from pygls.server import LanguageServer

server = LanguageServer("window-create-progress", "v1")


@server.command("do.progress")
async def do_progress(ls: LanguageServer, *args):
token = "a-token"

await ls.progress.create_async(token)

# Begin
ls.progress.begin(
token,
types.WorkDoneProgressBegin(title="Indexing", percentage=0),
)
# Report
for i in range(1, 4):
ls.progress.report(
token,
types.WorkDoneProgressReport(message=f"{i * 25}%", percentage=i * 25),
)
# End
ls.progress.end(token, types.WorkDoneProgressEnd(message="Finished"))

return "a result"


@server.command("duplicate.progress")
async def duplicate_progress(ls: LanguageServer, *args):
token = "duplicate-token"

# Need to stop pygls preventing us from using the progress API wrong.
ls.progress._check_token_registered = Mock()
await ls.progress.create_async(token)

# pytest-lsp should return an error here.
await ls.progress.create_async(token)


@server.command("no.progress")
async def no_progress(ls: LanguageServer, *args):
token = "undefined-token"

# Begin
ls.progress.begin(
token,
types.WorkDoneProgressBegin(title="Indexing", percentage=0, cancellable=False),
)
# Report
for i in range(1, 4):
ls.progress.report(
token,
types.WorkDoneProgressReport(message=f"{i * 25}%", percentage=i * 25),
)
# End
ls.progress.end(token, types.WorkDoneProgressEnd(message="Finished"))


if __name__ == "__main__":
server.start_io()
61 changes: 61 additions & 0 deletions lib/pytest-lsp/tests/examples/window-create-progress/t_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import sys

import pytest
from lsprotocol import types

import pytest_lsp
from pytest_lsp import ClientServerConfig
from pytest_lsp import LanguageClient
from pytest_lsp import LspSpecificationWarning


@pytest_lsp.fixture(
config=ClientServerConfig(server_command=[sys.executable, "server.py"]),
)
async def client(lsp_client: LanguageClient):
# Setup
params = types.InitializeParams(capabilities=types.ClientCapabilities())
await lsp_client.initialize_session(params)

yield

# Teardown
await lsp_client.shutdown_session()


@pytest.mark.asyncio
async def test_progress(client: LanguageClient):
result = await client.workspace_execute_command_async(
params=types.ExecuteCommandParams(command="do.progress")
)

assert result == "a result"

progress = client.progress_reports["a-token"]
assert progress == [
types.WorkDoneProgressBegin(title="Indexing", percentage=0),
types.WorkDoneProgressReport(message="25%", percentage=25),
types.WorkDoneProgressReport(message="50%", percentage=50),
types.WorkDoneProgressReport(message="75%", percentage=75),
types.WorkDoneProgressEnd(message="Finished"),
]


@pytest.mark.asyncio
async def test_duplicate_progress(client: LanguageClient):
with pytest.warns(
LspSpecificationWarning, match="Duplicate progress token: 'duplicate-token'"
):
await client.workspace_execute_command_async(
params=types.ExecuteCommandParams(command="duplicate.progress")
)


@pytest.mark.asyncio
async def test_unknown_progress(client: LanguageClient):
with pytest.warns(
LspSpecificationWarning, match="Unknown progress token: 'undefined-token'"
):
await client.workspace_execute_command_async(
params=types.ExecuteCommandParams(command="no.progress")
)
29 changes: 20 additions & 9 deletions lib/pytest-lsp/tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,28 @@ def setup_test(pytester: pytest.Pytester, example_name: str):
@pytest.mark.parametrize(
"name, expected",
[
("diagnostics", dict(passed=1)),
("getting-started", dict(passed=1)),
("fixture-passthrough", dict(passed=1)),
("parameterised-clients", dict(passed=2)),
("window-log-message", dict(passed=1)),
("window-show-document", dict(passed=1)),
("window-show-message", dict(passed=1)),
("workspace-configuration", dict(passed=1, warnings=1)),
pytest.param("diagnostics", dict(passed=1), id="diagnostics"),
pytest.param("getting-started", dict(passed=1), id="getting-started"),
pytest.param("fixture-passthrough", dict(passed=1), id="fixture-passthrough"),
pytest.param(
"parameterised-clients", dict(passed=2), id="parameterised-clients"
),
pytest.param("window-log-message", dict(passed=1), id="window-log-message"),
pytest.param(
"window-create-progress",
dict(passed=3),
id="window-create-progress",
),
pytest.param("window-show-document", dict(passed=1), id="window-show-document"),
pytest.param("window-show-message", dict(passed=1), id="window-show-message"),
pytest.param(
"workspace-configuration",
dict(passed=1, warnings=1),
id="workspace-configuration",
),
],
)
def test_documentation_examples(pytester: pytest.Pytester, name: str, expected: dict):
def test_examples(pytester: pytest.Pytester, name: str, expected: dict):
"""Ensure that the examples included in the documentation work as expected."""

setup_test(pytester, name)
Expand Down

0 comments on commit e82a8e6

Please sign in to comment.