diff --git a/pylsp/_utils.py b/pylsp/_utils.py index 2c6111d8..610cdbde 100644 --- a/pylsp/_utils.py +++ b/pylsp/_utils.py @@ -293,8 +293,7 @@ def is_process_alive(pid): os.kill(pid, 0) except OSError as e: return e.errno == errno.EPERM - else: - return True + return True def get_eol_chars(text): diff --git a/pylsp/plugins/autopep8_format.py b/pylsp/plugins/autopep8_format.py index 44f45dc2..50be2c33 100644 --- a/pylsp/plugins/autopep8_format.py +++ b/pylsp/plugins/autopep8_format.py @@ -23,17 +23,16 @@ def pylsp_format_document(config, workspace, document, options): # pylint: disa def pylsp_format_range( config, workspace, document, range, options ): # pylint: disable=redefined-builtin,unused-argument - with workspace.report_progress("format_range: autopep8"): - log.info("Formatting document %s in range %s with autopep8", document, range) + log.info("Formatting document %s in range %s with autopep8", document, range) - # First we 'round' the range up/down to full lines only - range['start']['character'] = 0 - range['end']['line'] += 1 - range['end']['character'] = 0 + # First we 'round' the range up/down to full lines only + range['start']['character'] = 0 + range['end']['line'] += 1 + range['end']['character'] = 0 - # Add 1 for 1-indexing vs LSP's 0-indexing - line_range = (range['start']['line'] + 1, range['end']['line'] + 1) - return _format(config, document, line_range=line_range) + # Add 1 for 1-indexing vs LSP's 0-indexing + line_range = (range['start']['line'] + 1, range['end']['line'] + 1) + return _format(config, document, line_range=line_range) def _format(config, document, line_range=None): diff --git a/pylsp/plugins/definition.py b/pylsp/plugins/definition.py index b2110af4..fe06ab3c 100644 --- a/pylsp/plugins/definition.py +++ b/pylsp/plugins/definition.py @@ -8,26 +8,25 @@ @hookimpl -def pylsp_definitions(config, workspace, document, position): - with workspace.report_progress("go to definitions"): - settings = config.plugin_settings('jedi_definition') - code_position = _utils.position_to_jedi_linecolumn(document, position) - definitions = document.jedi_script(use_document_path=True).goto( - follow_imports=settings.get('follow_imports', True), - follow_builtin_imports=settings.get('follow_builtin_imports', True), - **code_position) +def pylsp_definitions(config, document, position): + settings = config.plugin_settings('jedi_definition') + code_position = _utils.position_to_jedi_linecolumn(document, position) + definitions = document.jedi_script(use_document_path=True).goto( + follow_imports=settings.get('follow_imports', True), + follow_builtin_imports=settings.get('follow_builtin_imports', True), + **code_position) - follow_builtin_defns = settings.get("follow_builtin_definitions", True) - return [ - { - 'uri': uris.uri_with(document.uri, path=str(d.module_path)), - 'range': { - 'start': {'line': d.line - 1, 'character': d.column}, - 'end': {'line': d.line - 1, 'character': d.column + len(d.name)}, - } + follow_builtin_defns = settings.get("follow_builtin_definitions", True) + return [ + { + 'uri': uris.uri_with(document.uri, path=str(d.module_path)), + 'range': { + 'start': {'line': d.line - 1, 'character': d.column}, + 'end': {'line': d.line - 1, 'character': d.column + len(d.name)}, } - for d in definitions if d.is_definition() and (follow_builtin_defns or _not_internal_definition(d)) - ] + } + for d in definitions if d.is_definition() and (follow_builtin_defns or _not_internal_definition(d)) + ] def _not_internal_definition(definition): diff --git a/pylsp/plugins/jedi_rename.py b/pylsp/plugins/jedi_rename.py index 64949bb0..d44b28e9 100644 --- a/pylsp/plugins/jedi_rename.py +++ b/pylsp/plugins/jedi_rename.py @@ -9,44 +9,42 @@ @hookimpl -def pylsp_rename(config, workspace, document, position, new_name): # pylint: disable=unused-argument,too-many-locals - with workspace.report_progress("rename", percentage=0) as report_progress: - log.debug('Executing rename of %s to %s', document.word_at_position(position), new_name) - kwargs = _utils.position_to_jedi_linecolumn(document, position) - kwargs['new_name'] = new_name - report_progress("refactoring") - try: - refactoring = document.jedi_script().rename(**kwargs) - except NotImplementedError as exc: - raise Exception('No support for renaming in Python 2/3.5 with Jedi. ' - 'Consider using the rope_rename plugin instead') from exc - log.debug('Finished rename: %s', refactoring.get_diff()) - changes = [] - - changed_files = refactoring.get_changed_files() - for n, (file_path, changed_file) in enumerate(changed_files.items()): - report_progress(changed_file, percentage=n/len(changed_files)*100) - uri = uris.from_fs_path(str(file_path)) - doc = workspace.get_maybe_document(uri) - changes.append({ - 'textDocument': { - 'uri': uri, - 'version': doc.version if doc else None - }, - 'edits': [ - { - 'range': { - 'start': {'line': 0, 'character': 0}, - 'end': { - 'line': _num_lines(changed_file.get_new_code()), - 'character': 0, - }, +def pylsp_rename(config, workspace, document, position, new_name): # pylint: disable=unused-argument + log.debug('Executing rename of %s to %s', document.word_at_position(position), new_name) + kwargs = _utils.position_to_jedi_linecolumn(document, position) + kwargs['new_name'] = new_name + try: + refactoring = document.jedi_script().rename(**kwargs) + except NotImplementedError as exc: + # pylint: disable=broad-exception-raised + raise Exception('No support for renaming in Python 2/3.5 with Jedi. ' + 'Consider using the rope_rename plugin instead') from exc + log.debug('Finished rename: %s', refactoring.get_diff()) + changes = [] + + changed_files = refactoring.get_changed_files() + for file_path, changed_file in changed_files.items(): + uri = uris.from_fs_path(str(file_path)) + doc = workspace.get_maybe_document(uri) + changes.append({ + 'textDocument': { + 'uri': uri, + 'version': doc.version if doc else None + }, + 'edits': [ + { + 'range': { + 'start': {'line': 0, 'character': 0}, + 'end': { + 'line': _num_lines(changed_file.get_new_code()), + 'character': 0, }, - 'newText': changed_file.get_new_code(), - } - ], - }) - return {'documentChanges': changes} + }, + 'newText': changed_file.get_new_code(), + } + ], + }) + return {'documentChanges': changes} def _num_lines(file_contents): diff --git a/pylsp/plugins/references.py b/pylsp/plugins/references.py index 9873d7e1..4ef2072a 100644 --- a/pylsp/plugins/references.py +++ b/pylsp/plugins/references.py @@ -8,20 +8,19 @@ @hookimpl -def pylsp_references(document, workspace, position, exclude_declaration=False): - with workspace.report_progress("references"): - code_position = _utils.position_to_jedi_linecolumn(document, position) - usages = document.jedi_script().get_references(**code_position) +def pylsp_references(document, position, exclude_declaration=False): + code_position = _utils.position_to_jedi_linecolumn(document, position) + usages = document.jedi_script().get_references(**code_position) - if exclude_declaration: - # Filter out if the usage is the actual declaration of the thing - usages = [d for d in usages if not d.is_definition()] + if exclude_declaration: + # Filter out if the usage is the actual declaration of the thing + usages = [d for d in usages if not d.is_definition()] - # Filter out builtin modules - return [{ - 'uri': uris.uri_with(document.uri, path=str(d.module_path)) if d.module_path else document.uri, - 'range': { - 'start': {'line': d.line - 1, 'character': d.column}, - 'end': {'line': d.line - 1, 'character': d.column + len(d.name)} - } - } for d in usages if not d.in_builtin_module()] + # Filter out builtin modules + return [{ + 'uri': uris.uri_with(document.uri, path=str(d.module_path)) if d.module_path else document.uri, + 'range': { + 'start': {'line': d.line - 1, 'character': d.column}, + 'end': {'line': d.line - 1, 'character': d.column + len(d.name)} + } + } for d in usages if not d.in_builtin_module()] diff --git a/pylsp/plugins/rope_rename.py b/pylsp/plugins/rope_rename.py index eccbbbec..d9ebab5c 100644 --- a/pylsp/plugins/rope_rename.py +++ b/pylsp/plugins/rope_rename.py @@ -19,42 +19,41 @@ def pylsp_settings(): @hookimpl def pylsp_rename(config, workspace, document, position, new_name): - with workspace.report_progress("rename"): - rope_config = config.settings(document_path=document.path).get('rope', {}) - rope_project = workspace._rope_project_builder(rope_config) - - rename = Rename( - rope_project, - libutils.path_to_resource(rope_project, document.path), - document.offset_at_position(position) - ) - - log.debug("Executing rename of %s to %s", document.word_at_position(position), new_name) - changeset = rename.get_changes(new_name, in_hierarchy=True, docs=True) - log.debug("Finished rename: %s", changeset.changes) - changes = [] - for change in changeset.changes: - uri = uris.from_fs_path(change.resource.path) - doc = workspace.get_maybe_document(uri) - changes.append({ - 'textDocument': { - 'uri': uri, - 'version': doc.version if doc else None - }, - 'edits': [ - { - 'range': { - 'start': {'line': 0, 'character': 0}, - 'end': { - 'line': _num_lines(change.resource), - 'character': 0, - }, + rope_config = config.settings(document_path=document.path).get('rope', {}) + rope_project = workspace._rope_project_builder(rope_config) + + rename = Rename( + rope_project, + libutils.path_to_resource(rope_project, document.path), + document.offset_at_position(position) + ) + + log.debug("Executing rename of %s to %s", document.word_at_position(position), new_name) + changeset = rename.get_changes(new_name, in_hierarchy=True, docs=True) + log.debug("Finished rename: %s", changeset.changes) + changes = [] + for change in changeset.changes: + uri = uris.from_fs_path(change.resource.path) + doc = workspace.get_maybe_document(uri) + changes.append({ + 'textDocument': { + 'uri': uri, + 'version': doc.version if doc else None + }, + 'edits': [ + { + 'range': { + 'start': {'line': 0, 'character': 0}, + 'end': { + 'line': _num_lines(change.resource), + 'character': 0, }, - 'newText': change.new_contents, - } - ] - }) - return {'documentChanges': changes} + }, + 'newText': change.new_contents, + } + ] + }) + return {'documentChanges': changes} def _num_lines(resource): diff --git a/pylsp/plugins/yapf_format.py b/pylsp/plugins/yapf_format.py index 754d9cbb..308fd5cf 100644 --- a/pylsp/plugins/yapf_format.py +++ b/pylsp/plugins/yapf_format.py @@ -23,23 +23,22 @@ def pylsp_format_document(workspace, document, options): @hookimpl -def pylsp_format_range(workspace, document, range, options): # pylint: disable=redefined-builtin +def pylsp_format_range(document, range, options): # pylint: disable=redefined-builtin log.info("Formatting document %s in range %s with yapf", document, range) - with workspace.report_progress("format_range: yapf"): - # First we 'round' the range up/down to full lines only - range['start']['character'] = 0 - range['end']['line'] += 1 - range['end']['character'] = 0 - - # From Yapf docs: - # lines: (list of tuples of integers) A list of tuples of lines, [start, end], - # that we want to format. The lines are 1-based indexed. It can be used by - # third-party code (e.g., IDEs) when reformatting a snippet of code rather - # than a whole file. - - # Add 1 for 1-indexing vs LSP's 0-indexing - lines = [(range['start']['line'] + 1, range['end']['line'] + 1)] - return _format(document, lines=lines, options=options) + # First we 'round' the range up/down to full lines only + range['start']['character'] = 0 + range['end']['line'] += 1 + range['end']['character'] = 0 + + # From Yapf docs: + # lines: (list of tuples of integers) A list of tuples of lines, [start, end], + # that we want to format. The lines are 1-based indexed. It can be used by + # third-party code (e.g., IDEs) when reformatting a snippet of code rather + # than a whole file. + + # Add 1 for 1-indexing vs LSP's 0-indexing + lines = [(range['start']['line'] + 1, range['end']['line'] + 1)] + return _format(document, lines=lines, options=options) def get_style_config(document_path, options=None): diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index e663a8a0..43f886cc 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -152,7 +152,7 @@ class PythonLSPServer(MethodDispatcher): # pylint: disable=too-many-public-methods,redefined-builtin - def __init__(self, rx, tx, check_parent_process=False, consumer=None): + def __init__(self, rx, tx, check_parent_process=False, consumer=None, *, endpoint_cls=None): self.workspace = None self.config = None self.root_uri = None @@ -172,11 +172,13 @@ def __init__(self, rx, tx, check_parent_process=False, consumer=None): else: self._jsonrpc_stream_writer = None + endpoint_cls = endpoint_cls or Endpoint + # if consumer is None, it is assumed that the default streams-based approach is being used if consumer is None: - self._endpoint = Endpoint(self, self._jsonrpc_stream_writer.write, max_workers=MAX_WORKERS) + self._endpoint = endpoint_cls(self, self._jsonrpc_stream_writer.write, max_workers=MAX_WORKERS) else: - self._endpoint = Endpoint(self, consumer, max_workers=MAX_WORKERS) + self._endpoint = endpoint_cls(self, consumer, max_workers=MAX_WORKERS) self._dispatchers = [] self._shutdown = False @@ -358,7 +360,7 @@ def execute_command(self, command, arguments): return self._hook('pylsp_execute_command', command=command, arguments=arguments) def format_document(self, doc_uri, options): - return self._hook('pylsp_format_document', doc_uri, options=options) + return lambda: self._hook('pylsp_format_document', doc_uri, options=options) def format_range(self, doc_uri, range, options): return self._hook('pylsp_format_range', doc_uri, range=range, options=options) diff --git a/pylsp/workspace.py b/pylsp/workspace.py index 5647a90a..ea7f55e8 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -37,6 +37,7 @@ class Workspace: M_PUBLISH_DIAGNOSTICS = 'textDocument/publishDiagnostics' M_PROGRESS = '$/progress' + M_INITIALIZE_PROGRESS = 'window/workDoneProgress/create' M_APPLY_EDIT = 'workspace/applyEdit' M_SHOW_MESSAGE = 'window/showMessage' @@ -135,15 +136,43 @@ def report_progress( message: Optional[str] = None, percentage: Optional[int] = None, ) -> Generator[Callable[[str, Optional[int]], None], None, None]: - token = self._progress_begin(title, message, percentage) + if self._config: + client_supports_progress_reporting = ( + self._config.capabilities.get("window", {}).get("workDoneProgress", False) + ) + else: + client_supports_progress_reporting = False + + if client_supports_progress_reporting: + try: + token = self._progress_begin(title, message, percentage) + except Exception: # pylint: disable=broad-exception-caught + log.warning( + "There was an error while trying to initialize progress reporting." + "Likely progress reporting was used in a synchronous LSP handler, " + "which is not supported by progress reporting yet.", + exc_info=True + ) + + else: + def progress_message(message: str, percentage: Optional[int] = None) -> None: + self._progress_report(token, message, percentage) + + try: + yield progress_message + finally: + self._progress_end(token) - def progress_message(message: str, percentage: Optional[int] = None) -> None: - self._progress_report(token, message, percentage) + return - try: - yield progress_message - finally: - self._progress_end(token) + # FALLBACK: + # If the client doesn't support progress reporting, or if we failed to + # initialize it, we have a dummy method for the caller to use. + def dummy_progress_message(message: str, percentage: Optional[int] = None) -> None: + # pylint: disable=unused-argument + pass + + yield dummy_progress_message def _progress_begin( self, @@ -152,13 +181,16 @@ def _progress_begin( percentage: Optional[int] = None, ) -> str: token = str(uuid.uuid4()) + + self._endpoint.request(self.M_INITIALIZE_PROGRESS, {'token': token}).result(timeout=1.0) + value = { "kind": "begin", "title": title, } - if message: + if message is not None: value["message"] = message - if percentage: + if percentage is not None: value["percentage"] = percentage self._endpoint.notify( diff --git a/test/fixtures.py b/test/fixtures.py index 5763d462..7c7bcf62 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -4,8 +4,11 @@ import os from io import StringIO from unittest.mock import MagicMock + import pytest +from pylsp_jsonrpc.dispatchers import MethodDispatcher from pylsp_jsonrpc.endpoint import Endpoint +from pylsp_jsonrpc.exceptions import JsonRpcException from pylsp import uris from pylsp.config.config import Config @@ -21,10 +24,48 @@ def main(): """ +class FakeEditorMethodsMixin: + """ + Represents the methods to be added to a dispatcher class when faking an editor. + """ + def m_window__work_done_progress__create(self, *_args, **_kwargs): + """ + Fake editor method `window/workDoneProgress/create`. + + related spec: + https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#window_workDoneProgress_create + """ + return None + + +class FakePythonLSPServer(FakeEditorMethodsMixin, PythonLSPServer): + pass + + +class FakeEndpoint(Endpoint): + """ + Fake Endpoint representing the editor / LSP client. + + The `dispatcher` dict will be used to synchronously calculate the responses + for calls to `.request` and resolve the futures with the value or errors. + + Fake methods in the `dispatcher` should raise `JsonRpcException` for any + error. + """ + def request(self, method, params=None): + request_future = super().request(method, params) + try: + request_future.set_result(self._dispatcher[method](params)) + except JsonRpcException as e: + request_future.set_exception(e) + + return request_future + + @pytest.fixture def pylsp(tmpdir): """ Return an initialized python LS """ - ls = PythonLSPServer(StringIO, StringIO) + ls = FakePythonLSPServer(StringIO, StringIO, endpoint_cls=FakeEndpoint) ls.m_initialize( processId=1, @@ -38,7 +79,7 @@ def pylsp(tmpdir): @pytest.fixture def pylsp_w_workspace_folders(tmpdir): """ Return an initialized python LS """ - ls = PythonLSPServer(StringIO, StringIO) + ls = FakePythonLSPServer(StringIO, StringIO, endpoint_cls=FakeEndpoint) folder1 = tmpdir.mkdir('folder1') folder2 = tmpdir.mkdir('folder2') @@ -70,7 +111,10 @@ def consumer(): @pytest.fixture() def endpoint(consumer): # pylint: disable=redefined-outer-name - return Endpoint({}, consumer, id_generator=lambda: "id") + class Dispatcher(FakeEditorMethodsMixin, MethodDispatcher): + pass + + return FakeEndpoint(Dispatcher(), consumer, id_generator=lambda: "id") @pytest.fixture diff --git a/test/plugins/test_definitions.py b/test/plugins/test_definitions.py index a8972cd2..4a8855c6 100644 --- a/test/plugins/test_definitions.py +++ b/test/plugins/test_definitions.py @@ -35,7 +35,7 @@ def test_definitions(config, workspace): } doc = Document(DOC_URI, workspace, DOC) - assert [{'uri': DOC_URI, 'range': def_range}] == pylsp_definitions(config, workspace, doc, cursor_pos) + assert [{'uri': DOC_URI, 'range': def_range}] == pylsp_definitions(config, doc, cursor_pos) def test_builtin_definition(config, workspace): @@ -49,14 +49,14 @@ def test_builtin_definition(config, workspace): follow_defns_setting = {'follow_builtin_definitions': True} settings = {'plugins': {'jedi_definition': follow_defns_setting}} config.update(settings) - defns = pylsp_definitions(config, workspace, doc, cursor_pos) + defns = pylsp_definitions(config, doc, cursor_pos) assert len(defns) == 1 assert defns[0]["uri"].endswith("builtins.pyi") # Check no definitions for `dict` follow_defns_setting['follow_builtin_definitions'] = False config.update(settings) - defns = pylsp_definitions(config, workspace, doc, cursor_pos) + defns = pylsp_definitions(config, doc, cursor_pos) assert not defns config.update(orig_settings) @@ -73,7 +73,7 @@ def test_assignment(config, workspace): } doc = Document(DOC_URI, workspace, DOC) - assert [{'uri': DOC_URI, 'range': def_range}] == pylsp_definitions(config, workspace, doc, cursor_pos) + assert [{'uri': DOC_URI, 'range': def_range}] == pylsp_definitions(config, doc, cursor_pos) def test_document_path_definitions(config, workspace_other_root_path, tmpdir): @@ -107,5 +107,5 @@ def foo(): module_uri = uris.from_fs_path(module_path) assert [{"uri": module_uri, "range": def_range}] == pylsp_definitions( - config, workspace_other_root_path, doc, cursor_pos + config, doc, cursor_pos ) diff --git a/test/plugins/test_references.py b/test/plugins/test_references.py index 79cd7a0e..c1df037b 100644 --- a/test/plugins/test_references.py +++ b/test/plugins/test_references.py @@ -40,13 +40,13 @@ def test_references(tmp_workspace): # pylint: disable=redefined-outer-name DOC1_URI = uris.from_fs_path(os.path.join(tmp_workspace.root_path, DOC1_NAME)) doc1 = Document(DOC1_URI, tmp_workspace) - refs = pylsp_references(doc1, tmp_workspace, position) + refs = pylsp_references(doc1, position) # Definition, the import and the instantiation assert len(refs) == 3 # Briefly check excluding the definitions (also excludes imports, only counts uses) - no_def_refs = pylsp_references(doc1, tmp_workspace, position, exclude_declaration=True) + no_def_refs = pylsp_references(doc1, position, exclude_declaration=True) assert len(no_def_refs) == 1 # Make sure our definition is correctly located @@ -70,7 +70,7 @@ def test_references_builtin(tmp_workspace): # pylint: disable=redefined-outer-n doc2_uri = uris.from_fs_path(os.path.join(str(tmp_workspace.root_path), DOC2_NAME)) doc2 = Document(doc2_uri, tmp_workspace) - refs = pylsp_references(doc2, tmp_workspace, position) + refs = pylsp_references(doc2, position) assert len(refs) >= 1 expected = {'start': {'line': 4, 'character': 7}, diff --git a/test/plugins/test_yapf_format.py b/test/plugins/test_yapf_format.py index 0e989c0d..62808e6a 100644 --- a/test/plugins/test_yapf_format.py +++ b/test/plugins/test_yapf_format.py @@ -41,7 +41,7 @@ def test_range_format(workspace): 'start': {'line': 0, 'character': 0}, 'end': {'line': 4, 'character': 10} } - res = pylsp_format_range(workspace, doc, def_range, None) + res = pylsp_format_range(doc, def_range, None) # Make sure B is still badly formatted assert apply_text_edits(doc, res) == "A = ['h', 'w', 'a']\n\nB = ['h',\n\n\n'w']\n" diff --git a/test/test_workspace.py b/test/test_workspace.py index 6699b4b8..94a9cba1 100644 --- a/test/test_workspace.py +++ b/test/test_workspace.py @@ -295,23 +295,56 @@ def test_settings_of_added_workspace(pylsp, tmpdir): assert workspace1_jedi_settings == server_settings['pylsp']['plugins']['jedi'] +def test_no_progress_without_capability(workspace, consumer): + workspace._config.capabilities['window'] = {"workDoneProgress": False} + + with workspace.report_progress("some_title"): + pass + + assert len(consumer.call_args_list) == 0 + + def test_progress_simple(workspace, consumer): + workspace._config.capabilities['window'] = {"workDoneProgress": True} + with workspace.report_progress("some_title"): pass + init_call, *progress_calls = consumer.call_args_list + + assert init_call[0][0]['method'] == 'window/workDoneProgress/create' + # same method for all calls - assert all(call[0][0]["method"] == "$/progress" for call in consumer.call_args_list) + assert all(call[0][0]["method"] == "$/progress" for call in progress_calls), consumer.call_args_list # same token used in all calls - assert len({call[0][0]["params"]["token"] for call in consumer.call_args_list}) == 1 + assert len({call[0][0]["params"]["token"] for call in progress_calls} | {init_call[0][0]['params']['token']}) == 1 - assert [call[0][0]["params"]["value"] for call in consumer.call_args_list] == [ + assert [call[0][0]["params"]["value"] for call in progress_calls] == [ {"kind": "begin", "title": "some_title"}, {"kind": "end"}, ] +@pytest.mark.parametrize("exc", [Exception("something"), TimeoutError()]) +def test_progress_initialization_fails(workspace, consumer, endpoint, exc): + def failing_token_initialization(self, *_args, **_kwargs): + raise exc + endpoint._dispatcher.m_window__work_done_progress__create = failing_token_initialization + + workspace._config.capabilities['window'] = {"workDoneProgress": True} + + with workspace.report_progress("some_title"): + pass + + # we only see the failing token initialization call, no other calls + init_call, = consumer.call_args_list + assert init_call[0][0]['method'] == 'window/workDoneProgress/create' + + def test_progress_with_percent(workspace, consumer): + workspace._config.capabilities['window'] = {"workDoneProgress": True} + with workspace.report_progress( "some_title", "initial message", percentage=1 ) as progress_message: @@ -319,13 +352,17 @@ def test_progress_with_percent(workspace, consumer): progress_message("fifty", 50) progress_message("ninety", 90) - # same method for all calls - assert all(call[0][0]["method"] == "$/progress" for call in consumer.call_args_list) + init_call, *progress_calls = consumer.call_args_list + + assert init_call[0][0]['method'] == 'window/workDoneProgress/create' + + # same method for all progress calls + assert all(call[0][0]["method"] == "$/progress" for call in progress_calls) # same token used in all calls - assert len({call[0][0]["params"]["token"] for call in consumer.call_args_list}) == 1 + assert len({call[0][0]["params"]["token"] for call in progress_calls} | {init_call[0][0]['params']['token']}) == 1 - assert [call[0][0]["params"]["value"] for call in consumer.call_args_list] == [ + assert [call[0][0]["params"]["value"] for call in progress_calls] == [ { "kind": "begin", "message": "initial message", @@ -340,6 +377,8 @@ def test_progress_with_percent(workspace, consumer): def test_progress_with_exception(workspace, consumer): + workspace._config.capabilities['window'] = {"workDoneProgress": True} + class DummyError(Exception): pass @@ -353,13 +392,16 @@ class DummyError(Exception): # test. pass + init_call, *progress_calls = consumer.call_args_list + assert init_call[0][0]['method'] == 'window/workDoneProgress/create' + # same method for all calls - assert all(call[0][0]["method"] == "$/progress" for call in consumer.call_args_list) + assert all(call[0][0]["method"] == "$/progress" for call in progress_calls) # same token used in all calls - assert len({call[0][0]["params"]["token"] for call in consumer.call_args_list}) == 1 + assert len({call[0][0]["params"]["token"] for call in progress_calls} | {init_call[0][0]['params']['token']}) == 1 - assert [call[0][0]["params"]["value"] for call in consumer.call_args_list] == [ + assert [call[0][0]["params"]["value"] for call in progress_calls] == [ {"kind": "begin", "title": "some_title"}, {"kind": "end"}, ]