Skip to content

Commit 00fb232

Browse files
committed
fixes #325: initialize LSP progress token before using it
1 parent 9d33ab0 commit 00fb232

File tree

6 files changed

+78
-17
lines changed

6 files changed

+78
-17
lines changed

Diff for: pylsp/_utils.py

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import jedi
1515

1616
JEDI_VERSION = jedi.__version__
17+
CALL_TIMEOUT = 10
1718

1819
# Eol chars accepted by the LSP protocol
1920
# the ordering affects performance

Diff for: pylsp/python_lsp.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ class PythonLSPServer(MethodDispatcher):
152152

153153
# pylint: disable=too-many-public-methods,redefined-builtin
154154

155-
def __init__(self, rx, tx, check_parent_process=False, consumer=None):
155+
def __init__(self, rx, tx, check_parent_process=False, consumer=None, *, endpoint_cls=None):
156156
self.workspace = None
157157
self.config = None
158158
self.root_uri = None
@@ -172,11 +172,13 @@ def __init__(self, rx, tx, check_parent_process=False, consumer=None):
172172
else:
173173
self._jsonrpc_stream_writer = None
174174

175+
endpoint_cls = endpoint_cls or Endpoint
176+
175177
# if consumer is None, it is assumed that the default streams-based approach is being used
176178
if consumer is None:
177-
self._endpoint = Endpoint(self, self._jsonrpc_stream_writer.write, max_workers=MAX_WORKERS)
179+
self._endpoint = endpoint_cls(self, self._jsonrpc_stream_writer.write, max_workers=MAX_WORKERS)
178180
else:
179-
self._endpoint = Endpoint(self, consumer, max_workers=MAX_WORKERS)
181+
self._endpoint = endpoint_cls(self, consumer, max_workers=MAX_WORKERS)
180182

181183
self._dispatchers = []
182184
self._shutdown = False

Diff for: pylsp/workspace.py

+4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class Workspace:
3737

3838
M_PUBLISH_DIAGNOSTICS = 'textDocument/publishDiagnostics'
3939
M_PROGRESS = '$/progress'
40+
M_INITIALIZE_PROGRESS = 'window/workDoneProgress/create'
4041
M_APPLY_EDIT = 'workspace/applyEdit'
4142
M_SHOW_MESSAGE = 'window/showMessage'
4243

