From 6c168d0f8ee2e59ec8438373bc2c1b33cb74cdfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Krassowski?= <5832902+krassowski@users.noreply.github.com> Date: Sat, 9 Sep 2023 22:39:22 +0100 Subject: [PATCH 01/23] Add black reformatting commit to `.git-blame-ignore-revs` (#436) --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..96147baf --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Reformatting with Black +d47dc3c1fd1f2bafcc079006c3283e465b372f75 From 8377b2cb11208dfda917100807cf3d6019ac58ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Krassowski?= <5832902+krassowski@users.noreply.github.com> Date: Sun, 10 Sep 2023 17:11:42 +0100 Subject: [PATCH 02/23] Fix `include_declaration` handling in references request (#440) --- pylsp/plugins/references.py | 2 +- test/plugins/test_references.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pylsp/plugins/references.py b/pylsp/plugins/references.py index cfdf86b5..fadf1de8 100644 --- a/pylsp/plugins/references.py +++ b/pylsp/plugins/references.py @@ -8,7 +8,7 @@ @hookimpl -def pylsp_references(document, position, exclude_declaration=False): +def pylsp_references(document, position, exclude_declaration): code_position = _utils.position_to_jedi_linecolumn(document, position) usages = document.jedi_script().get_references(**code_position) diff --git a/test/plugins/test_references.py b/test/plugins/test_references.py index 20906dff..e8030a42 100644 --- a/test/plugins/test_references.py +++ b/test/plugins/test_references.py @@ -42,7 +42,7 @@ 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, position) + refs = pylsp_references(doc1, position, exclude_declaration=False) # Definition, the import and the instantiation assert len(refs) == 3 @@ -72,7 +72,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, position) + refs = pylsp_references(doc2, position, exclude_declaration=False) assert len(refs) >= 1 expected = { From 6059aa39e4fcc7b645c2f2c7f3d9463c1ab58f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Krassowski?= <5832902+krassowski@users.noreply.github.com> Date: Sun, 10 Sep 2023 18:22:16 +0100 Subject: [PATCH 03/23] Pass a single copy of the document's source around for flake8 (#441) --- pylsp/plugins/flake8_lint.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pylsp/plugins/flake8_lint.py b/pylsp/plugins/flake8_lint.py index 068585ef..d31783bf 100644 --- a/pylsp/plugins/flake8_lint.py +++ b/pylsp/plugins/flake8_lint.py @@ -79,11 +79,15 @@ def pylsp_lint(workspace, document): flake8_executable = settings.get("executable", "flake8") args = build_args(opts) - output = run_flake8(flake8_executable, args, document) - return parse_stdout(document, output) + # ensure the same source is used for flake8 execution and result parsing; + # single source access improves performance as it is only one disk access + source = document.source + output = run_flake8(flake8_executable, args, document, source) + return parse_stdout(source, output) -def run_flake8(flake8_executable, args, document): + +def run_flake8(flake8_executable, args, document, source): """Run flake8 with the provided arguments, logs errors from stderr if any. """ @@ -127,7 +131,7 @@ def run_flake8(flake8_executable, args, document): p = Popen( # pylint: disable=consider-using-with cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, **popen_kwargs ) - (stdout, stderr) = p.communicate(document.source.encode()) + (stdout, stderr) = p.communicate(source.encode()) if stderr: log.error("Error while running flake8 '%s'", stderr.decode()) return stdout.decode() @@ -155,7 +159,7 @@ def build_args(options): return args -def parse_stdout(document, stdout): +def parse_stdout(source, stdout): """ Build a diagnostics from flake8's output, it should extract every result and format it into a dict that looks like this: @@ -183,6 +187,7 @@ def parse_stdout(document, stdout): A list of dictionaries. """ + document_lines = source.splitlines(True) diagnostics = [] lines = stdout.splitlines() for raw_line in lines: @@ -212,7 +217,7 @@ def parse_stdout(document, stdout): "end": { "line": line, # no way to determine the column - "character": len(document.lines[line]), + "character": len(document_lines[line]), }, }, "message": msg, From 4211502fabf650d49f1731d608699dd07ea41723 Mon Sep 17 00:00:00 2001 From: Stephen Macke Date: Sat, 30 Sep 2023 07:40:45 -0700 Subject: [PATCH 04/23] Allow Jedi "goto" to perform multiple hops for "go to definition" (#443) --- pylsp/plugins/definition.py | 42 ++++++++++++++++++++++---- test/plugins/test_definitions.py | 51 +++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 6 deletions(-) diff --git a/pylsp/plugins/definition.py b/pylsp/plugins/definition.py index a5ccbd70..ffc1b00b 100644 --- a/pylsp/plugins/definition.py +++ b/pylsp/plugins/definition.py @@ -1,22 +1,54 @@ # Copyright 2017-2020 Palantir Technologies, Inc. # Copyright 2021- Python Language Server Contributors. - +from __future__ import annotations import logging +from typing import Any, Dict, List, TYPE_CHECKING from pylsp import hookimpl, uris, _utils +if TYPE_CHECKING: + from jedi.api import Script + from jedi.api.classes import Name + from pylsp.config.config import Config + from pylsp.workspace import Document + log = logging.getLogger(__name__) +MAX_JEDI_GOTO_HOPS = 100 + + +def _resolve_definition( + maybe_defn: Name, script: Script, settings: Dict[str, Any] +) -> Name: + for _ in range(MAX_JEDI_GOTO_HOPS): + if maybe_defn.is_definition() or maybe_defn.module_path != script.path: + break + defns = script.goto( + follow_imports=settings.get("follow_imports", True), + follow_builtin_imports=settings.get("follow_builtin_imports", True), + line=maybe_defn.line, + column=maybe_defn.column, + ) + if len(defns) == 1: + maybe_defn = defns[0] + else: + break + return maybe_defn + + @hookimpl -def pylsp_definitions(config, document, position): +def pylsp_definitions( + config: Config, document: Document, position: Dict[str, int] +) -> List[Dict[str, Any]]: settings = config.plugin_settings("jedi_definition") code_position = _utils.position_to_jedi_linecolumn(document, position) - definitions = document.jedi_script(use_document_path=True).goto( + script = document.jedi_script(use_document_path=True) + definitions = script.goto( follow_imports=settings.get("follow_imports", True), follow_builtin_imports=settings.get("follow_builtin_imports", True), **code_position, ) - + definitions = [_resolve_definition(d, script, settings) for d in definitions] follow_builtin_defns = settings.get("follow_builtin_definitions", True) return [ { @@ -31,7 +63,7 @@ def pylsp_definitions(config, document, position): ] -def _not_internal_definition(definition): +def _not_internal_definition(definition: Name) -> bool: return ( definition.line is not None and definition.column is not None diff --git a/test/plugins/test_definitions.py b/test/plugins/test_definitions.py index 34acc6a9..f0e9ffef 100644 --- a/test/plugins/test_definitions.py +++ b/test/plugins/test_definitions.py @@ -12,7 +12,7 @@ DOC = """def a(): pass -print a() +print(a()) class Directory(object): @@ -21,6 +21,21 @@ def __init__(self): def add_member(self, id, name): self.members[id] = name + + +subscripted_before_reference = {} +subscripted_before_reference[0] = 0 +subscripted_before_reference + + +def my_func(): + print('called') + +alias = my_func +my_list = [1, None, alias] +inception = my_list[2] + +inception() """ @@ -40,6 +55,40 @@ def test_definitions(config, workspace): ) +def test_indirect_definitions(config, workspace): + # Over 'subscripted_before_reference' + cursor_pos = {"line": 16, "character": 0} + + # The definition of 'subscripted_before_reference', + # skipping intermediate writes to the most recent definition + def_range = { + "start": {"line": 14, "character": 0}, + "end": {"line": 14, "character": len("subscripted_before_reference")}, + } + + doc = Document(DOC_URI, workspace, DOC) + assert [{"uri": DOC_URI, "range": def_range}] == pylsp_definitions( + config, doc, cursor_pos + ) + + +def test_definition_with_multihop_inference_goto(config, workspace): + # Over 'inception()' + cursor_pos = {"line": 26, "character": 0} + + # The most recent definition of 'inception', + # ignoring alias hops + def_range = { + "start": {"line": 24, "character": 0}, + "end": {"line": 24, "character": len("inception")}, + } + + doc = Document(DOC_URI, workspace, DOC) + assert [{"uri": DOC_URI, "range": def_range}] == pylsp_definitions( + config, doc, cursor_pos + ) + + def test_builtin_definition(config, workspace): # Over 'i' in dict cursor_pos = {"line": 8, "character": 24} From ccbf6cd7b32db8fd338fb6767bdb73bdfd10b4cf Mon Sep 17 00:00:00 2001 From: Stephen Macke Date: Tue, 3 Oct 2023 14:29:00 -0700 Subject: [PATCH 05/23] Fix numpy go-to-definition by taking it off autoimport list for this case (#447) --- pylsp/plugins/definition.py | 22 ++++++++++++++++------ test/plugins/test_definitions.py | 12 ++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/pylsp/plugins/definition.py b/pylsp/plugins/definition.py index ffc1b00b..53eda915 100644 --- a/pylsp/plugins/definition.py +++ b/pylsp/plugins/definition.py @@ -3,6 +3,9 @@ from __future__ import annotations import logging from typing import Any, Dict, List, TYPE_CHECKING + +import jedi + from pylsp import hookimpl, uris, _utils if TYPE_CHECKING: @@ -43,12 +46,19 @@ def pylsp_definitions( settings = config.plugin_settings("jedi_definition") code_position = _utils.position_to_jedi_linecolumn(document, position) script = document.jedi_script(use_document_path=True) - definitions = script.goto( - follow_imports=settings.get("follow_imports", True), - follow_builtin_imports=settings.get("follow_builtin_imports", True), - **code_position, - ) - definitions = [_resolve_definition(d, script, settings) for d in definitions] + auto_import_modules = jedi.settings.auto_import_modules + + try: + jedi.settings.auto_import_modules = [] + definitions = script.goto( + follow_imports=settings.get("follow_imports", True), + follow_builtin_imports=settings.get("follow_builtin_imports", True), + **code_position, + ) + definitions = [_resolve_definition(d, script, settings) for d in definitions] + finally: + jedi.settings.auto_import_modules = auto_import_modules + follow_builtin_defns = settings.get("follow_builtin_definitions", True) return [ { diff --git a/test/plugins/test_definitions.py b/test/plugins/test_definitions.py index f0e9ffef..c366e8ca 100644 --- a/test/plugins/test_definitions.py +++ b/test/plugins/test_definitions.py @@ -36,6 +36,9 @@ def my_func(): inception = my_list[2] inception() + +import numpy +numpy.ones """ @@ -89,6 +92,15 @@ def test_definition_with_multihop_inference_goto(config, workspace): ) +def test_numpy_definition(config, workspace): + # Over numpy.ones + cursor_pos = {"line": 29, "character": 8} + + doc = Document(DOC_URI, workspace, DOC) + defns = pylsp_definitions(config, doc, cursor_pos) + assert len(defns) > 0, defns + + def test_builtin_definition(config, workspace): # Over 'i' in dict cursor_pos = {"line": 8, "character": 24} From 76ea9ae702ae9c1c2a821e5ce27a81d23eb81ebe Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 4 Oct 2023 12:17:48 -0400 Subject: [PATCH 06/23] Increase minimal required version of autopep8 to `>=2.0.4,<2.1.0` (#449) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a27c1316..d2983edb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ Homepage = "https://github.com/python-lsp/python-lsp-server" [project.optional-dependencies] all = [ - "autopep8>=1.6.0,<2.1.0", + "autopep8>=2.0.4,<2.1.0", "flake8>=6.1.0,<7", "mccabe>=0.7.0,<0.8.0", "pycodestyle>=2.11.0,<2.12.0", From ecefb7318ae31451ed6d4a7f7cc55583fff7049f Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 4 Oct 2023 13:18:21 -0400 Subject: [PATCH 07/23] Fix renaming when file has no EOLs (#450) --- pylsp/plugins/jedi_rename.py | 4 +++- pylsp/plugins/rope_rename.py | 8 +++++-- test/plugins/test_jedi_rename.py | 38 +++++++++++++++++++++++++++----- test/plugins/test_rope_rename.py | 28 ++++++++++++++++++++++- 4 files changed, 68 insertions(+), 10 deletions(-) diff --git a/pylsp/plugins/jedi_rename.py b/pylsp/plugins/jedi_rename.py index 700da508..9c89c1de 100644 --- a/pylsp/plugins/jedi_rename.py +++ b/pylsp/plugins/jedi_rename.py @@ -54,4 +54,6 @@ def pylsp_rename( def _num_lines(file_contents): "Count the number of lines in the given string." - return len(file_contents.splitlines()) + if _utils.get_eol_chars(file_contents): + return len(file_contents.splitlines()) + return 0 diff --git a/pylsp/plugins/rope_rename.py b/pylsp/plugins/rope_rename.py index f59ba890..a4323a42 100644 --- a/pylsp/plugins/rope_rename.py +++ b/pylsp/plugins/rope_rename.py @@ -6,7 +6,7 @@ from rope.base import libutils from rope.refactor.rename import Rename -from pylsp import hookimpl, uris +from pylsp import hookimpl, uris, _utils log = logging.getLogger(__name__) @@ -59,4 +59,8 @@ def pylsp_rename(config, workspace, document, position, new_name): def _num_lines(resource): "Count the number of lines in a `File` resource." - return len(resource.read().splitlines()) + text = resource.read() + + if _utils.get_eol_chars(text): + return len(text.splitlines()) + return 0 diff --git a/test/plugins/test_jedi_rename.py b/test/plugins/test_jedi_rename.py index c3a1e485..d88a9297 100644 --- a/test/plugins/test_jedi_rename.py +++ b/test/plugins/test_jedi_rename.py @@ -2,16 +2,12 @@ # Copyright 2021- Python Language Server Contributors. import os -import sys import pytest from pylsp import uris from pylsp.plugins.jedi_rename import pylsp_rename from pylsp.workspace import Document -LT_PY36 = sys.version_info.major < 3 or ( - sys.version_info.major == 3 and sys.version_info.minor < 6 -) DOC_NAME = "test1.py" DOC = """class Test1(): @@ -26,13 +22,17 @@ class Test2(Test1): x = Test1() """ +DOC_NAME_SIMPLE = "test3.py" +DOC_SIMPLE = "foo = 12" + @pytest.fixture def tmp_workspace(temp_workspace_factory): - return temp_workspace_factory({DOC_NAME: DOC, DOC_NAME_EXTRA: DOC_EXTRA}) + return temp_workspace_factory( + {DOC_NAME: DOC, DOC_NAME_EXTRA: DOC_EXTRA, DOC_NAME_SIMPLE: DOC_SIMPLE} + ) -@pytest.mark.skipif(LT_PY36, reason="Jedi refactoring isnt supported on Python 2.x/3.5") def test_jedi_rename(tmp_workspace, config): # pylint: disable=redefined-outer-name # rename the `Test1` class position = {"line": 0, "character": 6} @@ -56,6 +56,7 @@ def test_jedi_rename(tmp_workspace, config): # pylint: disable=redefined-outer- "newText": "class ShouldBeRenamed():\n pass\n\nclass Test2(ShouldBeRenamed):\n pass\n", } ] + path = os.path.join(tmp_workspace.root_path, DOC_NAME_EXTRA) uri_extra = uris.from_fs_path(path) assert changes[1]["textDocument"]["uri"] == uri_extra @@ -63,6 +64,7 @@ def test_jedi_rename(tmp_workspace, config): # pylint: disable=redefined-outer- # but that do need to be renamed in the project have a `null` version # number. assert changes[1]["textDocument"]["version"] is None + expected = "from test1 import ShouldBeRenamed\nx = ShouldBeRenamed()\n" if os.name == "nt": # The .write method in the temp_workspace_factory functions writes @@ -77,3 +79,27 @@ def test_jedi_rename(tmp_workspace, config): # pylint: disable=redefined-outer- "newText": expected, } ] + + # Regression test for issue python-lsp/python-lsp-server#413 + # rename foo + position = {"line": 0, "character": 0} + DOC_URI = uris.from_fs_path(os.path.join(tmp_workspace.root_path, DOC_NAME_SIMPLE)) + doc = Document(DOC_URI, tmp_workspace) + + result = pylsp_rename(config, tmp_workspace, doc, position, "bar") + assert len(result.keys()) == 1 + + changes = result.get("documentChanges") + assert len(changes) == 1 + + assert changes[0]["textDocument"]["uri"] == doc.uri + assert changes[0]["textDocument"]["version"] == doc.version + assert changes[0].get("edits") == [ + { + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": 0, "character": 0}, + }, + "newText": "bar = 12", + } + ] diff --git a/test/plugins/test_rope_rename.py b/test/plugins/test_rope_rename.py index 285a565e..9b9039ba 100644 --- a/test/plugins/test_rope_rename.py +++ b/test/plugins/test_rope_rename.py @@ -8,6 +8,7 @@ from pylsp.plugins.rope_rename import pylsp_rename from pylsp.workspace import Document + DOC_NAME = "test1.py" DOC = """class Test1(): pass @@ -16,10 +17,13 @@ class Test2(Test1): pass """ +DOC_NAME_SIMPLE = "test2.py" +DOC_SIMPLE = "foo = 12" + @pytest.fixture def tmp_workspace(temp_workspace_factory): - return temp_workspace_factory({DOC_NAME: DOC}) + return temp_workspace_factory({DOC_NAME: DOC, DOC_NAME_SIMPLE: DOC_SIMPLE}) def test_rope_rename(tmp_workspace, config): # pylint: disable=redefined-outer-name @@ -45,3 +49,25 @@ def test_rope_rename(tmp_workspace, config): # pylint: disable=redefined-outer- "newText": "class ShouldBeRenamed():\n pass\n\nclass Test2(ShouldBeRenamed):\n pass\n", } ] + + # Regression test for issue python-lsp/python-lsp-server#413 + # rename foo + position = {"line": 0, "character": 0} + DOC_URI = uris.from_fs_path(os.path.join(tmp_workspace.root_path, DOC_NAME_SIMPLE)) + doc = Document(DOC_URI, tmp_workspace) + + result = pylsp_rename(config, tmp_workspace, doc, position, "bar") + assert len(result.keys()) == 1 + + changes = result.get("documentChanges") + assert len(changes) == 1 + + assert changes[0].get("edits") == [ + { + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": 0, "character": 0}, + }, + "newText": "bar = 12", + } + ] From 05698fa11bfc566ae7e040a2ed272247f8d406b2 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Thu, 5 Oct 2023 12:21:02 -0400 Subject: [PATCH 08/23] Update changelog for 1.8.1 (#451) --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed800436..0b0e8d1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # History of changes +## Version 1.8.1 (2023/10/05) + +### Issues Closed + +* [Issue 439](https://github.com/python-lsp/python-lsp-server/issues/439) - `includeDeclaration` is no longer respected in `textDocument/references` ([PR 440](https://github.com/python-lsp/python-lsp-server/pull/440) by [@krassowski](https://github.com/krassowski)) +* [Issue 438](https://github.com/python-lsp/python-lsp-server/issues/438) - flake8 can error out when deleting lines ([PR 441](https://github.com/python-lsp/python-lsp-server/pull/441) by [@krassowski](https://github.com/krassowski)) +* [Issue 413](https://github.com/python-lsp/python-lsp-server/issues/413) - textDocument/rename reports positions outside of the document ([PR 450](https://github.com/python-lsp/python-lsp-server/pull/450) by [@ccordoba12](https://github.com/ccordoba12)) + +In this release 3 issues were closed. + +### Pull Requests Merged + +* [PR 450](https://github.com/python-lsp/python-lsp-server/pull/450) - Fix renaming when file has no EOLs, by [@ccordoba12](https://github.com/ccordoba12) ([413](https://github.com/python-lsp/python-lsp-server/issues/413)) +* [PR 449](https://github.com/python-lsp/python-lsp-server/pull/449) - Increase minimal required version of autopep8 to `>=2.0.4,<2.1.0`, by [@ccordoba12](https://github.com/ccordoba12) +* [PR 447](https://github.com/python-lsp/python-lsp-server/pull/447) - Fix numpy go-to-definition by taking it off autoimport list for this case, by [@smacke](https://github.com/smacke) +* [PR 443](https://github.com/python-lsp/python-lsp-server/pull/443) - Allow Jedi "goto" to perform multiple hops for "go to definition", by [@smacke](https://github.com/smacke) +* [PR 441](https://github.com/python-lsp/python-lsp-server/pull/441) - Pass a single copy of the document's source around for flake8, by [@krassowski](https://github.com/krassowski) ([438](https://github.com/python-lsp/python-lsp-server/issues/438)) +* [PR 440](https://github.com/python-lsp/python-lsp-server/pull/440) - Fix `include_declaration` handling in references request, by [@krassowski](https://github.com/krassowski) ([439](https://github.com/python-lsp/python-lsp-server/issues/439)) +* [PR 436](https://github.com/python-lsp/python-lsp-server/pull/436) - Add black reformatting commit to `.git-blame-ignore-revs`, by [@krassowski](https://github.com/krassowski) + +In this release 7 pull requests were closed. + +---- + ## Version 1.8.0 (2023/09/08) ### New features From 6b975be4de2697979ee8039a59d84cfd7923c827 Mon Sep 17 00:00:00 2001 From: Stephen Macke Date: Sun, 8 Oct 2023 18:19:07 -0700 Subject: [PATCH 09/23] Fix notebook document selector not being a list in capabilities (#454) --- pylsp/python_lsp.py | 2 +- test/test_notebook_document.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index 3eeb2f7f..e2b541d5 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -295,7 +295,7 @@ def capabilities(self): "openClose": True, }, "notebookDocumentSync": { - "notebookSelector": {"cells": [{"language": "python"}]} + "notebookSelector": [{"cells": [{"language": "python"}]}] }, "workspace": { "workspaceFolders": {"supported": True, "changeNotifications": True} diff --git a/test/test_notebook_document.py b/test/test_notebook_document.py index e8e7ac75..6050b58f 100644 --- a/test/test_notebook_document.py +++ b/test/test_notebook_document.py @@ -33,7 +33,8 @@ def test_initialize(client_server_pair): }, ).result(timeout=CALL_TIMEOUT_IN_SECONDS) assert server.workspace is not None - assert "notebookDocumentSync" in response["capabilities"].keys() + selector = response["capabilities"]["notebookDocumentSync"]["notebookSelector"] + assert isinstance(selector, list) @pytest.mark.skipif(IS_WIN, reason="Flaky on Windows") From d33927208a60890f48148d2cd1c007e859e6f084 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 9 Oct 2023 10:57:39 -0400 Subject: [PATCH 10/23] Update changelog for 1.8.2 (#456) --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b0e8d1f..eb9c7957 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # History of changes +## Version 1.8.2 (2023/10/09) + +### Issues Closed + +* [Issue 453](https://github.com/python-lsp/python-lsp-server/issues/453) - notebookDocumentSync notebookSelector type error ([PR 454](https://github.com/python-lsp/python-lsp-server/pull/454) by [@smacke](https://github.com/smacke)) + +In this release 1 issue was closed. + +### Pull Requests Merged + +* [PR 454](https://github.com/python-lsp/python-lsp-server/pull/454) - Fix notebook document selector not being a list in capabilities, by [@smacke](https://github.com/smacke) ([453](https://github.com/python-lsp/python-lsp-server/issues/453)) + +In this release 1 pull request was closed. + +---- + ## Version 1.8.1 (2023/10/05) ### Issues Closed From 4f6aa2073add92720cb65aa8d5e124582eeb6fe1 Mon Sep 17 00:00:00 2001 From: Riley <44530786+staticf0x@users.noreply.github.com> Date: Wed, 11 Oct 2023 07:18:38 -0700 Subject: [PATCH 11/23] Fix missing signatures for docstrings in Markdown (#457) --- pylsp/_utils.py | 1 - test/test_utils.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/pylsp/_utils.py b/pylsp/_utils.py index 644a00e0..9d393b92 100644 --- a/pylsp/_utils.py +++ b/pylsp/_utils.py @@ -209,7 +209,6 @@ def format_docstring( if markup_kind == "markdown": try: value = docstring_to_markdown.convert(contents) - return {"kind": "markdown", "value": value} except docstring_to_markdown.UnknownFormatError: # try to escape the Markdown syntax instead: value = escape_markdown(contents) diff --git a/test/test_utils.py b/test/test_utils.py index 7d01ba01..bc2782dc 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -9,6 +9,7 @@ from unittest import mock from flaky import flaky +from docstring_to_markdown import UnknownFormatError from pylsp import _utils from pylsp.python_lsp import PythonLSPServer, start_io_lang_server @@ -154,3 +155,53 @@ def test_clip_column(): assert _utils.clip_column(2, ["123\n", "123"], 0) == 2 assert _utils.clip_column(3, ["123\n", "123"], 0) == 3 assert _utils.clip_column(4, ["123\n", "123"], 1) == 3 + + +@mock.patch("docstring_to_markdown.convert") +def test_format_docstring_valid_rst_signature(mock_convert): + """Test that a valid RST docstring includes the function signature.""" + docstring = """A function docstring. + + Parameters + ---------- + a : str, something + """ + + # Mock the return value to avoid depedency on the real thing + mock_convert.return_value = """A function docstring. + + #### Parameters + + - `a`: str, something + """ + + markdown = _utils.format_docstring( + docstring, + "markdown", + ["something(a: str) -> str"], + )["value"] + + assert markdown.startswith( + _utils.wrap_signature("something(a: str) -> str"), + ) + + +@mock.patch("docstring_to_markdown.convert", side_effect=UnknownFormatError) +def test_format_docstring_invalid_rst_signature(_): + """Test that an invalid RST docstring includes the function signature.""" + docstring = """A function docstring. + + Parameters + ---------- + a : str, something + """ + + markdown = _utils.format_docstring( + docstring, + "markdown", + ["something(a: str) -> str"], + )["value"] + + assert markdown.startswith( + _utils.wrap_signature("something(a: str) -> str"), + ) From 6ff9aa90ee1a6e14d58958af93d0794c0d0de099 Mon Sep 17 00:00:00 2001 From: tkrabel-db <91616041+tkrabel-db@users.noreply.github.com> Date: Sat, 14 Oct 2023 00:25:06 +0200 Subject: [PATCH 12/23] Load `rope_autoimport` cache on `workspace/didChangeConfiguration` (#461) --- pylsp/hookspecs.py | 5 +++++ pylsp/plugins/rope_autoimport.py | 12 ++++++++++++ pylsp/python_lsp.py | 1 + 3 files changed, 18 insertions(+) diff --git a/pylsp/hookspecs.py b/pylsp/hookspecs.py index d1a2458e..d732b1d0 100644 --- a/pylsp/hookspecs.py +++ b/pylsp/hookspecs.py @@ -127,3 +127,8 @@ def pylsp_settings(config): @hookspec(firstresult=True) def pylsp_signature_help(config, workspace, document, position): pass + + +@hookspec +def pylsp_workspace_configuration_changed(config, workspace): + pass diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index be40fe41..c13907a4 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -238,3 +238,15 @@ def pylsp_document_did_open(config: Config, workspace: Workspace): def pylsp_document_did_save(config: Config, workspace: Workspace, document: Document): """Update the names associated with this document.""" _reload_cache(config, workspace, [document]) + + +@hookimpl +def pylsp_workspace_configuration_chaged(config: Config, workspace: Workspace): + """ + Initialize autoimport if it has been enabled through a + workspace/didChangeConfiguration message from the frontend. + + Generates the cache for local and global items. + """ + if config.plugin_settings("rope_autoimport").get("enabled", False): + _reload_cache(config, workspace) diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index e2b541d5..4c3ea0d2 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -766,6 +766,7 @@ def m_workspace__did_change_configuration(self, settings=None): self.config.update((settings or {}).get("pylsp", {})) for workspace in self.workspaces.values(): workspace.update_config(settings) + self._hook("pylsp_workspace_configuration_changed") for doc_uri in workspace.documents: self.lint(doc_uri, is_saved=False) From 681d81e65c563fb9dbb7e1e4b1101a0be6517504 Mon Sep 17 00:00:00 2001 From: tkrabel-db <91616041+tkrabel-db@users.noreply.github.com> Date: Wed, 18 Oct 2023 06:43:19 +0200 Subject: [PATCH 13/23] Make workspace/didChangeConfig work with notebook documents (#462) --- pylsp/workspace.py | 6 +++- test/test_notebook_document.py | 57 ++++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/pylsp/workspace.py b/pylsp/workspace.py index 27af5f83..e0e16ef9 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -167,7 +167,11 @@ def update_document(self, doc_uri, change, version=None): def update_config(self, settings): self._config.update((settings or {}).get("pylsp", {})) for doc_uri in self.documents: - self.get_document(doc_uri).update_config(settings) + if isinstance(document := self.get_document(doc_uri), Notebook): + # Notebook documents don't have a config. The config is + # handled at the cell level. + return + document.update_config(settings) def apply_edit(self, edit): return self._endpoint.request(self.M_APPLY_EDIT, {"edit": edit}) diff --git a/test/test_notebook_document.py b/test/test_notebook_document.py index 6050b58f..15f187d3 100644 --- a/test/test_notebook_document.py +++ b/test/test_notebook_document.py @@ -2,11 +2,11 @@ import os import time -from unittest.mock import patch, call +from unittest.mock import patch, call from test.fixtures import CALL_TIMEOUT_IN_SECONDS - import pytest +from pylsp.workspace import Notebook from pylsp import IS_WIN from pylsp.lsp import NotebookCellKind @@ -37,6 +37,59 @@ def test_initialize(client_server_pair): assert isinstance(selector, list) +@pytest.mark.skipif(IS_WIN, reason="Flaky on Windows") +def test_workspace_did_change_configuration(client_server_pair): + """Test that we can update a workspace config w/o error when a notebook is open.""" + client, server = client_server_pair + client._endpoint.request( + "initialize", + { + "processId": 1234, + "rootPath": os.path.dirname(__file__), + }, + ).result(timeout=CALL_TIMEOUT_IN_SECONDS) + assert server.workspace is not None + + with patch.object(server._endpoint, "notify") as mock_notify: + client._endpoint.notify( + "notebookDocument/didOpen", + { + "notebookDocument": { + "uri": "notebook_uri", + "notebookType": "jupyter-notebook", + "cells": [ + { + "kind": NotebookCellKind.Code, + "document": "cell_1_uri", + }, + ], + }, + "cellTextDocuments": [ + { + "uri": "cell_1_uri", + "languageId": "python", + "text": "", + }, + ], + }, + ) + wait_for_condition(lambda: mock_notify.call_count >= 1) + assert isinstance(server.workspace.get_document("notebook_uri"), Notebook) + assert len(server.workspace.documents) == 2 + + server.workspace.update_config( + {"pylsp": {"plugins": {"flake8": {"enabled": True}}}} + ) + + assert server.config.plugin_settings("flake8").get("enabled") is True + assert ( + server.workspace.get_document("cell_1_uri") + ._config.plugin_settings("flake8") + .get("enabled") + is True + ) + + @pytest.mark.skipif(IS_WIN, reason="Flaky on Windows") def test_notebook_document__did_open( client_server_pair, From 2f2c0e8f62e20dca8fd5d5772ee8bc27518284eb Mon Sep 17 00:00:00 2001 From: tkrabel-db <91616041+tkrabel-db@users.noreply.github.com> Date: Wed, 18 Oct 2023 06:46:11 +0200 Subject: [PATCH 14/23] Minor bug fix in Rope autoimport plugin (#464) --- pylsp/plugins/rope_autoimport.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index c13907a4..6c4784aa 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -241,7 +241,7 @@ def pylsp_document_did_save(config: Config, workspace: Workspace, document: Docu @hookimpl -def pylsp_workspace_configuration_chaged(config: Config, workspace: Workspace): +def pylsp_workspace_configuration_changed(config: Config, workspace: Workspace): """ Initialize autoimport if it has been enabled through a workspace/didChangeConfiguration message from the frontend. @@ -250,3 +250,5 @@ def pylsp_workspace_configuration_chaged(config: Config, workspace: Workspace): """ if config.plugin_settings("rope_autoimport").get("enabled", False): _reload_cache(config, workspace) + else: + log.debug("autoimport: Skipping cache reload.") From b864c4fccd1ad4bb4f125e321c5ac03c6c21f044 Mon Sep 17 00:00:00 2001 From: tkrabel-db <91616041+tkrabel-db@users.noreply.github.com> Date: Thu, 19 Oct 2023 02:24:24 +0200 Subject: [PATCH 15/23] Ignore notebook names on cell completion for autoimport (#466) --- pylsp/hookspecs.py | 2 +- pylsp/plugins/rope_autoimport.py | 12 +- pylsp/python_lsp.py | 11 +- pylsp/workspace.py | 26 ++++ test/fixtures.py | 20 ++- test/plugins/test_autoimport.py | 130 ++++++++++++------ test/test_language_server.py | 11 +- test/test_notebook_document.py | 220 +++---------------------------- test/test_utils.py | 57 ++++++++ 9 files changed, 228 insertions(+), 261 deletions(-) diff --git a/pylsp/hookspecs.py b/pylsp/hookspecs.py index d732b1d0..9c9cf387 100644 --- a/pylsp/hookspecs.py +++ b/pylsp/hookspecs.py @@ -25,7 +25,7 @@ def pylsp_commands(config, workspace): @hookspec -def pylsp_completions(config, workspace, document, position): +def pylsp_completions(config, workspace, document, position, ignored_names): pass diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index 6c4784aa..1caab35d 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -1,7 +1,7 @@ # Copyright 2022- Python Language Server Contributors. import logging -from typing import Any, Dict, Generator, List, Optional, Set +from typing import Any, Dict, Generator, List, Optional, Set, Union import parso from jedi import Script @@ -153,7 +153,11 @@ def get_names(script: Script) -> Set[str]: @hookimpl def pylsp_completions( - config: Config, workspace: Workspace, document: Document, position + config: Config, + workspace: Workspace, + document: Document, + position, + ignored_names: Union[Set[str], None], ): """Get autoimport suggestions.""" line = document.lines[position["line"]] @@ -164,7 +168,9 @@ def pylsp_completions( word = word_node.value log.debug(f"autoimport: searching for word: {word}") rope_config = config.settings(document_path=document.path).get("rope", {}) - ignored_names: Set[str] = get_names(document.jedi_script(use_document_path=True)) + ignored_names: Set[str] = ignored_names or get_names( + document.jedi_script(use_document_path=True) + ) autoimport = workspace._rope_autoimport(rope_config) suggestions = list(autoimport.search_full(word, ignored_names=ignored_names)) results = list( diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index 4c3ea0d2..760ad974 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -394,7 +394,16 @@ def code_lens(self, doc_uri): return flatten(self._hook("pylsp_code_lens", doc_uri)) def completions(self, doc_uri, position): - completions = self._hook("pylsp_completions", doc_uri, position=position) + workspace = self._match_uri_to_workspace(doc_uri) + document = workspace.get_document(doc_uri) + ignored_names = None + if isinstance(document, Cell): + # We need to get the ignored names from the whole notebook document + notebook_document = workspace.get_maybe_document(document.notebook_uri) + ignored_names = notebook_document.jedi_names(doc_uri) + completions = self._hook( + "pylsp_completions", doc_uri, position=position, ignored_names=ignored_names + ) return {"isIncomplete": False, "items": flatten(completions)} def completion_item_resolve(self, completion_item): diff --git a/pylsp/workspace.py b/pylsp/workspace.py index e0e16ef9..fb524c71 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -595,6 +595,7 @@ def __init__( self.version = version self.cells = cells or [] self.metadata = metadata or {} + self._lock = RLock() def __str__(self): return "Notebook with URI '%s'" % str(self.uri) @@ -625,6 +626,31 @@ def cell_data(self): offset += num_lines return cell_data + @lock + def jedi_names( + self, + up_to_cell_uri: Optional[str] = None, + all_scopes=False, + definitions=True, + references=False, + ): + """ + Get the names in the notebook up to a certain cell. + + Parameters + ---------- + up_to_cell_uri: str, optional + The cell uri to stop at. If None, all cells are considered. + """ + names = set() + for cell in self.cells: + cell_uri = cell["document"] + cell_document = self.workspace.get_cell_document(cell_uri) + names.update(cell_document.jedi_names(all_scopes, definitions, references)) + if cell_uri == up_to_cell_uri: + break + return set(name.name for name in names) + class Cell(Document): """ diff --git a/test/fixtures.py b/test/fixtures.py index 03d0f824..ed6206af 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -5,9 +5,11 @@ from io import StringIO from unittest.mock import MagicMock -from test.test_utils import ClientServerPair +from test.test_utils import ClientServerPair, CALL_TIMEOUT_IN_SECONDS import pytest +import pylsp_jsonrpc + from pylsp_jsonrpc.dispatchers import MethodDispatcher from pylsp_jsonrpc.endpoint import Endpoint from pylsp_jsonrpc.exceptions import JsonRpcException @@ -24,7 +26,6 @@ def main(): print sys.stdin.read() """ -CALL_TIMEOUT_IN_SECONDS = 30 class FakeEditorMethodsMixin: @@ -175,8 +176,13 @@ def client_server_pair(): yield (client_server_pair_obj.client, client_server_pair_obj.server) - shutdown_response = client_server_pair_obj.client._endpoint.request( - "shutdown" - ).result(timeout=CALL_TIMEOUT_IN_SECONDS) - assert shutdown_response is None - client_server_pair_obj.client._endpoint.notify("exit") + try: + shutdown_response = client_server_pair_obj.client._endpoint.request( + "shutdown" + ).result(timeout=CALL_TIMEOUT_IN_SECONDS) + assert shutdown_response is None + client_server_pair_obj.client._endpoint.notify("exit") + except pylsp_jsonrpc.exceptions.JsonRpcInvalidParams: + # SQLite objects created in a thread can only be used in that same thread. + # This exeception is raised when testing rope autoimport. + client_server_pair_obj.client._endpoint.notify("exit") diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index dbb6f7a4..b1c46775 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -1,13 +1,16 @@ # Copyright 2022- Python Language Server Contributors. -from typing import Dict, List -from unittest.mock import Mock +from typing import Any, Dict, List +from unittest.mock import Mock, patch + +from test.test_notebook_document import wait_for_condition +from test.test_utils import send_initialize_request, send_notebook_did_open import jedi import parso import pytest -from pylsp import lsp, uris +from pylsp import IS_WIN, lsp, uris from pylsp.config.config import Config from pylsp.plugins.rope_autoimport import _get_score, _should_insert, get_names from pylsp.plugins.rope_autoimport import ( @@ -16,9 +19,17 @@ from pylsp.plugins.rope_autoimport import pylsp_initialize from pylsp.workspace import Workspace + DOC_URI = uris.from_fs_path(__file__) +def contains_autoimport(suggestion: Dict[str, Any], module: str) -> bool: + """Checks if `suggestion` contains an autoimport for `module`.""" + return suggestion.get("label", "") == module and "import" in suggestion.get( + "detail", "" + ) + + @pytest.fixture(scope="session") def autoimport_workspace(tmp_path_factory) -> Workspace: "Special autoimport workspace. Persists across sessions to make in-memory sqlite3 database fast." @@ -39,7 +50,9 @@ def completions(config: Config, autoimport_workspace: Workspace, request): com_position = {"line": 0, "character": position} autoimport_workspace.put_document(DOC_URI, source=document) doc = autoimport_workspace.get_document(DOC_URI) - yield pylsp_autoimport_completions(config, autoimport_workspace, doc, com_position) + yield pylsp_autoimport_completions( + config, autoimport_workspace, doc, com_position, None + ) autoimport_workspace.rm_document(DOC_URI) @@ -141,45 +154,13 @@ def test_autoimport_defined_name(config, workspace): com_position = {"line": 1, "character": 3} workspace.put_document(DOC_URI, source=document) doc = workspace.get_document(DOC_URI) - completions = pylsp_autoimport_completions(config, workspace, doc, com_position) + completions = pylsp_autoimport_completions( + config, workspace, doc, com_position, None + ) workspace.rm_document(DOC_URI) assert not check_dict({"label": "List"}, completions) -# This test has several large issues. -# 1. autoimport relies on its sources being written to disk. This makes testing harder -# 2. the hook doesn't handle removed files -# 3. The testing framework cannot access the actual autoimport object so it cannot clear the cache -# def test_autoimport_update_module(config: Config, workspace: Workspace): -# document2 = "SomethingYouShouldntWrite = 1" -# document = """SomethingYouShouldntWrit""" -# com_position = { -# "line": 0, -# "character": 3, -# } -# doc2_path = workspace.root_path + "/test_file_no_one_should_write_to.py" -# if os.path.exists(doc2_path): -# os.remove(doc2_path) -# DOC2_URI = uris.from_fs_path(doc2_path) -# workspace.put_document(DOC_URI, source=document) -# doc = workspace.get_document(DOC_URI) -# completions = pylsp_autoimport_completions(config, workspace, doc, com_position) -# assert len(completions) == 0 -# with open(doc2_path, "w") as f: -# f.write(document2) -# workspace.put_document(DOC2_URI, source=document2) -# doc2 = workspace.get_document(DOC2_URI) -# pylsp_document_did_save(config, workspace, doc2) -# assert check_dict({"label": "SomethingYouShouldntWrite"}, completions) -# workspace.put_document(DOC2_URI, source="\n") -# doc2 = workspace.get_document(DOC2_URI) -# os.remove(doc2_path) -# pylsp_document_did_save(config, workspace, doc2) -# completions = pylsp_autoimport_completions(config, workspace, doc, com_position) -# assert len(completions) == 0 -# workspace.rm_document(DOC_URI) - - class TestShouldInsert: def test_dot(self): assert not should_insert("""str.""", 4) @@ -233,3 +214,74 @@ class sfa: """ results = get_names(jedi.Script(code=source)) assert results == set(["blah", "bleh", "e", "hello", "someone", "sfa", "a", "b"]) + + +@pytest.mark.skipif(IS_WIN, reason="Flaky on Windows") +def test_autoimport_for_notebook_document( + client_server_pair, +): + client, server = client_server_pair + send_initialize_request(client) + + with patch.object(server._endpoint, "notify") as mock_notify: + # Expectations: + # 1. We receive an autoimport suggestion for "os" in the first cell because + # os is imported after that. + # 2. We don't receive an autoimport suggestion for "os" in the second cell because it's + # already imported in the second cell. + # 3. We don't receive an autoimport suggestion for "os" in the third cell because it's + # already imported in the second cell. + # 4. We receive an autoimport suggestion for "sys" because it's not already imported + send_notebook_did_open(client, ["os", "import os\nos", "os", "sys"]) + wait_for_condition(lambda: mock_notify.call_count >= 3) + + server.m_workspace__did_change_configuration( + settings={ + "pylsp": {"plugins": {"rope_autoimport": {"enabled": True, "memory": True}}} + } + ) + rope_autoimport_settings = server.workspace._config.plugin_settings( + "rope_autoimport" + ) + assert rope_autoimport_settings.get("enabled", False) is True + assert rope_autoimport_settings.get("memory", False) is True + + # 1. + suggestions = server.completions("cell_1_uri", {"line": 0, "character": 2}).get( + "items" + ) + assert any( + suggestion + for suggestion in suggestions + if contains_autoimport(suggestion, "os") + ) + + # 2. + suggestions = server.completions("cell_2_uri", {"line": 1, "character": 2}).get( + "items" + ) + assert not any( + suggestion + for suggestion in suggestions + if contains_autoimport(suggestion, "os") + ) + + # 3. + suggestions = server.completions("cell_3_uri", {"line": 0, "character": 2}).get( + "items" + ) + assert not any( + suggestion + for suggestion in suggestions + if contains_autoimport(suggestion, "os") + ) + + # 4. + suggestions = server.completions("cell_4_uri", {"line": 0, "character": 3}).get( + "items" + ) + assert any( + suggestion + for suggestion in suggestions + if contains_autoimport(suggestion, "sys") + ) diff --git a/test/test_language_server.py b/test/test_language_server.py index 280a62fa..401b1ceb 100644 --- a/test/test_language_server.py +++ b/test/test_language_server.py @@ -5,7 +5,7 @@ import time import sys -from test.test_utils import ClientServerPair +from test.test_utils import ClientServerPair, send_initialize_request from flaky import flaky from pylsp_jsonrpc.exceptions import JsonRpcMethodNotFound @@ -73,14 +73,7 @@ def test_not_exit_without_check_parent_process_flag( client_server_pair, ): client, _ = client_server_pair - response = client._endpoint.request( - "initialize", - { - "processId": 1234, - "rootPath": os.path.dirname(__file__), - "initializationOptions": {}, - }, - ).result(timeout=CALL_TIMEOUT_IN_SECONDS) + response = send_initialize_request(client) assert "capabilities" in response diff --git a/test/test_notebook_document.py b/test/test_notebook_document.py index 15f187d3..c63d2791 100644 --- a/test/test_notebook_document.py +++ b/test/test_notebook_document.py @@ -1,10 +1,14 @@ # Copyright 2021- Python Language Server Contributors. -import os import time - from unittest.mock import patch, call -from test.fixtures import CALL_TIMEOUT_IN_SECONDS + +from test.test_utils import ( + CALL_TIMEOUT_IN_SECONDS, + send_initialize_request, + send_notebook_did_open, +) + import pytest from pylsp.workspace import Notebook @@ -24,14 +28,7 @@ def wait_for_condition(condition, timeout=CALL_TIMEOUT_IN_SECONDS): @pytest.mark.skipif(IS_WIN, reason="Flaky on Windows") def test_initialize(client_server_pair): client, server = client_server_pair - response = client._endpoint.request( - "initialize", - { - "processId": 1234, - "rootPath": os.path.dirname(__file__), - "initializationOptions": {}, - }, - ).result(timeout=CALL_TIMEOUT_IN_SECONDS) + response = send_initialize_request(client) assert server.workspace is not None selector = response["capabilities"]["notebookDocumentSync"]["notebookSelector"] assert isinstance(selector, list) @@ -41,13 +38,7 @@ def test_initialize(client_server_pair): def test_workspace_did_change_configuration(client_server_pair): """Test that we can update a workspace config w/o error when a notebook is open.""" client, server = client_server_pair - client._endpoint.request( - "initialize", - { - "processId": 1234, - "rootPath": os.path.dirname(__file__), - }, - ).result(timeout=CALL_TIMEOUT_IN_SECONDS) + send_initialize_request(client) assert server.workspace is not None with patch.object(server._endpoint, "notify") as mock_notify: @@ -95,74 +86,12 @@ def test_notebook_document__did_open( client_server_pair, ): client, server = client_server_pair - client._endpoint.request( - "initialize", - { - "processId": 1234, - "rootPath": os.path.dirname(__file__), - "initializationOptions": {}, - }, - ).result(timeout=CALL_TIMEOUT_IN_SECONDS) + send_initialize_request(client) with patch.object(server._endpoint, "notify") as mock_notify: - client._endpoint.notify( - "notebookDocument/didOpen", - { - "notebookDocument": { - "uri": "notebook_uri", - "notebookType": "jupyter-notebook", - "cells": [ - { - "kind": NotebookCellKind.Code, - "document": "cell_1_uri", - }, - { - "kind": NotebookCellKind.Code, - "document": "cell_2_uri", - }, - { - "kind": NotebookCellKind.Code, - "document": "cell_3_uri", - }, - { - "kind": NotebookCellKind.Code, - "document": "cell_4_uri", - }, - { - "kind": NotebookCellKind.Code, - "document": "cell_5_uri", - }, - ], - }, - # Test as many edge cases as possible for the diagnostics message - "cellTextDocuments": [ - { - "uri": "cell_1_uri", - "languageId": "python", - "text": "", - }, - { - "uri": "cell_2_uri", - "languageId": "python", - "text": "\n", - }, - { - "uri": "cell_3_uri", - "languageId": "python", - "text": "\nimport sys\n\nabc\n\n", - }, - { - "uri": "cell_4_uri", - "languageId": "python", - "text": "x", - }, - { - "uri": "cell_5_uri", - "languageId": "python", - "text": "y\n", - }, - ], - }, + # Test as many edge cases as possible for the diagnostics messages + send_notebook_did_open( + client, ["", "\n", "\nimport sys\n\nabc\n\n", "x", "y\n"] ) wait_for_condition(lambda: mock_notify.call_count >= 5) expected_call_args = [ @@ -259,48 +188,11 @@ def test_notebook_document__did_change( client_server_pair, ): client, server = client_server_pair - client._endpoint.request( - "initialize", - { - "processId": 1234, - "rootPath": os.path.dirname(__file__), - "initializationOptions": {}, - }, - ).result(timeout=CALL_TIMEOUT_IN_SECONDS) + send_initialize_request(client) # Open notebook with patch.object(server._endpoint, "notify") as mock_notify: - client._endpoint.notify( - "notebookDocument/didOpen", - { - "notebookDocument": { - "uri": "notebook_uri", - "notebookType": "jupyter-notebook", - "cells": [ - { - "kind": NotebookCellKind.Code, - "document": "cell_1_uri", - }, - { - "kind": NotebookCellKind.Code, - "document": "cell_2_uri", - }, - ], - }, - "cellTextDocuments": [ - { - "uri": "cell_1_uri", - "languageId": "python", - "text": "import sys", - }, - { - "uri": "cell_2_uri", - "languageId": "python", - "text": "", - }, - ], - }, - ) + send_notebook_did_open(client, ["import sys", ""]) wait_for_condition(lambda: mock_notify.call_count >= 2) assert len(server.workspace.documents) == 3 for uri in ["cell_1_uri", "cell_2_uri", "notebook_uri"]: @@ -531,48 +423,11 @@ def test_notebook__did_close( client_server_pair, ): client, server = client_server_pair - client._endpoint.request( - "initialize", - { - "processId": 1234, - "rootPath": os.path.dirname(__file__), - "initializationOptions": {}, - }, - ).result(timeout=CALL_TIMEOUT_IN_SECONDS) + send_initialize_request(client) # Open notebook with patch.object(server._endpoint, "notify") as mock_notify: - client._endpoint.notify( - "notebookDocument/didOpen", - { - "notebookDocument": { - "uri": "notebook_uri", - "notebookType": "jupyter-notebook", - "cells": [ - { - "kind": NotebookCellKind.Code, - "document": "cell_1_uri", - }, - { - "kind": NotebookCellKind.Code, - "document": "cell_2_uri", - }, - ], - }, - "cellTextDocuments": [ - { - "uri": "cell_1_uri", - "languageId": "python", - "text": "import sys", - }, - { - "uri": "cell_2_uri", - "languageId": "python", - "text": "", - }, - ], - }, - ) + send_notebook_did_open(client, ["import sys", ""]) wait_for_condition(lambda: mock_notify.call_count >= 2) assert len(server.workspace.documents) == 3 for uri in ["cell_1_uri", "cell_2_uri", "notebook_uri"]: @@ -603,48 +458,11 @@ def test_notebook__did_close( @pytest.mark.skipif(IS_WIN, reason="Flaky on Windows") def test_notebook_definition(client_server_pair): client, server = client_server_pair - client._endpoint.request( - "initialize", - { - "processId": 1234, - "rootPath": os.path.dirname(__file__), - "initializationOptions": {}, - }, - ).result(timeout=CALL_TIMEOUT_IN_SECONDS) + send_initialize_request(client) # Open notebook with patch.object(server._endpoint, "notify") as mock_notify: - client._endpoint.notify( - "notebookDocument/didOpen", - { - "notebookDocument": { - "uri": "notebook_uri", - "notebookType": "jupyter-notebook", - "cells": [ - { - "kind": NotebookCellKind.Code, - "document": "cell_1_uri", - }, - { - "kind": NotebookCellKind.Code, - "document": "cell_2_uri", - }, - ], - }, - "cellTextDocuments": [ - { - "uri": "cell_1_uri", - "languageId": "python", - "text": "y=2\nx=1", - }, - { - "uri": "cell_2_uri", - "languageId": "python", - "text": "x", - }, - ], - }, - ) + send_notebook_did_open(client, ["y=2\nx=1", "x"]) # wait for expected diagnostics messages wait_for_condition(lambda: mock_notify.call_count >= 2) assert len(server.workspace.documents) == 3 diff --git a/test/test_utils.py b/test/test_utils.py index bc2782dc..fb4a8b81 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -6,15 +6,72 @@ import sys from threading import Thread import time +from typing import List from unittest import mock from flaky import flaky from docstring_to_markdown import UnknownFormatError from pylsp import _utils +from pylsp.lsp import NotebookCellKind from pylsp.python_lsp import PythonLSPServer, start_io_lang_server +CALL_TIMEOUT_IN_SECONDS = 30 + + +def send_notebook_did_open(client, cells: List[str]): + """ + Sends a notebookDocument/didOpen notification with the given python cells. + + The notebook has the uri "notebook_uri" and the cells have the uris + "cell_1_uri", "cell_2_uri", etc. + """ + client._endpoint.notify( + "notebookDocument/didOpen", notebook_with_python_cells(cells) + ) + + +def notebook_with_python_cells(cells: List[str]): + """ + Create a notebook document with the given python cells. + + The notebook has the uri "notebook_uri" and the cells have the uris + "cell_1_uri", "cell_2_uri", etc. + """ + return { + "notebookDocument": { + "uri": "notebook_uri", + "notebookType": "jupyter-notebook", + "cells": [ + { + "kind": NotebookCellKind.Code, + "document": f"cell_{i+1}_uri", + } + for i in range(len(cells)) + ], + }, + "cellTextDocuments": [ + { + "uri": f"cell_{i+1}_uri", + "languageId": "python", + "text": cell, + } + for i, cell in enumerate(cells) + ], + } + + +def send_initialize_request(client): + return client._endpoint.request( + "initialize", + { + "processId": 1234, + "rootPath": os.path.dirname(__file__), + }, + ).result(timeout=CALL_TIMEOUT_IN_SECONDS) + + def start(obj): obj.start() From 1f415b5cb9b984dab5a8f2ecdc90f9da72663928 Mon Sep 17 00:00:00 2001 From: tkrabel-db <91616041+tkrabel-db@users.noreply.github.com> Date: Sat, 21 Oct 2023 00:17:30 +0200 Subject: [PATCH 16/23] Support `initializationOptions` to configure the server (#459) --- pylsp/config/config.py | 4 +++ test/test_configuration.py | 53 ++++++++++++++++++++++++++++++++++++++ test/test_utils.py | 5 ++-- 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 test/test_configuration.py diff --git a/pylsp/config/config.py b/pylsp/config/config.py index b458069b..454ee4b3 100644 --- a/pylsp/config/config.py +++ b/pylsp/config/config.py @@ -98,6 +98,10 @@ def __init__(self, root_uri, init_opts, process_id, capabilities): self._plugin_settings, plugin_conf ) + self._plugin_settings = _utils.merge_dicts( + self._plugin_settings, self._init_opts.get("pylsp", {}) + ) + self._update_disabled_plugins() @property diff --git a/test/test_configuration.py b/test/test_configuration.py new file mode 100644 index 00000000..91da4212 --- /dev/null +++ b/test/test_configuration.py @@ -0,0 +1,53 @@ +# Copyright 2021- Python Language Server Contributors. + +from unittest.mock import patch + +from test.test_utils import send_initialize_request +from test.test_notebook_document import wait_for_condition + +import pytest + +from pylsp import IS_WIN + + +INITIALIZATION_OPTIONS = { + "pylsp": { + "plugins": { + "flake8": {"enabled": True}, + "pycodestyle": {"enabled": False}, + "pyflakes": {"enabled": False}, + }, + } +} + + +@pytest.mark.skipif(IS_WIN, reason="Flaky on Windows") +def test_set_flake8_using_init_opts(client_server_pair): + client, server = client_server_pair + send_initialize_request(client, INITIALIZATION_OPTIONS) + for key, value in INITIALIZATION_OPTIONS["pylsp"]["plugins"].items(): + assert server.workspace._config.settings().get("plugins").get(key).get( + "enabled" + ) == value.get("enabled") + + +@pytest.mark.skipif(IS_WIN, reason="Flaky on Windows") +def test_set_flake8_using_workspace_did_change_configuration(client_server_pair): + client, server = client_server_pair + send_initialize_request(client, None) + assert ( + server.workspace._config.settings().get("plugins").get("flake8").get("enabled") + is False + ) + + with patch.object(server.workspace, "update_config") as mock_update_config: + client._endpoint.notify( + "workspace/didChangeConfiguration", + {"settings": INITIALIZATION_OPTIONS}, + ) + wait_for_condition(lambda: mock_update_config.call_count >= 1) + + for key, value in INITIALIZATION_OPTIONS["pylsp"]["plugins"].items(): + assert server.workspace._config.settings().get("plugins").get(key).get( + "enabled" + ) == value.get("enabled") diff --git a/test/test_utils.py b/test/test_utils.py index fb4a8b81..8b518d72 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -6,7 +6,7 @@ import sys from threading import Thread import time -from typing import List +from typing import Any, Dict, List from unittest import mock from flaky import flaky @@ -62,12 +62,13 @@ def notebook_with_python_cells(cells: List[str]): } -def send_initialize_request(client): +def send_initialize_request(client, initialization_options: Dict[str, Any] = None): return client._endpoint.request( "initialize", { "processId": 1234, "rootPath": os.path.dirname(__file__), + "initializationOptions": initialization_options, }, ).result(timeout=CALL_TIMEOUT_IN_SECONDS) From 728929c4ce8e9b280e12b01b3d7765bd679caf49 Mon Sep 17 00:00:00 2001 From: UnkwUsr Date: Sun, 22 Oct 2023 00:38:39 +0300 Subject: [PATCH 17/23] Pass argument `extendIgnore` to flake8 (#469) --- CONFIGURATION.md | 1 + pylsp/config/flake8_conf.py | 1 + pylsp/config/schema.json | 8 ++++++++ pylsp/plugins/flake8_lint.py | 1 + 4 files changed, 11 insertions(+) diff --git a/CONFIGURATION.md b/CONFIGURATION.md index f2626e44..f88e425c 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -8,6 +8,7 @@ This server can be configured using the `workspace/didChangeConfiguration` metho | `pylsp.plugins.flake8.config` | `string` | Path to the config file that will be the authoritative config source. | `null` | | `pylsp.plugins.flake8.enabled` | `boolean` | Enable or disable the plugin. | `false` | | `pylsp.plugins.flake8.exclude` | `array` of `string` items | List of files or directories to exclude. | `[]` | +| `pylsp.plugins.flake8.extendIgnore` | `array` of `string` items | List of errors and warnings to append to ignore list. | `[]` | | `pylsp.plugins.flake8.executable` | `string` | Path to the flake8 executable. | `"flake8"` | | `pylsp.plugins.flake8.filename` | `string` | Only check for filenames matching the patterns in this list. | `null` | | `pylsp.plugins.flake8.hangClosing` | `boolean` | Hang closing bracket instead of matching indentation of opening bracket's line. | `null` | diff --git a/pylsp/config/flake8_conf.py b/pylsp/config/flake8_conf.py index 485945df..ca3b199c 100644 --- a/pylsp/config/flake8_conf.py +++ b/pylsp/config/flake8_conf.py @@ -24,6 +24,7 @@ ("select", "plugins.pycodestyle.select", list), # flake8 ("exclude", "plugins.flake8.exclude", list), + ("extend-ignore", "plugins.flake8.extendIgnore", list), ("filename", "plugins.flake8.filename", list), ("hang-closing", "plugins.flake8.hangClosing", bool), ("ignore", "plugins.flake8.ignore", list), diff --git a/pylsp/config/schema.json b/pylsp/config/schema.json index 4ac085d0..fbf7f014 100644 --- a/pylsp/config/schema.json +++ b/pylsp/config/schema.json @@ -37,6 +37,14 @@ }, "description": "List of files or directories to exclude." }, + "pylsp.plugins.flake8.extendIgnore": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "List of errors and warnings to append to ignore list." + }, "pylsp.plugins.flake8.executable": { "type": "string", "default": "flake8", diff --git a/pylsp/plugins/flake8_lint.py b/pylsp/plugins/flake8_lint.py index d31783bf..8d8d4c5f 100644 --- a/pylsp/plugins/flake8_lint.py +++ b/pylsp/plugins/flake8_lint.py @@ -58,6 +58,7 @@ def pylsp_lint(workspace, document): opts = { "config": settings.get("config"), "exclude": settings.get("exclude"), + "extend-ignore": settings.get("extendIgnore"), "filename": settings.get("filename"), "hang-closing": settings.get("hangClosing"), "ignore": ignores or None, From 2a5a9539c94f0a8caa9beef2bd1abd1500e22303 Mon Sep 17 00:00:00 2001 From: tkrabel-db <91616041+tkrabel-db@users.noreply.github.com> Date: Sat, 28 Oct 2023 12:53:57 +0200 Subject: [PATCH 18/23] Add code completions to `rope_autoimport` plugin (#471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Carlos Cordoba Co-authored-by: Michał Krassowski <5832902+krassowski@users.noreply.github.com> --- CONFIGURATION.md | 4 +- docs/autoimport.md | 1 + pylsp/config/schema.json | 130 +++++++++++++++++---- pylsp/plugins/rope_autoimport.py | 139 +++++++++++++++++++--- pylsp/python_lsp.py | 2 +- pylsp/workspace.py | 30 +++-- test/fixtures.py | 16 +-- test/plugins/test_autoimport.py | 194 +++++++++++++++++++------------ 8 files changed, 381 insertions(+), 135 deletions(-) diff --git a/CONFIGURATION.md b/CONFIGURATION.md index f88e425c..acf8a85f 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -64,7 +64,9 @@ This server can be configured using the `workspace/didChangeConfiguration` metho | `pylsp.plugins.pylint.enabled` | `boolean` | Enable or disable the plugin. | `false` | | `pylsp.plugins.pylint.args` | `array` of non-unique `string` items | Arguments to pass to pylint. | `[]` | | `pylsp.plugins.pylint.executable` | `string` | Executable to run pylint with. Enabling this will run pylint on unsaved files via stdin. Can slow down workflow. Only works with python3. | `null` | -| `pylsp.plugins.rope_autoimport.enabled` | `boolean` | Enable or disable autoimport. | `false` | +| `pylsp.plugins.rope_autoimport.enabled` | `boolean` | Enable or disable autoimport. If false, neither completions nor code actions are enabled. If true, the respective features can be enabled or disabled individually. | `false` | +| `pylsp.plugins.rope_autoimport.completions.enabled` | `boolean` | Enable or disable autoimport completions. | `true` | +| `pylsp.plugins.rope_autoimport.code_actions.enabled` | `boolean` | Enable or disable autoimport code actions (e.g. for quick fixes). | `true` | | `pylsp.plugins.rope_autoimport.memory` | `boolean` | Make the autoimport database memory only. Drastically increases startup time. | `false` | | `pylsp.plugins.rope_completion.enabled` | `boolean` | Enable or disable the plugin. | `false` | | `pylsp.plugins.rope_completion.eager` | `boolean` | Resolve documentation and detail eagerly. | `false` | diff --git a/docs/autoimport.md b/docs/autoimport.md index 5bf573e9..893a5e98 100644 --- a/docs/autoimport.md +++ b/docs/autoimport.md @@ -4,6 +4,7 @@ Requirements: 1. install `python-lsp-server[rope]` 2. set `pylsp.plugins.rope_autoimport.enabled` to `true` +3. This enables both completions and code actions. You can switch them off by setting `pylsp.plugins.rope_autoimport.completions.enabled` and/or `pylsp.plugins.rope_autoimport.code_actions.enabled` to `false` ## Startup diff --git a/pylsp/config/schema.json b/pylsp/config/schema.json index fbf7f014..ba1d36f8 100644 --- a/pylsp/config/schema.json +++ b/pylsp/config/schema.json @@ -6,11 +6,16 @@ "properties": { "pylsp.configurationSources": { "type": "array", - "default": ["pycodestyle"], + "default": [ + "pycodestyle" + ], "description": "List of configuration sources to use.", "items": { "type": "string", - "enum": ["pycodestyle", "flake8"] + "enum": [ + "pycodestyle", + "flake8" + ] }, "uniqueItems": true }, @@ -20,7 +25,10 @@ "description": "Enable or disable the plugin (disabling required to use `yapf`)." }, "pylsp.plugins.flake8.config": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null, "description": "Path to the config file that will be the authoritative config source." }, @@ -51,12 +59,18 @@ "description": "Path to the flake8 executable." }, "pylsp.plugins.flake8.filename": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null, "description": "Only check for filenames matching the patterns in this list." }, "pylsp.plugins.flake8.hangClosing": { - "type": ["boolean", "null"], + "type": [ + "boolean", + "null" + ], "default": null, "description": "Hang closing bracket instead of matching indentation of opening bracket's line." }, @@ -74,17 +88,25 @@ "description": "Maximum allowed complexity threshold." }, "pylsp.plugins.flake8.maxLineLength": { - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "default": null, "description": "Maximum allowed line length for the entirety of this run." }, "pylsp.plugins.flake8.indentSize": { - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "default": null, "description": "Set indentation spaces." }, "pylsp.plugins.flake8.perFileIgnores": { - "type": ["array"], + "type": [ + "array" + ], "default": [], "items": { "type": "string" @@ -92,7 +114,10 @@ "description": "A pairing of filenames and violation codes that defines which violations to ignore in a particular file, for example: `[\"file_path.py:W305,W304\"]`)." }, "pylsp.plugins.flake8.select": { - "type": ["array", "null"], + "type": [ + "array", + "null" + ], "default": null, "items": { "type": "string" @@ -102,7 +127,9 @@ }, "pylsp.plugins.jedi.auto_import_modules": { "type": "array", - "default": ["numpy"], + "default": [ + "numpy" + ], "items": { "type": "string" }, @@ -117,12 +144,18 @@ "description": "Define extra paths for jedi.Script." }, "pylsp.plugins.jedi.env_vars": { - "type": ["object", "null"], + "type": [ + "object", + "null" + ], "default": null, "description": "Define environment variables for jedi.Script and Jedi.names." }, "pylsp.plugins.jedi.environment": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null, "description": "Define environment for jedi.Script and Jedi.names." }, @@ -166,7 +199,12 @@ "items": { "type": "string" }, - "default": ["pandas", "numpy", "tensorflow", "matplotlib"], + "default": [ + "pandas", + "numpy", + "tensorflow", + "matplotlib" + ], "description": "Modules for which labels and snippets should be cached." }, "pylsp.plugins.jedi_definition.enabled": { @@ -267,7 +305,10 @@ "description": "When parsing directories, only check filenames matching these patterns." }, "pylsp.plugins.pycodestyle.select": { - "type": ["array", "null"], + "type": [ + "array", + "null" + ], "default": null, "items": { "type": "string" @@ -285,17 +326,26 @@ "description": "Ignore errors and warnings" }, "pylsp.plugins.pycodestyle.hangClosing": { - "type": ["boolean", "null"], + "type": [ + "boolean", + "null" + ], "default": null, "description": "Hang closing bracket instead of matching indentation of opening bracket's line." }, "pylsp.plugins.pycodestyle.maxLineLength": { - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "default": null, "description": "Set maximum allowed line length." }, "pylsp.plugins.pycodestyle.indentSize": { - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "default": null, "description": "Set indentation spaces." }, @@ -305,9 +355,17 @@ "description": "Enable or disable the plugin." }, "pylsp.plugins.pydocstyle.convention": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null, - "enum": ["pep257", "numpy", "google", null], + "enum": [ + "pep257", + "numpy", + "google", + null + ], "description": "Choose the basic list of checked errors by specifying an existing convention." }, "pylsp.plugins.pydocstyle.addIgnore": { @@ -338,7 +396,10 @@ "description": "Ignore errors and warnings" }, "pylsp.plugins.pydocstyle.select": { - "type": ["array", "null"], + "type": [ + "array", + "null" + ], "default": null, "items": { "type": "string" @@ -376,14 +437,27 @@ "description": "Arguments to pass to pylint." }, "pylsp.plugins.pylint.executable": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null, "description": "Executable to run pylint with. Enabling this will run pylint on unsaved files via stdin. Can slow down workflow. Only works with python3." }, "pylsp.plugins.rope_autoimport.enabled": { "type": "boolean", "default": false, - "description": "Enable or disable autoimport." + "description": "Enable or disable autoimport. If false, neither completions nor code actions are enabled. If true, the respective features can be enabled or disabled individually." + }, + "pylsp.plugins.rope_autoimport.completions.enabled": { + "type": "boolean", + "default": true, + "description": "Enable or disable autoimport completions." + }, + "pylsp.plugins.rope_autoimport.code_actions.enabled": { + "type": "boolean", + "default": true, + "description": "Enable or disable autoimport code actions (e.g. for quick fixes)." }, "pylsp.plugins.rope_autoimport.memory": { "type": "boolean", @@ -406,12 +480,18 @@ "description": "Enable or disable the plugin." }, "pylsp.rope.extensionModules": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "default": null, "description": "Builtin and c-extension modules that are allowed to be imported and inspected by rope." }, "pylsp.rope.ropeFolder": { - "type": ["array", "null"], + "type": [ + "array", + "null" + ], "default": null, "items": { "type": "string" @@ -420,4 +500,4 @@ "description": "The name of the folder in which rope stores project configurations and data. Pass `null` for not using such a folder at all." } } -} +} \ No newline at end of file diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index 1caab35d..ca3db1cf 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -21,13 +21,27 @@ _score_pow = 5 _score_max = 10**_score_pow -MAX_RESULTS = 1000 +MAX_RESULTS_COMPLETIONS = 1000 +MAX_RESULTS_CODE_ACTIONS = 5 @hookimpl def pylsp_settings() -> Dict[str, Dict[str, Dict[str, Any]]]: # Default rope_completion to disabled - return {"plugins": {"rope_autoimport": {"enabled": False, "memory": False}}} + return { + "plugins": { + "rope_autoimport": { + "enabled": False, + "memory": False, + "completions": { + "enabled": True, + }, + "code_actions": { + "enabled": True, + }, + } + } + } # pylint: disable=too-many-return-statements @@ -122,6 +136,7 @@ def _process_statements( word: str, autoimport: AutoImport, document: Document, + feature: str = "completions", ) -> Generator[Dict[str, Any], None, None]: for suggestion in suggestions: insert_line = autoimport.find_insertion_line(document.source) - 1 @@ -134,14 +149,26 @@ def _process_statements( if score > _score_max: continue # TODO make this markdown - yield { - "label": suggestion.name, - "kind": suggestion.itemkind, - "sortText": _sort_import(score), - "data": {"doc_uri": doc_uri}, - "detail": _document(suggestion.import_statement), - "additionalTextEdits": [edit], - } + if feature == "completions": + yield { + "label": suggestion.name, + "kind": suggestion.itemkind, + "sortText": _sort_import(score), + "data": {"doc_uri": doc_uri}, + "detail": _document(suggestion.import_statement), + "additionalTextEdits": [edit], + } + elif feature == "code_actions": + yield { + "title": suggestion.import_statement, + "kind": "quickfix", + "edit": {"changes": {doc_uri: [edit]}}, + # data is a supported field for codeAction responses + # See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_codeAction # pylint: disable=line-too-long + "data": {"sortText": _sort_import(score)}, + } + else: + raise ValueError(f"Unknown feature: {feature}") def get_names(script: Script) -> Set[str]: @@ -160,6 +187,13 @@ def pylsp_completions( ignored_names: Union[Set[str], None], ): """Get autoimport suggestions.""" + if ( + not config.plugin_settings("rope_autoimport") + .get("completions", {}) + .get("enabled", True) + ): + return [] + line = document.lines[position["line"]] expr = parso.parse(line) word_node = expr.get_leaf_for_position((1, position["character"])) @@ -175,12 +209,14 @@ def pylsp_completions( suggestions = list(autoimport.search_full(word, ignored_names=ignored_names)) results = list( sorted( - _process_statements(suggestions, document.uri, word, autoimport, document), + _process_statements( + suggestions, document.uri, word, autoimport, document, "completions" + ), key=lambda statement: statement["sortText"], ) ) - if len(results) > MAX_RESULTS: - results = results[:MAX_RESULTS] + if len(results) > MAX_RESULTS_COMPLETIONS: + results = results[:MAX_RESULTS_COMPLETIONS] return results @@ -206,6 +242,83 @@ def _sort_import(score: int) -> str: return "[z" + str(score).rjust(_score_pow, "0") +def get_name_or_module(document, diagnostic) -> str: + start = diagnostic["range"]["start"] + return ( + parso.parse(document.lines[start["line"]]) + .get_leaf_for_position((1, start["character"] + 1)) + .value + ) + + +@hookimpl +def pylsp_code_actions( + config: Config, + workspace: Workspace, + document: Document, + range: Dict, # pylint: disable=redefined-builtin + context: Dict, +) -> List[Dict]: + """ + Provide code actions through rope. + + Parameters + ---------- + config : pylsp.config.config.Config + Current config. + workspace : pylsp.workspace.Workspace + Current workspace. + document : pylsp.workspace.Document + Document to apply code actions on. + range : Dict + Range argument given by pylsp. Not used here. + context : Dict + CodeActionContext given as dict. + + Returns + ------- + List of dicts containing the code actions. + """ + if ( + not config.plugin_settings("rope_autoimport") + .get("code_actions", {}) + .get("enabled", True) + ): + return [] + + log.debug(f"textDocument/codeAction: {document} {range} {context}") + code_actions = [] + for diagnostic in context.get("diagnostics", []): + if "undefined name" not in diagnostic.get("message", "").lower(): + continue + + word = get_name_or_module(document, diagnostic) + log.debug(f"autoimport: searching for word: {word}") + rope_config = config.settings(document_path=document.path).get("rope", {}) + autoimport = workspace._rope_autoimport(rope_config, feature="code_actions") + suggestions = list(autoimport.search_full(word)) + log.debug("autoimport: suggestions: %s", suggestions) + results = list( + sorted( + _process_statements( + suggestions, + document.uri, + word, + autoimport, + document, + "code_actions", + ), + key=lambda statement: statement["data"]["sortText"], + ) + ) + + if len(results) > MAX_RESULTS_CODE_ACTIONS: + results = results[:MAX_RESULTS_CODE_ACTIONS] + code_actions.extend(results) + + return code_actions + + def _reload_cache( config: Config, workspace: Workspace, files: Optional[List[Document]] = None ): diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index 760ad974..52a22a3e 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -385,7 +385,7 @@ def watch_parent_process(pid): def m_initialized(self, **_kwargs): self._hook("pylsp_initialized") - def code_actions(self, doc_uri, range, context): + def code_actions(self, doc_uri: str, range: Dict, context: Dict): return flatten( self._hook("pylsp_code_actions", doc_uri, range=range, context=context) ) diff --git a/pylsp/workspace.py b/pylsp/workspace.py index fb524c71..5c6880c9 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -8,7 +8,7 @@ import re import uuid import functools -from typing import Optional, Generator, Callable, List +from typing import Literal, Optional, Generator, Callable, List from threading import RLock import jedi @@ -58,16 +58,30 @@ def __init__(self, root_uri, endpoint, config=None): # Whilst incubating, keep rope private self.__rope = None self.__rope_config = None - self.__rope_autoimport = None - def _rope_autoimport(self, rope_config: Optional, memory: bool = False): + # We have a sperate AutoImport object for each feature to avoid sqlite errors + # from accessing the same database from multiple threads. + # An upstream fix discussion is here: https://github.com/python-rope/rope/issues/713 + self.__rope_autoimport = ( + {} + ) # Type: Dict[Literal["completions", "code_actions"], rope.contrib.autoimport.sqlite.AutoImport] + + def _rope_autoimport( + self, + rope_config: Optional, + memory: bool = False, + feature: Literal["completions", "code_actions"] = "completions", + ): # pylint: disable=import-outside-toplevel from rope.contrib.autoimport.sqlite import AutoImport - if self.__rope_autoimport is None: + if feature not in ["completions", "code_actions"]: + raise ValueError(f"Unknown feature {feature}") + + if self.__rope_autoimport.get(feature, None) is None: project = self._rope_project_builder(rope_config) - self.__rope_autoimport = AutoImport(project, memory=memory) - return self.__rope_autoimport + self.__rope_autoimport[feature] = AutoImport(project, memory=memory) + return self.__rope_autoimport[feature] def _rope_project_builder(self, rope_config): # pylint: disable=import-outside-toplevel @@ -374,8 +388,8 @@ def _create_cell_document( ) def close(self): - if self.__rope_autoimport is not None: - self.__rope_autoimport.close() + for _, autoimport in self.__rope_autoimport.items(): + autoimport.close() class Document: diff --git a/test/fixtures.py b/test/fixtures.py index ed6206af..11c302b0 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -8,7 +8,6 @@ from test.test_utils import ClientServerPair, CALL_TIMEOUT_IN_SECONDS import pytest -import pylsp_jsonrpc from pylsp_jsonrpc.dispatchers import MethodDispatcher from pylsp_jsonrpc.endpoint import Endpoint @@ -176,13 +175,8 @@ def client_server_pair(): yield (client_server_pair_obj.client, client_server_pair_obj.server) - try: - shutdown_response = client_server_pair_obj.client._endpoint.request( - "shutdown" - ).result(timeout=CALL_TIMEOUT_IN_SECONDS) - assert shutdown_response is None - client_server_pair_obj.client._endpoint.notify("exit") - except pylsp_jsonrpc.exceptions.JsonRpcInvalidParams: - # SQLite objects created in a thread can only be used in that same thread. - # This exeception is raised when testing rope autoimport. - client_server_pair_obj.client._endpoint.notify("exit") + shutdown_response = client_server_pair_obj.client._endpoint.request( + "shutdown" + ).result(timeout=CALL_TIMEOUT_IN_SECONDS) + assert shutdown_response is None + client_server_pair_obj.client._endpoint.notify("exit") diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index b1c46775..ec5c0a33 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -1,18 +1,20 @@ # Copyright 2022- Python Language Server Contributors. from typing import Any, Dict, List -from unittest.mock import Mock, patch - -from test.test_notebook_document import wait_for_condition -from test.test_utils import send_initialize_request, send_notebook_did_open +from unittest.mock import Mock import jedi import parso import pytest -from pylsp import IS_WIN, lsp, uris +from pylsp import lsp, uris from pylsp.config.config import Config -from pylsp.plugins.rope_autoimport import _get_score, _should_insert, get_names +from pylsp.plugins.rope_autoimport import ( + _get_score, + _should_insert, + get_name_or_module, + get_names, +) from pylsp.plugins.rope_autoimport import ( pylsp_completions as pylsp_autoimport_completions, ) @@ -37,7 +39,16 @@ def autoimport_workspace(tmp_path_factory) -> Workspace: uris.from_fs_path(str(tmp_path_factory.mktemp("pylsp"))), Mock() ) workspace._config = Config(workspace.root_uri, {}, 0, {}) - workspace._config.update({"rope_autoimport": {"memory": True, "enabled": True}}) + workspace._config.update( + { + "rope_autoimport": { + "memory": True, + "enabled": True, + "completions": {"enabled": True}, + "code_actions": {"enabled": True}, + } + } + ) pylsp_initialize(workspace._config, workspace) yield workspace workspace.close() @@ -216,72 +227,103 @@ class sfa: assert results == set(["blah", "bleh", "e", "hello", "someone", "sfa", "a", "b"]) -@pytest.mark.skipif(IS_WIN, reason="Flaky on Windows") -def test_autoimport_for_notebook_document( - client_server_pair, -): - client, server = client_server_pair - send_initialize_request(client) - - with patch.object(server._endpoint, "notify") as mock_notify: - # Expectations: - # 1. We receive an autoimport suggestion for "os" in the first cell because - # os is imported after that. - # 2. We don't receive an autoimport suggestion for "os" in the second cell because it's - # already imported in the second cell. - # 3. We don't receive an autoimport suggestion for "os" in the third cell because it's - # already imported in the second cell. - # 4. We receive an autoimport suggestion for "sys" because it's not already imported - send_notebook_did_open(client, ["os", "import os\nos", "os", "sys"]) - wait_for_condition(lambda: mock_notify.call_count >= 3) - - server.m_workspace__did_change_configuration( - settings={ - "pylsp": {"plugins": {"rope_autoimport": {"enabled": True, "memory": True}}} - } - ) - rope_autoimport_settings = server.workspace._config.plugin_settings( - "rope_autoimport" - ) - assert rope_autoimport_settings.get("enabled", False) is True - assert rope_autoimport_settings.get("memory", False) is True - - # 1. - suggestions = server.completions("cell_1_uri", {"line": 0, "character": 2}).get( - "items" - ) - assert any( - suggestion - for suggestion in suggestions - if contains_autoimport(suggestion, "os") - ) - - # 2. - suggestions = server.completions("cell_2_uri", {"line": 1, "character": 2}).get( - "items" - ) - assert not any( - suggestion - for suggestion in suggestions - if contains_autoimport(suggestion, "os") - ) - - # 3. - suggestions = server.completions("cell_3_uri", {"line": 0, "character": 2}).get( - "items" - ) - assert not any( - suggestion - for suggestion in suggestions - if contains_autoimport(suggestion, "os") - ) - - # 4. - suggestions = server.completions("cell_4_uri", {"line": 0, "character": 3}).get( - "items" - ) - assert any( - suggestion - for suggestion in suggestions - if contains_autoimport(suggestion, "sys") - ) +# Tests ruff, flake8 and pyflakes messages +@pytest.mark.parametrize( + "message", + ["Undefined name `os`", "F821 undefined name 'numpy'", "undefined name 'numpy'"], +) +def test_autoimport_code_actions_get_correct_module_name(autoimport_workspace, message): + source = "os.path.join('a', 'b')" + autoimport_workspace.put_document(DOC_URI, source=source) + doc = autoimport_workspace.get_document(DOC_URI) + diagnostic = { + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": 0, "character": 2}, + }, + "message": message, + } + module_name = get_name_or_module(doc, diagnostic) + autoimport_workspace.rm_document(DOC_URI) + assert module_name == "os" + + +# rope autoimport launches a sqlite database which checks from which thread it is called. +# This makes the test below fail because we access the db from a different thread. +# See https://stackoverflow.com/questions/48218065/objects-created-in-a-thread-can-only-be-used-in-that-same-thread +# @pytest.mark.skipif(IS_WIN, reason="Flaky on Windows") +# def test_autoimport_completions_for_notebook_document( +# client_server_pair, +# ): +# client, server = client_server_pair +# send_initialize_request(client) + +# with patch.object(server._endpoint, "notify") as mock_notify: +# # Expectations: +# # 1. We receive an autoimport suggestion for "os" in the first cell because +# # os is imported after that. +# # 2. We don't receive an autoimport suggestion for "os" in the second cell because it's +# # already imported in the second cell. +# # 3. We don't receive an autoimport suggestion for "os" in the third cell because it's +# # already imported in the second cell. +# # 4. We receive an autoimport suggestion for "sys" because it's not already imported +# send_notebook_did_open(client, ["os", "import os\nos", "os", "sys"]) +# wait_for_condition(lambda: mock_notify.call_count >= 3) + +# server.m_workspace__did_change_configuration( +# settings={ +# "pylsp": { +# "plugins": { +# "rope_autoimport": { +# "memory": True, +# "completions": {"enabled": True}, +# }, +# } +# } +# } +# ) +# rope_autoimport_settings = server.workspace._config.plugin_settings( +# "rope_autoimport" +# ) +# assert rope_autoimport_settings.get("completions", {}).get("enabled", False) is True +# assert rope_autoimport_settings.get("memory", False) is True + +# # 1. +# suggestions = server.completions("cell_1_uri", {"line": 0, "character": 2}).get( +# "items" +# ) +# assert any( +# suggestion +# for suggestion in suggestions +# if contains_autoimport(suggestion, "os") +# ) + +# # 2. +# suggestions = server.completions("cell_2_uri", {"line": 1, "character": 2}).get( +# "items" +# ) +# assert not any( +# suggestion +# for suggestion in suggestions +# if contains_autoimport(suggestion, "os") +# ) + +# # 3. +# suggestions = server.completions("cell_3_uri", {"line": 0, "character": 2}).get( +# "items" +# ) +# assert not any( +# suggestion +# for suggestion in suggestions +# if contains_autoimport(suggestion, "os") +# ) + +# # 4. +# suggestions = server.completions("cell_4_uri", {"line": 0, "character": 3}).get( +# "items" +# ) +# assert any( +# suggestion +# for suggestion in suggestions +# if contains_autoimport(suggestion, "sys") +# ) From 5f53f8e7ecacaa2e2ea91bc8707729980dffc407 Mon Sep 17 00:00:00 2001 From: Ben Greiner Date: Tue, 31 Oct 2023 16:37:57 +0100 Subject: [PATCH 19/23] Raise supported Pylint upper version (#475) --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d2983edb..17569525 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ all = [ "pycodestyle>=2.11.0,<2.12.0", "pydocstyle>=6.3.0,<6.4.0", "pyflakes>=3.1.0,<3.2.0", - "pylint>=2.5.0,<3", + "pylint>=2.5.0,<3.1", "rope>1.2.0", "yapf>=0.33.0", "whatthepatch>=1.0.2,<2.0.0" @@ -44,12 +44,12 @@ mccabe = ["mccabe>=0.7.0,<0.8.0"] pycodestyle = ["pycodestyle>=2.11.0,<2.12.0"] pydocstyle = ["pydocstyle>=6.3.0,<6.4.0"] pyflakes = ["pyflakes>=3.1.0,<3.2.0"] -pylint = ["pylint>=2.5.0,<3"] +pylint = ["pylint>=2.5.0,<3.1"] rope = ["rope>1.2.0"] yapf = ["yapf>=0.33.0", "whatthepatch>=1.0.2,<2.0.0"] websockets = ["websockets>=10.3"] test = [ - "pylint>=2.5.0,<3", + "pylint>=2.5.0,<3.1", "pytest", "pytest-cov", "coverage", From ec876bb3426d09efc2359fc31757806ce08c0abf Mon Sep 17 00:00:00 2001 From: Tobias Krabel Date: Thu, 2 Nov 2023 17:30:25 +0100 Subject: [PATCH 20/23] Improve/simplify README Development section (#473) --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2e2c7218..842cdbad 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ pip install -U setuptools If you use Anaconda/Miniconda, you can install `python-lsp-server` using this conda command ``` -conda install -c conda-forge python-lsp-server +conda install -c conda-forge python-lsp-server ``` Python-lsp-server is available in the repos of every major Linux distribution, and it is usually called `python-lsp-server` or `python3-pylsp`. @@ -164,25 +164,25 @@ pip install 'python-lsp-server[websockets]' Dev install ``` -# create conda env -conda create --name python-lsp-server python=3.8 -y +# (optional) create conda env +conda create --name python-lsp-server python=3.11 -y conda activate python-lsp-server -pip install ".[all]" -pip install ".[websockets]" +pip install -e ".[all,websockets,test]" ``` Run server with ws ``` -pylsp --ws -v # Info level logging -pylsp --ws -v -v # Debug level logging +pylsp --ws -v # Info level logging +pylsp --ws -vv # Debug level logging ``` To run the test suite: ```sh -pip install ".[test]" && pytest +# requires: pip install ".[test]" (see above) +pytest ``` After adding configuration options to `schema.json`, refresh the `CONFIGURATION.md` file with From 1c4ad711deed051d3a18fa91287f3c86ce4beed3 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 6 Nov 2023 11:51:06 -0500 Subject: [PATCH 21/23] Rename `_utils` module to `utils` (#480) --- pylsp/config/config.py | 16 ++++++------- pylsp/config/flake8_conf.py | 2 +- pylsp/config/pycodestyle_conf.py | 2 +- pylsp/plugins/autopep8_format.py | 2 +- pylsp/plugins/definition.py | 4 ++-- pylsp/plugins/highlight.py | 4 ++-- pylsp/plugins/hover.py | 8 +++---- pylsp/plugins/jedi_completion.py | 10 ++++---- pylsp/plugins/jedi_rename.py | 6 ++--- pylsp/plugins/pycodestyle_lint.py | 2 +- pylsp/plugins/references.py | 4 ++-- pylsp/plugins/rope_completion.py | 8 +++---- pylsp/plugins/rope_rename.py | 4 ++-- pylsp/plugins/signature.py | 10 ++++---- pylsp/plugins/yapf_format.py | 2 +- pylsp/python_lsp.py | 8 +++---- pylsp/{_utils.py => utils.py} | 0 pylsp/workspace.py | 8 +++---- test/plugins/test_completion.py | 2 +- test/test_utils.py | 40 +++++++++++++++---------------- 20 files changed, 71 insertions(+), 71 deletions(-) rename pylsp/{_utils.py => utils.py} (100%) diff --git a/pylsp/config/config.py b/pylsp/config/config.py index 454ee4b3..cae6b374 100644 --- a/pylsp/config/config.py +++ b/pylsp/config/config.py @@ -10,7 +10,7 @@ import pluggy from pluggy._hooks import HookImpl -from pylsp import _utils, hookspecs, uris, PYLSP +from pylsp import utils, hookspecs, uris, PYLSP # See compatibility note on `group` keyword: # https://docs.python.org/3/library/importlib.metadata.html#entry-points @@ -94,11 +94,11 @@ def __init__(self, root_uri, init_opts, process_id, capabilities): log.info("Loaded pylsp plugin %s from %s", name, plugin) for plugin_conf in self._pm.hook.pylsp_settings(config=self): - self._plugin_settings = _utils.merge_dicts( + self._plugin_settings = utils.merge_dicts( self._plugin_settings, plugin_conf ) - self._plugin_settings = _utils.merge_dicts( + self._plugin_settings = utils.merge_dicts( self._plugin_settings, self._init_opts.get("pylsp", {}) ) @@ -144,10 +144,10 @@ def settings(self, document_path=None): sources = self._settings.get("configurationSources", DEFAULT_CONFIG_SOURCES) # Plugin configuration - settings = _utils.merge_dicts(settings, self._plugin_settings) + settings = utils.merge_dicts(settings, self._plugin_settings) # LSP configuration - settings = _utils.merge_dicts(settings, self._settings) + settings = utils.merge_dicts(settings, self._settings) # User configuration for source_name in reversed(sources): @@ -158,7 +158,7 @@ def settings(self, document_path=None): log.debug( "Got user config from %s: %s", source.__class__.__name__, source_conf ) - settings = _utils.merge_dicts(settings, source_conf) + settings = utils.merge_dicts(settings, source_conf) # Project configuration for source_name in reversed(sources): @@ -169,7 +169,7 @@ def settings(self, document_path=None): log.debug( "Got project config from %s: %s", source.__class__.__name__, source_conf ) - settings = _utils.merge_dicts(settings, source_conf) + settings = utils.merge_dicts(settings, source_conf) log.debug("With configuration: %s", settings) @@ -177,7 +177,7 @@ def settings(self, document_path=None): def find_parents(self, path, names): root_path = uris.to_fs_path(self._root_uri) - return _utils.find_parents(root_path, path, names) + return utils.find_parents(root_path, path, names) def plugin_settings(self, plugin, document_path=None): return ( diff --git a/pylsp/config/flake8_conf.py b/pylsp/config/flake8_conf.py index ca3b199c..4514eb53 100644 --- a/pylsp/config/flake8_conf.py +++ b/pylsp/config/flake8_conf.py @@ -3,7 +3,7 @@ import logging import os -from pylsp._utils import find_parents +from pylsp.utils import find_parents from .source import ConfigSource log = logging.getLogger(__name__) diff --git a/pylsp/config/pycodestyle_conf.py b/pylsp/config/pycodestyle_conf.py index 98d8a1b1..c324ec46 100644 --- a/pylsp/config/pycodestyle_conf.py +++ b/pylsp/config/pycodestyle_conf.py @@ -2,7 +2,7 @@ # Copyright 2021- Python Language Server Contributors. import pycodestyle -from pylsp._utils import find_parents +from pylsp.utils import find_parents from .source import ConfigSource diff --git a/pylsp/plugins/autopep8_format.py b/pylsp/plugins/autopep8_format.py index 1ae3e5f1..9710a86a 100644 --- a/pylsp/plugins/autopep8_format.py +++ b/pylsp/plugins/autopep8_format.py @@ -7,7 +7,7 @@ from autopep8 import fix_code, continued_indentation as autopep8_c_i from pylsp import hookimpl -from pylsp._utils import get_eol_chars +from pylsp.utils import get_eol_chars log = logging.getLogger(__name__) diff --git a/pylsp/plugins/definition.py b/pylsp/plugins/definition.py index 53eda915..73d156a9 100644 --- a/pylsp/plugins/definition.py +++ b/pylsp/plugins/definition.py @@ -6,7 +6,7 @@ import jedi -from pylsp import hookimpl, uris, _utils +from pylsp import hookimpl, uris, utils if TYPE_CHECKING: from jedi.api import Script @@ -44,7 +44,7 @@ def pylsp_definitions( config: Config, document: Document, position: Dict[str, int] ) -> List[Dict[str, Any]]: settings = config.plugin_settings("jedi_definition") - code_position = _utils.position_to_jedi_linecolumn(document, position) + code_position = utils.position_to_jedi_linecolumn(document, position) script = document.jedi_script(use_document_path=True) auto_import_modules = jedi.settings.auto_import_modules diff --git a/pylsp/plugins/highlight.py b/pylsp/plugins/highlight.py index 0dd896c6..0ed040e4 100644 --- a/pylsp/plugins/highlight.py +++ b/pylsp/plugins/highlight.py @@ -2,14 +2,14 @@ # Copyright 2021- Python Language Server Contributors. import logging -from pylsp import hookimpl, lsp, _utils +from pylsp import hookimpl, lsp, utils log = logging.getLogger(__name__) @hookimpl def pylsp_document_highlight(document, position): - code_position = _utils.position_to_jedi_linecolumn(document, position) + code_position = utils.position_to_jedi_linecolumn(document, position) usages = document.jedi_script().get_references(**code_position) def is_valid(definition): diff --git a/pylsp/plugins/hover.py b/pylsp/plugins/hover.py index ae07b3dc..cccfdfbc 100644 --- a/pylsp/plugins/hover.py +++ b/pylsp/plugins/hover.py @@ -3,14 +3,14 @@ import logging -from pylsp import hookimpl, _utils +from pylsp import hookimpl, utils log = logging.getLogger(__name__) @hookimpl def pylsp_hover(config, document, position): - code_position = _utils.position_to_jedi_linecolumn(document, position) + code_position = utils.position_to_jedi_linecolumn(document, position) definitions = document.jedi_script(use_document_path=True).infer(**code_position) word = document.word_at_position(position) @@ -28,7 +28,7 @@ def pylsp_hover(config, document, position): hover_capabilities = config.capabilities.get("textDocument", {}).get("hover", {}) supported_markup_kinds = hover_capabilities.get("contentFormat", ["markdown"]) - preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) + preferred_markup_kind = utils.choose_markup_kind(supported_markup_kinds) # Find first exact matching signature signature = next( @@ -41,7 +41,7 @@ def pylsp_hover(config, document, position): ) return { - "contents": _utils.format_docstring( + "contents": utils.format_docstring( # raw docstring returns only doc, without signature definition.docstring(raw=True), preferred_markup_kind, diff --git a/pylsp/plugins/jedi_completion.py b/pylsp/plugins/jedi_completion.py index 2ecf0bec..324a127f 100644 --- a/pylsp/plugins/jedi_completion.py +++ b/pylsp/plugins/jedi_completion.py @@ -6,7 +6,7 @@ import parso -from pylsp import _utils, hookimpl, lsp +from pylsp import utils, hookimpl, lsp from pylsp.plugins._resolvers import LABEL_RESOLVER, SNIPPET_RESOLVER log = logging.getLogger(__name__) @@ -41,7 +41,7 @@ def pylsp_completions(config, document, position): # pylint: disable=too-many-locals settings = config.plugin_settings("jedi_completion", document_path=document.path) resolve_eagerly = settings.get("eager", False) - code_position = _utils.position_to_jedi_linecolumn(document, position) + code_position = utils.position_to_jedi_linecolumn(document, position) code_position["fuzzy"] = settings.get("fuzzy", False) completions = document.jedi_script(use_document_path=True).complete(**code_position) @@ -55,7 +55,7 @@ def pylsp_completions(config, document, position): item_capabilities = completion_capabilities.get("completionItem", {}) snippet_support = item_capabilities.get("snippetSupport") supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"]) - preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) + preferred_markup_kind = utils.choose_markup_kind(supported_markup_kinds) should_include_params = settings.get("include_params") should_include_class_objects = settings.get("include_class_objects", False) @@ -146,7 +146,7 @@ def pylsp_completion_item_resolve(config, completion_item, document): ) item_capabilities = completion_capabilities.get("completionItem", {}) supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"]) - preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) + preferred_markup_kind = utils.choose_markup_kind(supported_markup_kinds) if shared_data: completion, data = shared_data @@ -209,7 +209,7 @@ def _resolve_completion(completion, d, markup_kind: str): # pylint: disable=broad-except completion["detail"] = _detail(d) try: - docs = _utils.format_docstring( + docs = utils.format_docstring( d.docstring(raw=True), signatures=[signature.to_string() for signature in d.get_signatures()], markup_kind=markup_kind, diff --git a/pylsp/plugins/jedi_rename.py b/pylsp/plugins/jedi_rename.py index 9c89c1de..65ae7594 100644 --- a/pylsp/plugins/jedi_rename.py +++ b/pylsp/plugins/jedi_rename.py @@ -3,7 +3,7 @@ import logging -from pylsp import hookimpl, uris, _utils +from pylsp import hookimpl, uris, utils log = logging.getLogger(__name__) @@ -15,7 +15,7 @@ def pylsp_rename( log.debug( "Executing rename of %s to %s", document.word_at_position(position), new_name ) - kwargs = _utils.position_to_jedi_linecolumn(document, position) + kwargs = utils.position_to_jedi_linecolumn(document, position) kwargs["new_name"] = new_name try: refactoring = document.jedi_script().rename(**kwargs) @@ -54,6 +54,6 @@ def pylsp_rename( def _num_lines(file_contents): "Count the number of lines in the given string." - if _utils.get_eol_chars(file_contents): + if utils.get_eol_chars(file_contents): return len(file_contents.splitlines()) return 0 diff --git a/pylsp/plugins/pycodestyle_lint.py b/pylsp/plugins/pycodestyle_lint.py index 62b0b8ad..83dc7ddd 100644 --- a/pylsp/plugins/pycodestyle_lint.py +++ b/pylsp/plugins/pycodestyle_lint.py @@ -6,7 +6,7 @@ import pycodestyle from pylsp import hookimpl, lsp -from pylsp._utils import get_eol_chars +from pylsp.utils import get_eol_chars try: from autopep8 import continued_indentation as autopep8_c_i diff --git a/pylsp/plugins/references.py b/pylsp/plugins/references.py index fadf1de8..b05c76b0 100644 --- a/pylsp/plugins/references.py +++ b/pylsp/plugins/references.py @@ -2,14 +2,14 @@ # Copyright 2021- Python Language Server Contributors. import logging -from pylsp import hookimpl, uris, _utils +from pylsp import hookimpl, uris, utils log = logging.getLogger(__name__) @hookimpl def pylsp_references(document, position, exclude_declaration): - code_position = _utils.position_to_jedi_linecolumn(document, position) + code_position = utils.position_to_jedi_linecolumn(document, position) usages = document.jedi_script().get_references(**code_position) if exclude_declaration: diff --git a/pylsp/plugins/rope_completion.py b/pylsp/plugins/rope_completion.py index ca0d4349..b30458c6 100644 --- a/pylsp/plugins/rope_completion.py +++ b/pylsp/plugins/rope_completion.py @@ -4,7 +4,7 @@ import logging from rope.contrib.codeassist import code_assist, sorted_proposals -from pylsp import _utils, hookimpl, lsp +from pylsp import utils, hookimpl, lsp log = logging.getLogger(__name__) @@ -19,7 +19,7 @@ def pylsp_settings(): def _resolve_completion(completion, data, markup_kind): # pylint: disable=broad-except try: - doc = _utils.format_docstring(data.get_doc(), markup_kind=markup_kind) + doc = utils.format_docstring(data.get_doc(), markup_kind=markup_kind) except Exception as e: log.debug("Failed to resolve Rope completion: %s", e) doc = "" @@ -57,7 +57,7 @@ def pylsp_completions(config, workspace, document, position): ) item_capabilities = completion_capabilities.get("completionItem", {}) supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"]) - preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) + preferred_markup_kind = utils.choose_markup_kind(supported_markup_kinds) try: definitions = code_assist( @@ -104,7 +104,7 @@ def pylsp_completion_item_resolve(config, completion_item, document): ) item_capabilities = completion_capabilities.get("completionItem", {}) supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"]) - preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) + preferred_markup_kind = utils.choose_markup_kind(supported_markup_kinds) if shared_data: completion, data = shared_data diff --git a/pylsp/plugins/rope_rename.py b/pylsp/plugins/rope_rename.py index a4323a42..d0873e69 100644 --- a/pylsp/plugins/rope_rename.py +++ b/pylsp/plugins/rope_rename.py @@ -6,7 +6,7 @@ from rope.base import libutils from rope.refactor.rename import Rename -from pylsp import hookimpl, uris, _utils +from pylsp import hookimpl, uris, utils log = logging.getLogger(__name__) @@ -61,6 +61,6 @@ def _num_lines(resource): "Count the number of lines in a `File` resource." text = resource.read() - if _utils.get_eol_chars(text): + if utils.get_eol_chars(text): return len(text.splitlines()) return 0 diff --git a/pylsp/plugins/signature.py b/pylsp/plugins/signature.py index 4fc93dfb..0e625b27 100644 --- a/pylsp/plugins/signature.py +++ b/pylsp/plugins/signature.py @@ -3,7 +3,7 @@ import logging import re -from pylsp import hookimpl, _utils +from pylsp import hookimpl, utils log = logging.getLogger(__name__) @@ -16,7 +16,7 @@ @hookimpl def pylsp_signature_help(config, document, position): - code_position = _utils.position_to_jedi_linecolumn(document, position) + code_position = utils.position_to_jedi_linecolumn(document, position) signatures = document.jedi_script().get_signatures(**code_position) if not signatures: @@ -31,7 +31,7 @@ def pylsp_signature_help(config, document, position): supported_markup_kinds = signature_information_support.get( "documentationFormat", ["markdown"] ) - preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) + preferred_markup_kind = utils.choose_markup_kind(supported_markup_kinds) s = signatures[0] @@ -42,7 +42,7 @@ def pylsp_signature_help(config, document, position): function_sig = " ".join([line.strip() for line in function_sig_lines]) sig = { "label": function_sig, - "documentation": _utils.format_docstring( + "documentation": utils.format_docstring( s.docstring(raw=True), markup_kind=preferred_markup_kind ), } @@ -52,7 +52,7 @@ def pylsp_signature_help(config, document, position): sig["parameters"] = [ { "label": p.name, - "documentation": _utils.format_docstring( + "documentation": utils.format_docstring( _param_docs(docstring, p.name), markup_kind=preferred_markup_kind ), } diff --git a/pylsp/plugins/yapf_format.py b/pylsp/plugins/yapf_format.py index 4a8111be..78a0a797 100644 --- a/pylsp/plugins/yapf_format.py +++ b/pylsp/plugins/yapf_format.py @@ -10,7 +10,7 @@ import whatthepatch from pylsp import hookimpl -from pylsp._utils import get_eol_chars +from pylsp.utils import get_eol_chars log = logging.getLogger(__name__) diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index 52a22a3e..25e3126a 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -14,7 +14,7 @@ from pylsp_jsonrpc.endpoint import Endpoint from pylsp_jsonrpc.streams import JsonRpcStreamReader, JsonRpcStreamWriter -from . import lsp, _utils, uris +from . import lsp, utils, uris from .config import config from .workspace import Workspace, Document, Notebook, Cell from ._version import __version__ @@ -250,7 +250,7 @@ def m_exit(self, **_kwargs): self._jsonrpc_stream_writer.close() def _match_uri_to_workspace(self, uri): - workspace_uri = _utils.match_uri_to_workspace(uri, self.workspaces) + workspace_uri = utils.match_uri_to_workspace(uri, self.workspaces) return self.workspaces.get(workspace_uri, self.workspace) def _hook(self, hook_name, doc_uri=None, **kwargs): @@ -360,7 +360,7 @@ def m_initialize( def watch_parent_process(pid): # exit when the given pid is not alive - if not _utils.is_process_alive(pid): + if not utils.is_process_alive(pid): log.info("parent process %s is not alive, exiting!", pid) self.m_exit() else: @@ -439,7 +439,7 @@ def highlight(self, doc_uri, position): def hover(self, doc_uri, position): return self._hook("pylsp_hover", doc_uri, position=position) or {"contents": ""} - @_utils.debounce(LINT_DEBOUNCE_S, keyed_by="doc_uri") + @utils.debounce(LINT_DEBOUNCE_S, keyed_by="doc_uri") def lint(self, doc_uri, is_saved): # Since we're debounced, the document may no longer be open workspace = self._match_uri_to_workspace(doc_uri) diff --git a/pylsp/_utils.py b/pylsp/utils.py similarity index 100% rename from pylsp/_utils.py rename to pylsp/utils.py diff --git a/pylsp/workspace.py b/pylsp/workspace.py index 5c6880c9..e87b0300 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -13,7 +13,7 @@ import jedi -from . import lsp, uris, _utils +from . import lsp, uris, utils log = logging.getLogger(__name__) @@ -339,7 +339,7 @@ def show_message(self, message, msg_type=lsp.MessageType.Info): def source_roots(self, document_path): """Return the source roots for the given document.""" files = ( - _utils.find_parents( + utils.find_parents( self._root_path, document_path, ["setup.py", "pyproject.toml"] ) or [] @@ -406,7 +406,7 @@ def __init__( self.uri = uri self.version = version self.path = uris.to_fs_path(uri) - self.dot_path = _utils.path_to_dot_name(self.path) + self.dot_path = utils.path_to_dot_name(self.path) self.filename = os.path.basename(self.path) self.shared_data = {} @@ -567,7 +567,7 @@ def jedi_script(self, position=None, use_document_path=False): if position: # Deprecated by Jedi to use in Script() constructor - kwargs += _utils.position_to_jedi_linecolumn(self, position) + kwargs += utils.position_to_jedi_linecolumn(self, position) return jedi.Script(**kwargs) diff --git a/test/plugins/test_completion.py b/test/plugins/test_completion.py index 00a54eb7..6ccff821 100644 --- a/test/plugins/test_completion.py +++ b/test/plugins/test_completion.py @@ -17,7 +17,7 @@ pylsp_completion_item_resolve as pylsp_jedi_completion_item_resolve, ) from pylsp.plugins.rope_completion import pylsp_completions as pylsp_rope_completions -from pylsp._utils import JEDI_VERSION +from pylsp.utils import JEDI_VERSION PY2 = sys.version[0] == "2" diff --git a/test/test_utils.py b/test/test_utils.py index 8b518d72..b77f1f4e 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -12,7 +12,7 @@ from flaky import flaky from docstring_to_markdown import UnknownFormatError -from pylsp import _utils +from pylsp import utils from pylsp.lsp import NotebookCellKind from pylsp.python_lsp import PythonLSPServer, start_io_lang_server @@ -128,7 +128,7 @@ def test_debounce(): interval = 0.1 obj = mock.Mock() - @_utils.debounce(0.1) + @utils.debounce(0.1) def call_m(): obj() @@ -152,7 +152,7 @@ def test_debounce_keyed_by(): interval = 0.1 obj = mock.Mock() - @_utils.debounce(0.1, keyed_by="key") + @utils.debounce(0.1, keyed_by="key") def call_m(key): obj(key) @@ -182,8 +182,8 @@ def call_m(key): def test_list_to_string(): - assert _utils.list_to_string("string") == "string" - assert _utils.list_to_string(["a", "r", "r", "a", "y"]) == "a,r,r,a,y" + assert utils.list_to_string("string") == "string" + assert utils.list_to_string(["a", "r", "r", "a", "y"]) == "a,r,r,a,y" def test_find_parents(tmpdir): @@ -191,28 +191,28 @@ def test_find_parents(tmpdir): path = subsubdir.ensure("path.py") test_cfg = tmpdir.ensure("test.cfg") - assert _utils.find_parents(tmpdir.strpath, path.strpath, ["test.cfg"]) == [ + assert utils.find_parents(tmpdir.strpath, path.strpath, ["test.cfg"]) == [ test_cfg.strpath ] def test_merge_dicts(): - assert _utils.merge_dicts( + assert utils.merge_dicts( {"a": True, "b": {"x": 123, "y": {"hello": "world"}}}, {"a": False, "b": {"y": [], "z": 987}}, ) == {"a": False, "b": {"x": 123, "y": [], "z": 987}} def test_clip_column(): - assert _utils.clip_column(0, [], 0) == 0 - assert _utils.clip_column(2, ["123"], 0) == 2 - assert _utils.clip_column(3, ["123"], 0) == 3 - assert _utils.clip_column(5, ["123"], 0) == 3 - assert _utils.clip_column(0, ["\n", "123"], 0) == 0 - assert _utils.clip_column(1, ["\n", "123"], 0) == 0 - assert _utils.clip_column(2, ["123\n", "123"], 0) == 2 - assert _utils.clip_column(3, ["123\n", "123"], 0) == 3 - assert _utils.clip_column(4, ["123\n", "123"], 1) == 3 + assert utils.clip_column(0, [], 0) == 0 + assert utils.clip_column(2, ["123"], 0) == 2 + assert utils.clip_column(3, ["123"], 0) == 3 + assert utils.clip_column(5, ["123"], 0) == 3 + assert utils.clip_column(0, ["\n", "123"], 0) == 0 + assert utils.clip_column(1, ["\n", "123"], 0) == 0 + assert utils.clip_column(2, ["123\n", "123"], 0) == 2 + assert utils.clip_column(3, ["123\n", "123"], 0) == 3 + assert utils.clip_column(4, ["123\n", "123"], 1) == 3 @mock.patch("docstring_to_markdown.convert") @@ -233,14 +233,14 @@ def test_format_docstring_valid_rst_signature(mock_convert): - `a`: str, something """ - markdown = _utils.format_docstring( + markdown = utils.format_docstring( docstring, "markdown", ["something(a: str) -> str"], )["value"] assert markdown.startswith( - _utils.wrap_signature("something(a: str) -> str"), + utils.wrap_signature("something(a: str) -> str"), ) @@ -254,12 +254,12 @@ def test_format_docstring_invalid_rst_signature(_): a : str, something """ - markdown = _utils.format_docstring( + markdown = utils.format_docstring( docstring, "markdown", ["something(a: str) -> str"], )["value"] assert markdown.startswith( - _utils.wrap_signature("something(a: str) -> str"), + utils.wrap_signature("something(a: str) -> str"), ) From 3e508b11b3694a3f46699de560cb1289eab13caa Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 6 Nov 2023 16:21:55 -0500 Subject: [PATCH 22/23] Revert "Rename `_utils` module to `utils`" (#481) --- pylsp/{utils.py => _utils.py} | 0 pylsp/config/config.py | 16 ++++++------- pylsp/config/flake8_conf.py | 2 +- pylsp/config/pycodestyle_conf.py | 2 +- pylsp/plugins/autopep8_format.py | 2 +- pylsp/plugins/definition.py | 4 ++-- pylsp/plugins/highlight.py | 4 ++-- pylsp/plugins/hover.py | 8 +++---- pylsp/plugins/jedi_completion.py | 10 ++++---- pylsp/plugins/jedi_rename.py | 6 ++--- pylsp/plugins/pycodestyle_lint.py | 2 +- pylsp/plugins/references.py | 4 ++-- pylsp/plugins/rope_completion.py | 8 +++---- pylsp/plugins/rope_rename.py | 4 ++-- pylsp/plugins/signature.py | 10 ++++---- pylsp/plugins/yapf_format.py | 2 +- pylsp/python_lsp.py | 8 +++---- pylsp/workspace.py | 8 +++---- test/plugins/test_completion.py | 2 +- test/test_utils.py | 40 +++++++++++++++---------------- 20 files changed, 71 insertions(+), 71 deletions(-) rename pylsp/{utils.py => _utils.py} (100%) diff --git a/pylsp/utils.py b/pylsp/_utils.py similarity index 100% rename from pylsp/utils.py rename to pylsp/_utils.py diff --git a/pylsp/config/config.py b/pylsp/config/config.py index cae6b374..454ee4b3 100644 --- a/pylsp/config/config.py +++ b/pylsp/config/config.py @@ -10,7 +10,7 @@ import pluggy from pluggy._hooks import HookImpl -from pylsp import utils, hookspecs, uris, PYLSP +from pylsp import _utils, hookspecs, uris, PYLSP # See compatibility note on `group` keyword: # https://docs.python.org/3/library/importlib.metadata.html#entry-points @@ -94,11 +94,11 @@ def __init__(self, root_uri, init_opts, process_id, capabilities): log.info("Loaded pylsp plugin %s from %s", name, plugin) for plugin_conf in self._pm.hook.pylsp_settings(config=self): - self._plugin_settings = utils.merge_dicts( + self._plugin_settings = _utils.merge_dicts( self._plugin_settings, plugin_conf ) - self._plugin_settings = utils.merge_dicts( + self._plugin_settings = _utils.merge_dicts( self._plugin_settings, self._init_opts.get("pylsp", {}) ) @@ -144,10 +144,10 @@ def settings(self, document_path=None): sources = self._settings.get("configurationSources", DEFAULT_CONFIG_SOURCES) # Plugin configuration - settings = utils.merge_dicts(settings, self._plugin_settings) + settings = _utils.merge_dicts(settings, self._plugin_settings) # LSP configuration - settings = utils.merge_dicts(settings, self._settings) + settings = _utils.merge_dicts(settings, self._settings) # User configuration for source_name in reversed(sources): @@ -158,7 +158,7 @@ def settings(self, document_path=None): log.debug( "Got user config from %s: %s", source.__class__.__name__, source_conf ) - settings = utils.merge_dicts(settings, source_conf) + settings = _utils.merge_dicts(settings, source_conf) # Project configuration for source_name in reversed(sources): @@ -169,7 +169,7 @@ def settings(self, document_path=None): log.debug( "Got project config from %s: %s", source.__class__.__name__, source_conf ) - settings = utils.merge_dicts(settings, source_conf) + settings = _utils.merge_dicts(settings, source_conf) log.debug("With configuration: %s", settings) @@ -177,7 +177,7 @@ def settings(self, document_path=None): def find_parents(self, path, names): root_path = uris.to_fs_path(self._root_uri) - return utils.find_parents(root_path, path, names) + return _utils.find_parents(root_path, path, names) def plugin_settings(self, plugin, document_path=None): return ( diff --git a/pylsp/config/flake8_conf.py b/pylsp/config/flake8_conf.py index 4514eb53..ca3b199c 100644 --- a/pylsp/config/flake8_conf.py +++ b/pylsp/config/flake8_conf.py @@ -3,7 +3,7 @@ import logging import os -from pylsp.utils import find_parents +from pylsp._utils import find_parents from .source import ConfigSource log = logging.getLogger(__name__) diff --git a/pylsp/config/pycodestyle_conf.py b/pylsp/config/pycodestyle_conf.py index c324ec46..98d8a1b1 100644 --- a/pylsp/config/pycodestyle_conf.py +++ b/pylsp/config/pycodestyle_conf.py @@ -2,7 +2,7 @@ # Copyright 2021- Python Language Server Contributors. import pycodestyle -from pylsp.utils import find_parents +from pylsp._utils import find_parents from .source import ConfigSource diff --git a/pylsp/plugins/autopep8_format.py b/pylsp/plugins/autopep8_format.py index 9710a86a..1ae3e5f1 100644 --- a/pylsp/plugins/autopep8_format.py +++ b/pylsp/plugins/autopep8_format.py @@ -7,7 +7,7 @@ from autopep8 import fix_code, continued_indentation as autopep8_c_i from pylsp import hookimpl -from pylsp.utils import get_eol_chars +from pylsp._utils import get_eol_chars log = logging.getLogger(__name__) diff --git a/pylsp/plugins/definition.py b/pylsp/plugins/definition.py index 73d156a9..53eda915 100644 --- a/pylsp/plugins/definition.py +++ b/pylsp/plugins/definition.py @@ -6,7 +6,7 @@ import jedi -from pylsp import hookimpl, uris, utils +from pylsp import hookimpl, uris, _utils if TYPE_CHECKING: from jedi.api import Script @@ -44,7 +44,7 @@ def pylsp_definitions( config: Config, document: Document, position: Dict[str, int] ) -> List[Dict[str, Any]]: settings = config.plugin_settings("jedi_definition") - code_position = utils.position_to_jedi_linecolumn(document, position) + code_position = _utils.position_to_jedi_linecolumn(document, position) script = document.jedi_script(use_document_path=True) auto_import_modules = jedi.settings.auto_import_modules diff --git a/pylsp/plugins/highlight.py b/pylsp/plugins/highlight.py index 0ed040e4..0dd896c6 100644 --- a/pylsp/plugins/highlight.py +++ b/pylsp/plugins/highlight.py @@ -2,14 +2,14 @@ # Copyright 2021- Python Language Server Contributors. import logging -from pylsp import hookimpl, lsp, utils +from pylsp import hookimpl, lsp, _utils log = logging.getLogger(__name__) @hookimpl def pylsp_document_highlight(document, position): - code_position = utils.position_to_jedi_linecolumn(document, position) + code_position = _utils.position_to_jedi_linecolumn(document, position) usages = document.jedi_script().get_references(**code_position) def is_valid(definition): diff --git a/pylsp/plugins/hover.py b/pylsp/plugins/hover.py index cccfdfbc..ae07b3dc 100644 --- a/pylsp/plugins/hover.py +++ b/pylsp/plugins/hover.py @@ -3,14 +3,14 @@ import logging -from pylsp import hookimpl, utils +from pylsp import hookimpl, _utils log = logging.getLogger(__name__) @hookimpl def pylsp_hover(config, document, position): - code_position = utils.position_to_jedi_linecolumn(document, position) + code_position = _utils.position_to_jedi_linecolumn(document, position) definitions = document.jedi_script(use_document_path=True).infer(**code_position) word = document.word_at_position(position) @@ -28,7 +28,7 @@ def pylsp_hover(config, document, position): hover_capabilities = config.capabilities.get("textDocument", {}).get("hover", {}) supported_markup_kinds = hover_capabilities.get("contentFormat", ["markdown"]) - preferred_markup_kind = utils.choose_markup_kind(supported_markup_kinds) + preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) # Find first exact matching signature signature = next( @@ -41,7 +41,7 @@ def pylsp_hover(config, document, position): ) return { - "contents": utils.format_docstring( + "contents": _utils.format_docstring( # raw docstring returns only doc, without signature definition.docstring(raw=True), preferred_markup_kind, diff --git a/pylsp/plugins/jedi_completion.py b/pylsp/plugins/jedi_completion.py index 324a127f..2ecf0bec 100644 --- a/pylsp/plugins/jedi_completion.py +++ b/pylsp/plugins/jedi_completion.py @@ -6,7 +6,7 @@ import parso -from pylsp import utils, hookimpl, lsp +from pylsp import _utils, hookimpl, lsp from pylsp.plugins._resolvers import LABEL_RESOLVER, SNIPPET_RESOLVER log = logging.getLogger(__name__) @@ -41,7 +41,7 @@ def pylsp_completions(config, document, position): # pylint: disable=too-many-locals settings = config.plugin_settings("jedi_completion", document_path=document.path) resolve_eagerly = settings.get("eager", False) - code_position = utils.position_to_jedi_linecolumn(document, position) + code_position = _utils.position_to_jedi_linecolumn(document, position) code_position["fuzzy"] = settings.get("fuzzy", False) completions = document.jedi_script(use_document_path=True).complete(**code_position) @@ -55,7 +55,7 @@ def pylsp_completions(config, document, position): item_capabilities = completion_capabilities.get("completionItem", {}) snippet_support = item_capabilities.get("snippetSupport") supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"]) - preferred_markup_kind = utils.choose_markup_kind(supported_markup_kinds) + preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) should_include_params = settings.get("include_params") should_include_class_objects = settings.get("include_class_objects", False) @@ -146,7 +146,7 @@ def pylsp_completion_item_resolve(config, completion_item, document): ) item_capabilities = completion_capabilities.get("completionItem", {}) supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"]) - preferred_markup_kind = utils.choose_markup_kind(supported_markup_kinds) + preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) if shared_data: completion, data = shared_data @@ -209,7 +209,7 @@ def _resolve_completion(completion, d, markup_kind: str): # pylint: disable=broad-except completion["detail"] = _detail(d) try: - docs = utils.format_docstring( + docs = _utils.format_docstring( d.docstring(raw=True), signatures=[signature.to_string() for signature in d.get_signatures()], markup_kind=markup_kind, diff --git a/pylsp/plugins/jedi_rename.py b/pylsp/plugins/jedi_rename.py index 65ae7594..9c89c1de 100644 --- a/pylsp/plugins/jedi_rename.py +++ b/pylsp/plugins/jedi_rename.py @@ -3,7 +3,7 @@ import logging -from pylsp import hookimpl, uris, utils +from pylsp import hookimpl, uris, _utils log = logging.getLogger(__name__) @@ -15,7 +15,7 @@ def pylsp_rename( log.debug( "Executing rename of %s to %s", document.word_at_position(position), new_name ) - kwargs = utils.position_to_jedi_linecolumn(document, position) + kwargs = _utils.position_to_jedi_linecolumn(document, position) kwargs["new_name"] = new_name try: refactoring = document.jedi_script().rename(**kwargs) @@ -54,6 +54,6 @@ def pylsp_rename( def _num_lines(file_contents): "Count the number of lines in the given string." - if utils.get_eol_chars(file_contents): + if _utils.get_eol_chars(file_contents): return len(file_contents.splitlines()) return 0 diff --git a/pylsp/plugins/pycodestyle_lint.py b/pylsp/plugins/pycodestyle_lint.py index 83dc7ddd..62b0b8ad 100644 --- a/pylsp/plugins/pycodestyle_lint.py +++ b/pylsp/plugins/pycodestyle_lint.py @@ -6,7 +6,7 @@ import pycodestyle from pylsp import hookimpl, lsp -from pylsp.utils import get_eol_chars +from pylsp._utils import get_eol_chars try: from autopep8 import continued_indentation as autopep8_c_i diff --git a/pylsp/plugins/references.py b/pylsp/plugins/references.py index b05c76b0..fadf1de8 100644 --- a/pylsp/plugins/references.py +++ b/pylsp/plugins/references.py @@ -2,14 +2,14 @@ # Copyright 2021- Python Language Server Contributors. import logging -from pylsp import hookimpl, uris, utils +from pylsp import hookimpl, uris, _utils log = logging.getLogger(__name__) @hookimpl def pylsp_references(document, position, exclude_declaration): - code_position = utils.position_to_jedi_linecolumn(document, position) + code_position = _utils.position_to_jedi_linecolumn(document, position) usages = document.jedi_script().get_references(**code_position) if exclude_declaration: diff --git a/pylsp/plugins/rope_completion.py b/pylsp/plugins/rope_completion.py index b30458c6..ca0d4349 100644 --- a/pylsp/plugins/rope_completion.py +++ b/pylsp/plugins/rope_completion.py @@ -4,7 +4,7 @@ import logging from rope.contrib.codeassist import code_assist, sorted_proposals -from pylsp import utils, hookimpl, lsp +from pylsp import _utils, hookimpl, lsp log = logging.getLogger(__name__) @@ -19,7 +19,7 @@ def pylsp_settings(): def _resolve_completion(completion, data, markup_kind): # pylint: disable=broad-except try: - doc = utils.format_docstring(data.get_doc(), markup_kind=markup_kind) + doc = _utils.format_docstring(data.get_doc(), markup_kind=markup_kind) except Exception as e: log.debug("Failed to resolve Rope completion: %s", e) doc = "" @@ -57,7 +57,7 @@ def pylsp_completions(config, workspace, document, position): ) item_capabilities = completion_capabilities.get("completionItem", {}) supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"]) - preferred_markup_kind = utils.choose_markup_kind(supported_markup_kinds) + preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) try: definitions = code_assist( @@ -104,7 +104,7 @@ def pylsp_completion_item_resolve(config, completion_item, document): ) item_capabilities = completion_capabilities.get("completionItem", {}) supported_markup_kinds = item_capabilities.get("documentationFormat", ["markdown"]) - preferred_markup_kind = utils.choose_markup_kind(supported_markup_kinds) + preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) if shared_data: completion, data = shared_data diff --git a/pylsp/plugins/rope_rename.py b/pylsp/plugins/rope_rename.py index d0873e69..a4323a42 100644 --- a/pylsp/plugins/rope_rename.py +++ b/pylsp/plugins/rope_rename.py @@ -6,7 +6,7 @@ from rope.base import libutils from rope.refactor.rename import Rename -from pylsp import hookimpl, uris, utils +from pylsp import hookimpl, uris, _utils log = logging.getLogger(__name__) @@ -61,6 +61,6 @@ def _num_lines(resource): "Count the number of lines in a `File` resource." text = resource.read() - if utils.get_eol_chars(text): + if _utils.get_eol_chars(text): return len(text.splitlines()) return 0 diff --git a/pylsp/plugins/signature.py b/pylsp/plugins/signature.py index 0e625b27..4fc93dfb 100644 --- a/pylsp/plugins/signature.py +++ b/pylsp/plugins/signature.py @@ -3,7 +3,7 @@ import logging import re -from pylsp import hookimpl, utils +from pylsp import hookimpl, _utils log = logging.getLogger(__name__) @@ -16,7 +16,7 @@ @hookimpl def pylsp_signature_help(config, document, position): - code_position = utils.position_to_jedi_linecolumn(document, position) + code_position = _utils.position_to_jedi_linecolumn(document, position) signatures = document.jedi_script().get_signatures(**code_position) if not signatures: @@ -31,7 +31,7 @@ def pylsp_signature_help(config, document, position): supported_markup_kinds = signature_information_support.get( "documentationFormat", ["markdown"] ) - preferred_markup_kind = utils.choose_markup_kind(supported_markup_kinds) + preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) s = signatures[0] @@ -42,7 +42,7 @@ def pylsp_signature_help(config, document, position): function_sig = " ".join([line.strip() for line in function_sig_lines]) sig = { "label": function_sig, - "documentation": utils.format_docstring( + "documentation": _utils.format_docstring( s.docstring(raw=True), markup_kind=preferred_markup_kind ), } @@ -52,7 +52,7 @@ def pylsp_signature_help(config, document, position): sig["parameters"] = [ { "label": p.name, - "documentation": utils.format_docstring( + "documentation": _utils.format_docstring( _param_docs(docstring, p.name), markup_kind=preferred_markup_kind ), } diff --git a/pylsp/plugins/yapf_format.py b/pylsp/plugins/yapf_format.py index 78a0a797..4a8111be 100644 --- a/pylsp/plugins/yapf_format.py +++ b/pylsp/plugins/yapf_format.py @@ -10,7 +10,7 @@ import whatthepatch from pylsp import hookimpl -from pylsp.utils import get_eol_chars +from pylsp._utils import get_eol_chars log = logging.getLogger(__name__) diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index 25e3126a..52a22a3e 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -14,7 +14,7 @@ from pylsp_jsonrpc.endpoint import Endpoint from pylsp_jsonrpc.streams import JsonRpcStreamReader, JsonRpcStreamWriter -from . import lsp, utils, uris +from . import lsp, _utils, uris from .config import config from .workspace import Workspace, Document, Notebook, Cell from ._version import __version__ @@ -250,7 +250,7 @@ def m_exit(self, **_kwargs): self._jsonrpc_stream_writer.close() def _match_uri_to_workspace(self, uri): - workspace_uri = utils.match_uri_to_workspace(uri, self.workspaces) + workspace_uri = _utils.match_uri_to_workspace(uri, self.workspaces) return self.workspaces.get(workspace_uri, self.workspace) def _hook(self, hook_name, doc_uri=None, **kwargs): @@ -360,7 +360,7 @@ def m_initialize( def watch_parent_process(pid): # exit when the given pid is not alive - if not utils.is_process_alive(pid): + if not _utils.is_process_alive(pid): log.info("parent process %s is not alive, exiting!", pid) self.m_exit() else: @@ -439,7 +439,7 @@ def highlight(self, doc_uri, position): def hover(self, doc_uri, position): return self._hook("pylsp_hover", doc_uri, position=position) or {"contents": ""} - @utils.debounce(LINT_DEBOUNCE_S, keyed_by="doc_uri") + @_utils.debounce(LINT_DEBOUNCE_S, keyed_by="doc_uri") def lint(self, doc_uri, is_saved): # Since we're debounced, the document may no longer be open workspace = self._match_uri_to_workspace(doc_uri) diff --git a/pylsp/workspace.py b/pylsp/workspace.py index e87b0300..5c6880c9 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -13,7 +13,7 @@ import jedi -from . import lsp, uris, utils +from . import lsp, uris, _utils log = logging.getLogger(__name__) @@ -339,7 +339,7 @@ def show_message(self, message, msg_type=lsp.MessageType.Info): def source_roots(self, document_path): """Return the source roots for the given document.""" files = ( - utils.find_parents( + _utils.find_parents( self._root_path, document_path, ["setup.py", "pyproject.toml"] ) or [] @@ -406,7 +406,7 @@ def __init__( self.uri = uri self.version = version self.path = uris.to_fs_path(uri) - self.dot_path = utils.path_to_dot_name(self.path) + self.dot_path = _utils.path_to_dot_name(self.path) self.filename = os.path.basename(self.path) self.shared_data = {} @@ -567,7 +567,7 @@ def jedi_script(self, position=None, use_document_path=False): if position: # Deprecated by Jedi to use in Script() constructor - kwargs += utils.position_to_jedi_linecolumn(self, position) + kwargs += _utils.position_to_jedi_linecolumn(self, position) return jedi.Script(**kwargs) diff --git a/test/plugins/test_completion.py b/test/plugins/test_completion.py index 6ccff821..00a54eb7 100644 --- a/test/plugins/test_completion.py +++ b/test/plugins/test_completion.py @@ -17,7 +17,7 @@ pylsp_completion_item_resolve as pylsp_jedi_completion_item_resolve, ) from pylsp.plugins.rope_completion import pylsp_completions as pylsp_rope_completions -from pylsp.utils import JEDI_VERSION +from pylsp._utils import JEDI_VERSION PY2 = sys.version[0] == "2" diff --git a/test/test_utils.py b/test/test_utils.py index b77f1f4e..8b518d72 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -12,7 +12,7 @@ from flaky import flaky from docstring_to_markdown import UnknownFormatError -from pylsp import utils +from pylsp import _utils from pylsp.lsp import NotebookCellKind from pylsp.python_lsp import PythonLSPServer, start_io_lang_server @@ -128,7 +128,7 @@ def test_debounce(): interval = 0.1 obj = mock.Mock() - @utils.debounce(0.1) + @_utils.debounce(0.1) def call_m(): obj() @@ -152,7 +152,7 @@ def test_debounce_keyed_by(): interval = 0.1 obj = mock.Mock() - @utils.debounce(0.1, keyed_by="key") + @_utils.debounce(0.1, keyed_by="key") def call_m(key): obj(key) @@ -182,8 +182,8 @@ def call_m(key): def test_list_to_string(): - assert utils.list_to_string("string") == "string" - assert utils.list_to_string(["a", "r", "r", "a", "y"]) == "a,r,r,a,y" + assert _utils.list_to_string("string") == "string" + assert _utils.list_to_string(["a", "r", "r", "a", "y"]) == "a,r,r,a,y" def test_find_parents(tmpdir): @@ -191,28 +191,28 @@ def test_find_parents(tmpdir): path = subsubdir.ensure("path.py") test_cfg = tmpdir.ensure("test.cfg") - assert utils.find_parents(tmpdir.strpath, path.strpath, ["test.cfg"]) == [ + assert _utils.find_parents(tmpdir.strpath, path.strpath, ["test.cfg"]) == [ test_cfg.strpath ] def test_merge_dicts(): - assert utils.merge_dicts( + assert _utils.merge_dicts( {"a": True, "b": {"x": 123, "y": {"hello": "world"}}}, {"a": False, "b": {"y": [], "z": 987}}, ) == {"a": False, "b": {"x": 123, "y": [], "z": 987}} def test_clip_column(): - assert utils.clip_column(0, [], 0) == 0 - assert utils.clip_column(2, ["123"], 0) == 2 - assert utils.clip_column(3, ["123"], 0) == 3 - assert utils.clip_column(5, ["123"], 0) == 3 - assert utils.clip_column(0, ["\n", "123"], 0) == 0 - assert utils.clip_column(1, ["\n", "123"], 0) == 0 - assert utils.clip_column(2, ["123\n", "123"], 0) == 2 - assert utils.clip_column(3, ["123\n", "123"], 0) == 3 - assert utils.clip_column(4, ["123\n", "123"], 1) == 3 + assert _utils.clip_column(0, [], 0) == 0 + assert _utils.clip_column(2, ["123"], 0) == 2 + assert _utils.clip_column(3, ["123"], 0) == 3 + assert _utils.clip_column(5, ["123"], 0) == 3 + assert _utils.clip_column(0, ["\n", "123"], 0) == 0 + assert _utils.clip_column(1, ["\n", "123"], 0) == 0 + assert _utils.clip_column(2, ["123\n", "123"], 0) == 2 + assert _utils.clip_column(3, ["123\n", "123"], 0) == 3 + assert _utils.clip_column(4, ["123\n", "123"], 1) == 3 @mock.patch("docstring_to_markdown.convert") @@ -233,14 +233,14 @@ def test_format_docstring_valid_rst_signature(mock_convert): - `a`: str, something """ - markdown = utils.format_docstring( + markdown = _utils.format_docstring( docstring, "markdown", ["something(a: str) -> str"], )["value"] assert markdown.startswith( - utils.wrap_signature("something(a: str) -> str"), + _utils.wrap_signature("something(a: str) -> str"), ) @@ -254,12 +254,12 @@ def test_format_docstring_invalid_rst_signature(_): a : str, something """ - markdown = utils.format_docstring( + markdown = _utils.format_docstring( docstring, "markdown", ["something(a: str) -> str"], )["value"] assert markdown.startswith( - utils.wrap_signature("something(a: str) -> str"), + _utils.wrap_signature("something(a: str) -> str"), ) From 4428a656c4271580d2720df9962f4e5a5669c026 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 6 Nov 2023 16:42:51 -0500 Subject: [PATCH 23/23] Update changelog for 1.9.0 (#482) --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb9c7957..8c35d251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # History of changes +## Version 1.9.0 (2023/11/06) + +### Issues Closed + +* [Issue 460](https://github.com/python-lsp/python-lsp-server/issues/460) - rope_autoimport doesn't initialize after `workspace/didChangeConfiguration` message ([PR 461](https://github.com/python-lsp/python-lsp-server/pull/461) by [@tkrabel-db](https://github.com/tkrabel-db)) +* [Issue 403](https://github.com/python-lsp/python-lsp-server/issues/403) - Add code action for implementing auto-import ([PR 471](https://github.com/python-lsp/python-lsp-server/pull/471) by [@tkrabel-db](https://github.com/tkrabel-db)) +* [Issue 195](https://github.com/python-lsp/python-lsp-server/issues/195) - Maybe use initializationOptions as additional source of settings ([PR 459](https://github.com/python-lsp/python-lsp-server/pull/459) by [@tkrabel-db](https://github.com/tkrabel-db)) + +In this release 3 issues were closed. + +### Pull Requests Merged + +* [PR 481](https://github.com/python-lsp/python-lsp-server/pull/481) - Revert "Rename `_utils` module to `utils`", by [@ccordoba12](https://github.com/ccordoba12) +* [PR 480](https://github.com/python-lsp/python-lsp-server/pull/480) - Rename `_utils` module to `utils`, by [@ccordoba12](https://github.com/ccordoba12) +* [PR 475](https://github.com/python-lsp/python-lsp-server/pull/475) - Raise supported Pylint upper version, by [@bnavigator](https://github.com/bnavigator) +* [PR 473](https://github.com/python-lsp/python-lsp-server/pull/473) - Improve/simplify README Development section, by [@tkrabel](https://github.com/tkrabel) +* [PR 471](https://github.com/python-lsp/python-lsp-server/pull/471) - Add code completions to `rope_autoimport` plugin, by [@tkrabel-db](https://github.com/tkrabel-db) ([403](https://github.com/python-lsp/python-lsp-server/issues/403)) +* [PR 469](https://github.com/python-lsp/python-lsp-server/pull/469) - Pass argument `extendIgnore` to flake8, by [@UnkwUsr](https://github.com/UnkwUsr) +* [PR 466](https://github.com/python-lsp/python-lsp-server/pull/466) - Ignore notebook names on cell completion for autoimport, by [@tkrabel-db](https://github.com/tkrabel-db) +* [PR 464](https://github.com/python-lsp/python-lsp-server/pull/464) - Minor bug fix in Rope autoimport plugin, by [@tkrabel-db](https://github.com/tkrabel-db) +* [PR 462](https://github.com/python-lsp/python-lsp-server/pull/462) - Make workspace/didChangeConfig work with notebook documents, by [@tkrabel-db](https://github.com/tkrabel-db) +* [PR 461](https://github.com/python-lsp/python-lsp-server/pull/461) - Load `rope_autoimport` cache on `workspace/didChangeConfiguration`, by [@tkrabel-db](https://github.com/tkrabel-db) ([460](https://github.com/python-lsp/python-lsp-server/issues/460)) +* [PR 459](https://github.com/python-lsp/python-lsp-server/pull/459) - Support `initializationOptions` to configure the server, by [@tkrabel-db](https://github.com/tkrabel-db) ([195](https://github.com/python-lsp/python-lsp-server/issues/195)) +* [PR 457](https://github.com/python-lsp/python-lsp-server/pull/457) - Fix missing signatures for docstrings in Markdown, by [@staticf0x](https://github.com/staticf0x) + +In this release 12 pull requests were closed. + +---- + ## Version 1.8.2 (2023/10/09) ### Issues Closed