@@ -152,6 +153,9 @@ def _progress_begin(
152153
percentage: Optional[int] = None,
153154
) -> str:
154155
token = str(uuid.uuid4())
156+
157+
self._endpoint.request(self.M_INITIALIZE_PROGRESS, {'token': token}).result(_utils.CALL_TIMEOUT)
158+
155159
value = {
156160
"kind": "begin",
157161
"title": title,

Diff for: test/fixtures.py

+46-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
from io import StringIO
66
from unittest.mock import MagicMock
77
import pytest
8+
from pylsp_jsonrpc.dispatchers import MethodDispatcher
89
from pylsp_jsonrpc.endpoint import Endpoint
10+
from pylsp_jsonrpc.exceptions import JsonRpcException
911

1012
from pylsp import uris
1113
from pylsp.config.config import Config
@@ -24,7 +26,7 @@ def main():
2426
@pytest.fixture
2527
def pylsp(tmpdir):
2628
""" Return an initialized python LS """
27-
ls = PythonLSPServer(StringIO, StringIO)
29+
ls = FakePythonLSPServer(StringIO, StringIO, endpoint_cls=FakeEndpoint)
2830

2931
ls.m_initialize(
3032
processId=1,
@@ -38,7 +40,7 @@ def pylsp(tmpdir):
3840
@pytest.fixture
3941
def pylsp_w_workspace_folders(tmpdir):
4042
""" Return an initialized python LS """
41-
ls = PythonLSPServer(StringIO, StringIO)
43+
ls = FakePythonLSPServer(StringIO, StringIO, endpoint_cls=FakeEndpoint)
4244

4345
folder1 = tmpdir.mkdir('folder1')
4446
folder2 = tmpdir.mkdir('folder2')
@@ -63,14 +65,55 @@ def pylsp_w_workspace_folders(tmpdir):
6365
return (ls, workspace_folders)
6466

6567

68+
class FakeEditorMethodsMixin:
69+
"""
70+
Represents the methods to be added to a dispatcher class when faking an editor.
71+
"""
72+
def m_window__work_done_progress__create(self, *args, **kwargs):
73+
"""
74+
Fake editor method `window/workDoneProgress/create`.
75+
76+
related spec:
77+
https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#window_workDoneProgress_create
78+
"""
79+
return None
80+
81+
82+
class FakePythonLSPServer(FakeEditorMethodsMixin, PythonLSPServer):
83+
pass
84+
85+
86+
class FakeEndpoint(Endpoint):
87+
"""
88+
Fake Endpoint representing the editor / LSP client.
89+
90+
The `dispatcher` dict will be used to synchronously calculate the responses
91+
for calls to `.request` and resolve the futures with the value or errors.
92+
93+
Fake methods in the `dispatcher` should raise `JsonRpcException` for any
94+
error.
95+
"""
96+
def request(self, method, params):
97+
request_future = super().request(method, params)
98+
try:
99+
request_future.set_result(self._dispatcher[method](params))
100+
except JsonRpcException as e:
101+
request_future.set_exception(e)
102+
103+
return request_future
104+
105+
66106
@pytest.fixture()
67107
def consumer():
68108
return MagicMock()
69109

70110

71111
@pytest.fixture()
72112
def endpoint(consumer): # pylint: disable=redefined-outer-name
73-
return Endpoint({}, consumer, id_generator=lambda: "id")
113+
class Dispatcher(FakeEditorMethodsMixin, MethodDispatcher):
114+
pass
115+
116+
return FakeEndpoint(Dispatcher(), consumer, id_generator=lambda: "id")
74117

75118

76119
@pytest.fixture

Diff for: test/test_language_server.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
import pytest
1313

1414
from pylsp.python_lsp import start_io_lang_server, PythonLSPServer
15+
from pylsp._utils import CALL_TIMEOUT
1516

16-
CALL_TIMEOUT = 10
1717
RUNNING_IN_CI = bool(os.environ.get('CI'))
1818

1919

Diff for: test/test_workspace.py

+21-10
Original file line numberDiff line numberDiff line change
@@ -299,13 +299,17 @@ def test_progress_simple(workspace, consumer):
299299
with workspace.report_progress("some_title"):
300300
pass
301301

302+
init_call, *progress_calls = consumer.call_args_list
303+
304+
assert init_call[0][0]['method'] == 'window/workDoneProgress/create'
305+
302306
# same method for all calls
303-
assert all(call[0][0]["method"] == "$/progress" for call in consumer.call_args_list)
307+
assert all(call[0][0]["method"] == "$/progress" for call in progress_calls), consumer.call_args_list
304308

305309
# same token used in all calls
306-
assert len({call[0][0]["params"]["token"] for call in consumer.call_args_list}) == 1
310+
assert len({call[0][0]["params"]["token"] for call in progress_calls} | {init_call[0][0]['params']['token']}) == 1
307311

308-
assert [call[0][0]["params"]["value"] for call in consumer.call_args_list] == [
312+
assert [call[0][0]["params"]["value"] for call in progress_calls] == [
309313
{"kind": "begin", "title": "some_title"},
310314
{"kind": "end"},
311315
]
@@ -319,13 +323,17 @@ def test_progress_with_percent(workspace, consumer):
319323
progress_message("fifty", 50)
320324
progress_message("ninety", 90)
321325

322-
# same method for all calls
323-
assert all(call[0][0]["method"] == "$/progress" for call in consumer.call_args_list)
326+
init_call, *progress_calls = consumer.call_args_list
327+
328+
assert init_call[0][0]['method'] == 'window/workDoneProgress/create'
329+
330+
# same method for all progress calls
331+
assert all(call[0][0]["method"] == "$/progress" for call in progress_calls)
324332

325333
# same token used in all calls
326-
assert len({call[0][0]["params"]["token"] for call in consumer.call_args_list}) == 1
334+
assert len({call[0][0]["params"]["token"] for call in progress_calls} | {init_call[0][0]['params']['token']}) == 1
327335

328-
assert [call[0][0]["params"]["value"] for call in consumer.call_args_list] == [
336+
assert [call[0][0]["params"]["value"] for call in progress_calls] == [
329337
{
330338
"kind": "begin",
331339
"message": "initial message",
@@ -353,13 +361,16 @@ class DummyError(Exception):
353361
# test.
354362
pass
355363

364+
init_call, *progress_calls = consumer.call_args_list
365+
assert init_call[0][0]['method'] == 'window/workDoneProgress/create'
366+
356367
# same method for all calls
357-
assert all(call[0][0]["method"] == "$/progress" for call in consumer.call_args_list)
368+
assert all(call[0][0]["method"] == "$/progress" for call in progress_calls)
358369

359370
# same token used in all calls
360-
assert len({call[0][0]["params"]["token"] for call in consumer.call_args_list}) == 1
371+
assert len({call[0][0]["params"]["token"] for call in progress_calls} | {init_call[0][0]['params']['token']}) == 1
361372

362-
assert [call[0][0]["params"]["value"] for call in consumer.call_args_list] == [
373+
assert [call[0][0]["params"]["value"] for call in progress_calls] == [
363374
{"kind": "begin", "title": "some_title"},
364375
{"kind": "end"},
365376
]

0 commit comments

Comments
 (0)