From 60c310125a99194df2fdbd7d342b6953c8b81aa3 Mon Sep 17 00:00:00 2001 From: bageljr Date: Sat, 9 Apr 2022 15:40:04 -0500 Subject: [PATCH 01/39] initial autoimport work --- pylsp/config/schema.json | 4 ++++ pylsp/plugins/rope_autoimport.py | 32 ++++++++++++++++++++++++++++++++ setup.cfg | 1 + 3 files changed, 37 insertions(+) create mode 100644 pylsp/plugins/rope_autoimport.py diff --git a/pylsp/config/schema.json b/pylsp/config/schema.json index c29d78bd..550e7756 100644 --- a/pylsp/config/schema.json +++ b/pylsp/config/schema.json @@ -335,6 +335,10 @@ "type": "boolean", "default": true, "description": "Enable or disable the plugin." + },"pylsp.plugins.rope_autoimport.enabled": { + "type": "boolean", + "default": true, + "description": "Enable or disable the plugin." }, "pylsp.plugins.rope_completion.eager": { "type": "boolean", diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py new file mode 100644 index 00000000..fb0d77fc --- /dev/null +++ b/pylsp/plugins/rope_autoimport.py @@ -0,0 +1,32 @@ +import logging + +from rope.contrib.autoimport import AutoImport + +from pylsp import hookimpl, lsp +from pylsp.config.config import Config +from pylsp.workspace import Workspace + +log = logging.getLogger(__name__) + + +@hookimpl +def pylsp_settings(): + # Default rope_completion to disabled + return {"plugins": {"rope_autoimport": {"enabled": True}}} + + +@hookimpl +def pylsp_completions(config: Config, workspace: Workspace, document, position): + rope_config = config.settings(document_path=document.path).get("rope", {}) + rope_project = workspace._rope_project_builder(rope_config) + autoimport = AutoImport(rope_project, memory=False) + autoimport.close() + + +@hookimpl +def pylsp_initialize(config: Config, workspace: Workspace): + rope_config = config.settings().get("rope", {}) + rope_project = workspace._rope_project_builder(rope_config) + autoimport = AutoImport(rope_project, memory=False) + autoimport.generate_modules_cache() + autoimport.close() diff --git a/setup.cfg b/setup.cfg index 17db54d9..82ea9fe6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -75,6 +75,7 @@ pylsp = pylint = pylsp.plugins.pylint_lint rope_completion = pylsp.plugins.rope_completion rope_rename = pylsp.plugins.rope_rename + rope_autoimport = pylsp.plugins.rope_autoimport yapf = pylsp.plugins.yapf_format [pycodestyle] From f71f4e62f4818e9f34183e2bae5da5fc73d5590c Mon Sep 17 00:00:00 2001 From: bageljr Date: Sat, 16 Apr 2022 21:32:27 -0500 Subject: [PATCH 02/39] provide suggestions --- pylsp/plugins/rope_autoimport.py | 37 ++++++++++++++++++++++++++++++++ test/plugins/test_autoimport.py | 32 +++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 test/plugins/test_autoimport.py diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index fb0d77fc..51f70a43 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -1,4 +1,6 @@ import logging +from collections import OrderedDict +from typing import TypedDict from rope.contrib.autoimport import AutoImport @@ -15,12 +17,47 @@ def pylsp_settings(): return {"plugins": {"rope_autoimport": {"enabled": True}}} +def deduplicate(input): + return list(OrderedDict.fromkeys(input)) + + @hookimpl def pylsp_completions(config: Config, workspace: Workspace, document, position): + word = document.word_at_position(position) + if "." in word: + return [] rope_config = config.settings(document_path=document.path).get("rope", {}) rope_project = workspace._rope_project_builder(rope_config) autoimport = AutoImport(rope_project, memory=False) + autoimport.generate_modules_cache() + suggestions = deduplicate(autoimport.search_module(word)) + suggestions.extend(deduplicate(autoimport.search_name(word))) + results = [] + for import_statement, name, source, itemkind in suggestions: + item = { + "label": name, + "kind": itemkind, + "sortText": _sort_import(source, import_statement, name, word), + "data": {"doc_uri": document.uri}, + "documentation": _document(import_statement), + } + results.append(item) autoimport.close() + return results + + +def _document(import_statement: str) -> str: + return import_statement + + +def _sort_import( + source: int, full_statement: str, suggested_name: str, desired_name +) -> int: + import_length = len("import") + full_statement_score = 2 * (len(full_statement) - import_length) + suggested_name_score = 5 * (len(suggested_name) - len(desired_name)) + source_score = 20 * source + return source_score + suggested_name_score + full_statement_score @hookimpl diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py new file mode 100644 index 00000000..9d1e5c51 --- /dev/null +++ b/test/plugins/test_autoimport.py @@ -0,0 +1,32 @@ +from tabulate import tabulate + +from pylsp import lsp, uris +from pylsp.plugins.rope_autoimport import ( + pylsp_completions as pylsp_autoimport_completions, +) + +DOC_URI = uris.from_fs_path(__file__) +from typing import Dict, List + + +def check_dict(query: Dict, results: List[Dict]) -> bool: + for result in results: + if all(result[key] == query[key] for key in query.keys()): + return True + return False + + +def test_autoimport_completion(config, workspace): + AUTOIMPORT_DOC = """pa""" + # Over 'i' in os.path.isabs(...) + com_position = {"line": 0, "character": 2} + workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) + doc = workspace.get_document(DOC_URI) + items = pylsp_autoimport_completions(config, workspace, doc, com_position) + + assert items + print(tabulate(items)) + assert check_dict( + {"label": "pathlib", "kind": lsp.CompletionItemKind.Module}, items + ) + assert False From fea86787e17b2caa1530136673bfc9c60192c900 Mon Sep 17 00:00:00 2001 From: bageljr Date: Sat, 16 Apr 2022 21:53:29 -0500 Subject: [PATCH 03/39] use str for sorting --- pylsp/plugins/rope_autoimport.py | 5 +++-- test/plugins/test_autoimport.py | 11 +++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index 51f70a43..843ee918 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -32,6 +32,7 @@ def pylsp_completions(config: Config, workspace: Workspace, document, position): autoimport.generate_modules_cache() suggestions = deduplicate(autoimport.search_module(word)) suggestions.extend(deduplicate(autoimport.search_name(word))) + autoimport.close() results = [] for import_statement, name, source, itemkind in suggestions: item = { @@ -42,7 +43,6 @@ def pylsp_completions(config: Config, workspace: Workspace, document, position): "documentation": _document(import_statement), } results.append(item) - autoimport.close() return results @@ -57,7 +57,7 @@ def _sort_import( full_statement_score = 2 * (len(full_statement) - import_length) suggested_name_score = 5 * (len(suggested_name) - len(desired_name)) source_score = 20 * source - return source_score + suggested_name_score + full_statement_score + return reversed(str(source_score + suggested_name_score + full_statement_score)) @hookimpl @@ -66,4 +66,5 @@ def pylsp_initialize(config: Config, workspace: Workspace): rope_project = workspace._rope_project_builder(rope_config) autoimport = AutoImport(rope_project, memory=False) autoimport.generate_modules_cache() + autoimport.generate_cache() autoimport.close() diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index 9d1e5c51..0716495d 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -1,9 +1,6 @@ -from tabulate import tabulate - from pylsp import lsp, uris -from pylsp.plugins.rope_autoimport import ( - pylsp_completions as pylsp_autoimport_completions, -) +from pylsp.plugins.rope_autoimport import \ + pylsp_completions as pylsp_autoimport_completions DOC_URI = uris.from_fs_path(__file__) from typing import Dict, List @@ -17,7 +14,7 @@ def check_dict(query: Dict, results: List[Dict]) -> bool: def test_autoimport_completion(config, workspace): - AUTOIMPORT_DOC = """pa""" + AUTOIMPORT_DOC = """pathli""" # Over 'i' in os.path.isabs(...) com_position = {"line": 0, "character": 2} workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) @@ -25,8 +22,6 @@ def test_autoimport_completion(config, workspace): items = pylsp_autoimport_completions(config, workspace, doc, com_position) assert items - print(tabulate(items)) assert check_dict( {"label": "pathlib", "kind": lsp.CompletionItemKind.Module}, items ) - assert False From caa0e3c256c1e5e70835f4cb84dc32601d18d808 Mon Sep 17 00:00:00 2001 From: bageljr Date: Sun, 17 Apr 2022 22:32:15 -0500 Subject: [PATCH 04/39] textEdit to actually insert edits --- pylsp/plugins/rope_autoimport.py | 30 ++++++++++---- pylsp/workspace.py | 5 ++- test/plugins/test_autoimport.py | 69 +++++++++++++++++++++++++++++++- 3 files changed, 95 insertions(+), 9 deletions(-) diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index 843ee918..c82f4c98 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -6,7 +6,7 @@ from pylsp import hookimpl, lsp from pylsp.config.config import Config -from pylsp.workspace import Workspace +from pylsp.workspace import Document, Workspace log = logging.getLogger(__name__) @@ -18,46 +18,62 @@ def pylsp_settings(): def deduplicate(input): + """Remove duplicates from list.""" return list(OrderedDict.fromkeys(input)) @hookimpl -def pylsp_completions(config: Config, workspace: Workspace, document, position): +def pylsp_completions( + config: Config, workspace: Workspace, document: Document, position +): + first_word = document.word_at_position( + position={"line": position["line"], "character": 0} + ) word = document.word_at_position(position) - if "." in word: + if first_word in ("import", "from", "#") or "." in word: return [] rope_config = config.settings(document_path=document.path).get("rope", {}) rope_project = workspace._rope_project_builder(rope_config) autoimport = AutoImport(rope_project, memory=False) - autoimport.generate_modules_cache() + # TODO: update cache suggestions = deduplicate(autoimport.search_module(word)) suggestions.extend(deduplicate(autoimport.search_name(word))) autoimport.close() results = [] for import_statement, name, source, itemkind in suggestions: + # insert_line = autoimport.find_insertion_line(document) + # TODO: use isort to handle insertion line correctly + insert_line = 0 + start = {"line": insert_line, "character": 0} + range = {"start": start, "end": start} + edit = {"range": range, "newText": import_statement + "\n"} item = { "label": name, "kind": itemkind, "sortText": _sort_import(source, import_statement, name, word), "data": {"doc_uri": document.uri}, "documentation": _document(import_statement), + "additionalTextEdits": [edit], } results.append(item) return results def _document(import_statement: str) -> str: - return import_statement + return "__autoimport__\n" + import_statement def _sort_import( source: int, full_statement: str, suggested_name: str, desired_name -) -> int: +) -> str: import_length = len("import") full_statement_score = 2 * (len(full_statement) - import_length) suggested_name_score = 5 * (len(suggested_name) - len(desired_name)) source_score = 20 * source - return reversed(str(source_score + suggested_name_score + full_statement_score)) + score: int = source_score + suggested_name_score + full_statement_score + # Since we are using ints, we need to pad them. + # We also want to prioritize autoimport behind everything since its the last priority. + return "zz" + str(score).rjust(10, "0") @hookimpl diff --git a/pylsp/workspace.py b/pylsp/workspace.py index bf312f62..0a448c7b 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -56,7 +56,10 @@ def _rope_project_builder(self, rope_config): # TODO: we could keep track of dirty files and validate only those if self.__rope is None or self.__rope_config != rope_config: rope_folder = rope_config.get('ropeFolder') - self.__rope = Project(self._root_path, ropefolder=rope_folder) + if rope_folder: + self.__rope = Project(self._root_path, ropefolder=rope_folder) + else: + self.__rope = Project(self._root_path) self.__rope.prefs.set('extension_modules', rope_config.get('extensionModules', [])) self.__rope.prefs.set('ignore_syntax_errors', True) self.__rope.prefs.set('ignore_bad_imports', True) diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index 0716495d..d717d21a 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -1,4 +1,5 @@ from pylsp import lsp, uris +from pylsp.plugins.rope_autoimport import _sort_import from pylsp.plugins.rope_autoimport import \ pylsp_completions as pylsp_autoimport_completions @@ -16,7 +17,7 @@ def check_dict(query: Dict, results: List[Dict]) -> bool: def test_autoimport_completion(config, workspace): AUTOIMPORT_DOC = """pathli""" # Over 'i' in os.path.isabs(...) - com_position = {"line": 0, "character": 2} + com_position = {"line": 0, "character": 6} workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) doc = workspace.get_document(DOC_URI) items = pylsp_autoimport_completions(config, workspace, doc, com_position) @@ -25,3 +26,69 @@ def test_autoimport_completion(config, workspace): assert check_dict( {"label": "pathlib", "kind": lsp.CompletionItemKind.Module}, items ) + + +def test_autoimport_import(config, workspace): + AUTOIMPORT_DOC = """import""" + # Over 'i' in os.path.isabs(...) + com_position = {"line": 0, "character": 6} + workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) + doc = workspace.get_document(DOC_URI) + items = pylsp_autoimport_completions(config, workspace, doc, com_position) + + assert len(items) == 0 + + +def test_autoimport_dot(config, workspace): + AUTOIMPORT_DOC = """str.""" + # Over 'i' in os.path.isabs(...) + com_position = {"line": 0, "character": 4} + workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) + doc = workspace.get_document(DOC_URI) + items = pylsp_autoimport_completions(config, workspace, doc, com_position) + + assert len(items) == 0 + + +def test_autoimport_comment(config, workspace): + AUTOIMPORT_DOC = """#""" + # Over 'i' in os.path.isabs(...) + com_position = {"line": 0, "character": 0} + workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) + doc = workspace.get_document(DOC_URI) + items = pylsp_autoimport_completions(config, workspace, doc, com_position) + + assert len(items) == 0 + +def test_autoimport_from(config, workspace): + AUTOIMPORT_DOC = """from""" + # Over 'i' in os.path.isabs(...) + com_position = {"line": 0, "character": 6} + workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) + doc = workspace.get_document(DOC_URI) + items = pylsp_autoimport_completions(config, workspace, doc, com_position) + + + assert len(items) == 0 + + +def test_sort_sources(): + result1 = _sort_import(1, "import pathlib", "pathlib", "pathli") + result2 = _sort_import(2, "import pathlib", "pathlib", "pathli") + assert result1 < result2 + + +def test_sort_statements(): + result1 = _sort_import( + 2, "from importlib_metadata import pathlib", "pathlib", "pathli" + ) + result2 = _sort_import(2, "import pathlib", "pathlib", "pathli") + assert result1 > result2 + + +def test_sort_both(): + result1 = _sort_import( + 3, "from importlib_metadata import pathlib", "pathlib", "pathli" + ) + result2 = _sort_import(2, "import pathlib", "pathlib", "pathli") + assert result1 > result2 From e5bb74c9de02a98dacf2a659f533bf44c2a23177 Mon Sep 17 00:00:00 2001 From: bageljr Date: Mon, 18 Apr 2022 15:58:14 -0500 Subject: [PATCH 05/39] use parso to decide to use autoimport --- pylsp/plugins/rope_autoimport.py | 106 +++++++++++++++++++++++-------- test/plugins/test_autoimport.py | 82 ++++++++++++++++++++---- 2 files changed, 149 insertions(+), 39 deletions(-) diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index c82f4c98..417592e7 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -1,10 +1,13 @@ import logging from collections import OrderedDict -from typing import TypedDict +from typing import Generator, List +import parso +from parso.python import tree +from parso.tree import NodeOrLeaf from rope.contrib.autoimport import AutoImport -from pylsp import hookimpl, lsp +from pylsp import hookimpl from pylsp.config.config import Config from pylsp.workspace import Document, Workspace @@ -17,21 +20,87 @@ def pylsp_settings(): return {"plugins": {"rope_autoimport": {"enabled": True}}} -def deduplicate(input): +def deduplicate(input_list): """Remove duplicates from list.""" - return list(OrderedDict.fromkeys(input)) + return list(OrderedDict.fromkeys(input_list)) + + +def should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: + if len(expr.children) > 0: + first_child = expr.children[0] + if isinstance(first_child, tree.EndMarker): + if "#" in first_child.prefix: + return False + if isinstance( + first_child, + ( + tree.PythonLeaf, + tree.PythonErrorLeaf, + ), + ): + if first_child.value in ("import", "from") and first_child != word_node: + return False + if isinstance(first_child, (tree.PythonErrorNode)): + return should_insert(first_child, word_node) + if isinstance(first_child, tree.Keyword): + if first_child.value == "def": + return _should_import_function(word_node, expr) + return True + + +def _should_import_function(word_node: tree.Leaf, expr: tree.BaseNode) -> bool: + prev_node = None + for node in expr.children: + if _handle_argument(node, word_node): + return True + if isinstance(prev_node, tree.Operator): + if prev_node.value == "->": + if node == word_node: + return True + prev_node = node + return False + + +def _handle_argument(node: NodeOrLeaf, word_node: tree.Leaf): + if isinstance(node, tree.PythonNode): + if node.type == "tfpdef": + if node.children[2] == word_node: + return True + if node.type == "parameters": + for parameter in node.children: + if _handle_argument(parameter, word_node): + return True + return False + + +def _process_statements(suggestions: List, doc_uri: str, word: str) -> Generator: + for import_statement, name, source, itemkind in suggestions: + # insert_line = autoimport.find_insertion_line(document) + # TODO: use isort to handle insertion line correctly + insert_line = 0 + start = {"line": insert_line, "character": 0} + edit_range = {"start": start, "end": start} + edit = {"range": edit_range, "newText": import_statement + "\n"} + yield { + "label": name, + "kind": itemkind, + "sortText": _sort_import(source, import_statement, name, word), + "data": {"doc_uri": doc_uri}, + "documentation": _document(import_statement), + "additionalTextEdits": [edit], + } @hookimpl def pylsp_completions( config: Config, workspace: Workspace, document: Document, position ): - first_word = document.word_at_position( - position={"line": position["line"], "character": 0} - ) - word = document.word_at_position(position) - if first_word in ("import", "from", "#") or "." in word: + line = document.lines[position["line"]] + expr = parso.parse(line) + word_node = expr.get_leaf_for_position((1, position["character"])) + if not should_insert(expr, word_node): return [] + word = word_node.value rope_config = config.settings(document_path=document.path).get("rope", {}) rope_project = workspace._rope_project_builder(rope_config) autoimport = AutoImport(rope_project, memory=False) @@ -39,24 +108,7 @@ def pylsp_completions( suggestions = deduplicate(autoimport.search_module(word)) suggestions.extend(deduplicate(autoimport.search_name(word))) autoimport.close() - results = [] - for import_statement, name, source, itemkind in suggestions: - # insert_line = autoimport.find_insertion_line(document) - # TODO: use isort to handle insertion line correctly - insert_line = 0 - start = {"line": insert_line, "character": 0} - range = {"start": start, "end": start} - edit = {"range": range, "newText": import_statement + "\n"} - item = { - "label": name, - "kind": itemkind, - "sortText": _sort_import(source, import_statement, name, word), - "data": {"doc_uri": document.uri}, - "documentation": _document(import_statement), - "additionalTextEdits": [edit], - } - results.append(item) - return results + return list(_process_statements(suggestions, document.uri, word)) def _document(import_statement: str) -> str: diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index d717d21a..e8957030 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -1,10 +1,11 @@ +from typing import Dict, List + from pylsp import lsp, uris from pylsp.plugins.rope_autoimport import _sort_import from pylsp.plugins.rope_autoimport import \ pylsp_completions as pylsp_autoimport_completions DOC_URI = uris.from_fs_path(__file__) -from typing import Dict, List def check_dict(query: Dict, results: List[Dict]) -> bool: @@ -16,7 +17,6 @@ def check_dict(query: Dict, results: List[Dict]) -> bool: def test_autoimport_completion(config, workspace): AUTOIMPORT_DOC = """pathli""" - # Over 'i' in os.path.isabs(...) com_position = {"line": 0, "character": 6} workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) doc = workspace.get_document(DOC_URI) @@ -29,9 +29,18 @@ def test_autoimport_completion(config, workspace): def test_autoimport_import(config, workspace): - AUTOIMPORT_DOC = """import""" - # Over 'i' in os.path.isabs(...) - com_position = {"line": 0, "character": 6} + AUTOIMPORT_DOC = """import """ + com_position = {"line": 0, "character": 7} + workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) + doc = workspace.get_document(DOC_URI) + items = pylsp_autoimport_completions(config, workspace, doc, com_position) + + assert len(items) == 0 + + +def test_autoimport_function(config, workspace): + AUTOIMPORT_DOC = """def func(s""" + com_position = {"line": 0, "character": 10} workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) doc = workspace.get_document(DOC_URI) items = pylsp_autoimport_completions(config, workspace, doc, com_position) @@ -39,9 +48,40 @@ def test_autoimport_import(config, workspace): assert len(items) == 0 +def test_autoimport_function_typing(config, workspace): + AUTOIMPORT_DOC = """def func(s : Lis """ + com_position = {"line": 0, "character": 16} + workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) + doc = workspace.get_document(DOC_URI) + items = pylsp_autoimport_completions(config, workspace, doc, com_position) + + assert len(items) > 0 + + assert check_dict({"label": "List"}, items) + +def test_autoimport_function_typing_complete(config, workspace): + AUTOIMPORT_DOC = """def func(s : Lis ):""" + com_position = {"line": 0, "character": 16} + workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) + doc = workspace.get_document(DOC_URI) + items = pylsp_autoimport_completions(config, workspace, doc, com_position) + + assert len(items) > 0 + + assert check_dict({"label": "List"}, items) + +def test_autoimport_function_typing_return(config, workspace): + AUTOIMPORT_DOC = """def func(s : Lis ) -> Generat:""" + com_position = {"line": 0, "character": 29} + workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) + doc = workspace.get_document(DOC_URI) + items = pylsp_autoimport_completions(config, workspace, doc, com_position) + + assert len(items) > 0 + + assert check_dict({"label": "Generator"}, items) def test_autoimport_dot(config, workspace): AUTOIMPORT_DOC = """str.""" - # Over 'i' in os.path.isabs(...) com_position = {"line": 0, "character": 4} workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) doc = workspace.get_document(DOC_URI) @@ -52,26 +92,44 @@ def test_autoimport_dot(config, workspace): def test_autoimport_comment(config, workspace): AUTOIMPORT_DOC = """#""" - # Over 'i' in os.path.isabs(...) - com_position = {"line": 0, "character": 0} + com_position = {"line": 0, "character": 1} workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) doc = workspace.get_document(DOC_URI) items = pylsp_autoimport_completions(config, workspace, doc, com_position) assert len(items) == 0 -def test_autoimport_from(config, workspace): - AUTOIMPORT_DOC = """from""" - # Over 'i' in os.path.isabs(...) - com_position = {"line": 0, "character": 6} + +def test_autoimport_comment_indent(config, workspace): + AUTOIMPORT_DOC = """ # """ + com_position = {"line": 0, "character": 5} workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) doc = workspace.get_document(DOC_URI) items = pylsp_autoimport_completions(config, workspace, doc, com_position) + assert len(items) == 0 + + +def test_autoimport_from(config, workspace): + AUTOIMPORT_DOC = """from """ + com_position = {"line": 0, "character": 5} + workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) + doc = workspace.get_document(DOC_URI) + items = pylsp_autoimport_completions(config, workspace, doc, com_position) assert len(items) == 0 +def test_autoimport_from_(config, workspace): + AUTOIMPORT_DOC = """from """ + com_position = {"line": 0, "character": 4} + workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) + doc = workspace.get_document(DOC_URI) + items = pylsp_autoimport_completions(config, workspace, doc, com_position) + + assert len(items) > 0 + + def test_sort_sources(): result1 = _sort_import(1, "import pathlib", "pathlib", "pathli") result2 = _sort_import(2, "import pathlib", "pathlib", "pathli") From 95bf65e088fb178739ded32eb33690b054637b1c Mon Sep 17 00:00:00 2001 From: bageljr Date: Fri, 22 Apr 2022 12:46:10 -0500 Subject: [PATCH 06/39] use fixture on test suite, use new search_full api, ignore statments already in document --- pylsp/plugins/rope_autoimport.py | 45 ++++++++- test/plugins/test_autoimport.py | 167 +++++++++++++++---------------- 2 files changed, 121 insertions(+), 91 deletions(-) diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index 417592e7..11826130 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -1,10 +1,11 @@ import logging from collections import OrderedDict -from typing import Generator, List +from typing import Generator, List, Set import parso from parso.python import tree from parso.tree import NodeOrLeaf +from rope.base.resources import Resource from rope.contrib.autoimport import AutoImport from pylsp import hookimpl @@ -91,6 +92,34 @@ def _process_statements(suggestions: List, doc_uri: str, word: str) -> Generator } +def _get_names_from_import(node: tree.Import) -> Generator[str, None, None]: + if not node.is_star_import(): + for name in node.children: + if isinstance(name, tree.PythonNode): + for sub_name in name.children: + if isinstance(sub_name, tree.Name): + yield sub_name.value + elif isinstance(name, tree.Name): + yield name.value + + +def get_names(file: str) -> Generator[str, None, None]: + """Gets all names to ignore from the current file.""" + expr = parso.parse(file) + for item in expr.children: + if isinstance(item, tree.PythonNode): + for child in item.children: + if isinstance(child, (tree.ImportFrom, tree.ExprStmt)): + for name in child.get_defined_names(): + yield name.value + elif isinstance(child, tree.Import): + for name in _get_names_from_import(child): + yield name + + if isinstance(item, (tree.Function, tree.Class)): + yield item.name.value + + @hookimpl def pylsp_completions( config: Config, workspace: Workspace, document: Document, position @@ -103,10 +132,10 @@ def pylsp_completions( word = word_node.value rope_config = config.settings(document_path=document.path).get("rope", {}) rope_project = workspace._rope_project_builder(rope_config) + ignored_names: Set[str] = set(get_names(document.source)) autoimport = AutoImport(rope_project, memory=False) # TODO: update cache - suggestions = deduplicate(autoimport.search_module(word)) - suggestions.extend(deduplicate(autoimport.search_name(word))) + suggestions = list(autoimport.search_full(word, ignored_names=ignored_names)) autoimport.close() return list(_process_statements(suggestions, document.uri, word)) @@ -136,3 +165,13 @@ def pylsp_initialize(config: Config, workspace: Workspace): autoimport.generate_modules_cache() autoimport.generate_cache() autoimport.close() + + +@hookimpl +def pylsp_document_did_save(config: Config, workspace: Workspace, document: Document): + rope_config = config.settings().get("rope", {}) + rope_doucment: Resource = document._rope_resource(rope_config) + rope_project = workspace._rope_project_builder(rope_config) + autoimport = AutoImport(rope_project, memory=False) + autoimport.generate_cache(resources=[rope_doucment]) + autoimport.close() diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index e8957030..230e5f3b 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -1,13 +1,26 @@ from typing import Dict, List +import pytest + from pylsp import lsp, uris -from pylsp.plugins.rope_autoimport import _sort_import -from pylsp.plugins.rope_autoimport import \ - pylsp_completions as pylsp_autoimport_completions +from pylsp.plugins.rope_autoimport import _sort_import, get_names +from pylsp.plugins.rope_autoimport import ( + pylsp_completions as pylsp_autoimport_completions, +) DOC_URI = uris.from_fs_path(__file__) +@pytest.fixture +def completions(config, workspace, request): + document, position = request.param + com_position = {"line": 0, "character": position} + workspace.put_document(DOC_URI, source=document) + doc = workspace.get_document(DOC_URI) + yield pylsp_autoimport_completions(config, workspace, doc, com_position) + workspace.rm_document(DOC_URI) + + def check_dict(query: Dict, results: List[Dict]) -> bool: for result in results: if all(result[key] == query[key] for key in query.keys()): @@ -15,119 +28,82 @@ def check_dict(query: Dict, results: List[Dict]) -> bool: return False -def test_autoimport_completion(config, workspace): - AUTOIMPORT_DOC = """pathli""" - com_position = {"line": 0, "character": 6} - workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) - doc = workspace.get_document(DOC_URI) - items = pylsp_autoimport_completions(config, workspace, doc, com_position) - - assert items +@pytest.mark.parametrize("completions", [("""pathli """, 6)], indirect=True) +def test_autoimport_completion(completions): + assert completions assert check_dict( - {"label": "pathlib", "kind": lsp.CompletionItemKind.Module}, items + {"label": "pathlib", "kind": lsp.CompletionItemKind.Module}, completions ) -def test_autoimport_import(config, workspace): - AUTOIMPORT_DOC = """import """ - com_position = {"line": 0, "character": 7} - workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) - doc = workspace.get_document(DOC_URI) - items = pylsp_autoimport_completions(config, workspace, doc, com_position) +@pytest.mark.parametrize("completions", [("""import """, 7)], indirect=True) +def test_autoimport_import(completions): + assert len(completions) == 0 - assert len(items) == 0 +@pytest.mark.parametrize("completions", [("""def func(s""", 10)], indirect=True) +def test_autoimport_function(completions): -def test_autoimport_function(config, workspace): - AUTOIMPORT_DOC = """def func(s""" - com_position = {"line": 0, "character": 10} - workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) - doc = workspace.get_document(DOC_URI) - items = pylsp_autoimport_completions(config, workspace, doc, com_position) + assert len(completions) == 0 - assert len(items) == 0 +@pytest.mark.parametrize("completions", [("""def func(s:Lis""", 12)], indirect=True) +def test_autoimport_function_typing(completions): + assert len(completions) > 0 + assert check_dict({"label": "List"}, completions) -def test_autoimport_function_typing(config, workspace): - AUTOIMPORT_DOC = """def func(s : Lis """ - com_position = {"line": 0, "character": 16} - workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) - doc = workspace.get_document(DOC_URI) - items = pylsp_autoimport_completions(config, workspace, doc, com_position) - assert len(items) > 0 +@pytest.mark.parametrize( + "completions", [("""def func(s : Lis ):""", 16)], indirect=True +) +def test_autoimport_function_typing_complete(completions): + assert len(completions) > 0 + assert check_dict({"label": "List"}, completions) - assert check_dict({"label": "List"}, items) - -def test_autoimport_function_typing_complete(config, workspace): - AUTOIMPORT_DOC = """def func(s : Lis ):""" - com_position = {"line": 0, "character": 16} - workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) - doc = workspace.get_document(DOC_URI) - items = pylsp_autoimport_completions(config, workspace, doc, com_position) - assert len(items) > 0 +@pytest.mark.parametrize( + "completions", [("""def func(s : Lis ) -> Generat:""", 29)], indirect=True +) +def test_autoimport_function_typing_return(completions): + assert len(completions) > 0 + assert check_dict({"label": "Generator"}, completions) - assert check_dict({"label": "List"}, items) -def test_autoimport_function_typing_return(config, workspace): - AUTOIMPORT_DOC = """def func(s : Lis ) -> Generat:""" - com_position = {"line": 0, "character": 29} - workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) +def test_autoimport_defined_name(config, workspace): + document = """List = "hi"\nLis""" + com_position = {"line": 1, "character": 3} + workspace.put_document(DOC_URI, source=document) doc = workspace.get_document(DOC_URI) - items = pylsp_autoimport_completions(config, workspace, doc, com_position) + completions = pylsp_autoimport_completions(config, workspace, doc, com_position) + workspace.rm_document(DOC_URI) + assert not check_dict({"label": "List"}, completions) - assert len(items) > 0 - assert check_dict({"label": "Generator"}, items) -def test_autoimport_dot(config, workspace): - AUTOIMPORT_DOC = """str.""" - com_position = {"line": 0, "character": 4} - workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) - doc = workspace.get_document(DOC_URI) - items = pylsp_autoimport_completions(config, workspace, doc, com_position) - - assert len(items) == 0 - - -def test_autoimport_comment(config, workspace): - AUTOIMPORT_DOC = """#""" - com_position = {"line": 0, "character": 1} - workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) - doc = workspace.get_document(DOC_URI) - items = pylsp_autoimport_completions(config, workspace, doc, com_position) +@pytest.mark.parametrize("completions", [("""str.""", 4)], indirect=True) +def test_autoimport_dot(completions): - assert len(items) == 0 + assert len(completions) == 0 -def test_autoimport_comment_indent(config, workspace): - AUTOIMPORT_DOC = """ # """ - com_position = {"line": 0, "character": 5} - workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) - doc = workspace.get_document(DOC_URI) - items = pylsp_autoimport_completions(config, workspace, doc, com_position) +@pytest.mark.parametrize("completions", [("""#""", 1)], indirect=True) +def test_autoimport_comment(completions): + assert len(completions) == 0 - assert len(items) == 0 +@pytest.mark.parametrize("completions", [(""" # """, 5)], indirect=True) +def test_autoimport_comment_indent(completions): -def test_autoimport_from(config, workspace): - AUTOIMPORT_DOC = """from """ - com_position = {"line": 0, "character": 5} - workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) - doc = workspace.get_document(DOC_URI) - items = pylsp_autoimport_completions(config, workspace, doc, com_position) + assert len(completions) == 0 - assert len(items) == 0 +@pytest.mark.parametrize("completions", [("""from """, 5)], indirect=True) +def test_autoimport_from(completions): + assert len(completions) == 0 -def test_autoimport_from_(config, workspace): - AUTOIMPORT_DOC = """from """ - com_position = {"line": 0, "character": 4} - workspace.put_document(DOC_URI, source=AUTOIMPORT_DOC) - doc = workspace.get_document(DOC_URI) - items = pylsp_autoimport_completions(config, workspace, doc, com_position) - assert len(items) > 0 +@pytest.mark.parametrize("completions", [("""from """, 4)], indirect=True) +def test_autoimport_from_(completions): + assert len(completions) > 0 def test_sort_sources(): @@ -150,3 +126,18 @@ def test_sort_both(): ) result2 = _sort_import(2, "import pathlib", "pathlib", "pathli") assert result1 > result2 + + +def test_get_names(): + source = """ + from a import s as e + import blah, bleh + hello = "str" + a, b = 1, 2 + def someone(): + soemthing + class sfa: + sfiosifo + """ + results = set(get_names(source)) + assert results == set(["blah", "bleh", "e", "hello", "someone", "sfa", "a", "b"]) From 60dbe4702816307cca96ebcb19012249c6da33b0 Mon Sep 17 00:00:00 2001 From: bageljr Date: Fri, 22 Apr 2022 23:56:03 -0500 Subject: [PATCH 07/39] ignore class, dots, import statements --- pylsp/plugins/rope_autoimport.py | 32 +++++++++++++++++-- test/plugins/test_autoimport.py | 55 +++++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index 11826130..36a02727 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -1,5 +1,6 @@ import logging from collections import OrderedDict +from functools import lru_cache from typing import Generator, List, Set import parso @@ -32,6 +33,13 @@ def should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: if isinstance(first_child, tree.EndMarker): if "#" in first_child.prefix: return False + if first_child == word_node: + return True + if isinstance(first_child, tree.Import): + return False + if len(expr.children) > 1: + if expr.children[1].type == "trailer": + return False if isinstance( first_child, ( @@ -39,16 +47,30 @@ def should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: tree.PythonErrorLeaf, ), ): - if first_child.value in ("import", "from") and first_child != word_node: + if first_child.value in ("import", "from"): return False - if isinstance(first_child, (tree.PythonErrorNode)): + if isinstance(first_child, (tree.PythonErrorNode, tree.PythonNode)): return should_insert(first_child, word_node) if isinstance(first_child, tree.Keyword): if first_child.value == "def": return _should_import_function(word_node, expr) + if first_child.value == "class": + return _should_import_class(word_node, expr) return True +def _should_import_class(word_node: tree.Leaf, expr: tree.BaseNode) -> bool: + prev_node = None + for node in expr.children: + if isinstance(node, tree.Name): + if isinstance(prev_node, tree.Operator): + if node == word_node and prev_node.value == "(": + return True + prev_node = node + + return False + + def _should_import_function(word_node: tree.Leaf, expr: tree.BaseNode) -> bool: prev_node = None for node in expr.children: @@ -103,6 +125,7 @@ def _get_names_from_import(node: tree.Import) -> Generator[str, None, None]: yield name.value +@lru_cache(maxsize=100) def get_names(file: str) -> Generator[str, None, None]: """Gets all names to ignore from the current file.""" expr = parso.parse(file) @@ -154,11 +177,12 @@ def _sort_import( score: int = source_score + suggested_name_score + full_statement_score # Since we are using ints, we need to pad them. # We also want to prioritize autoimport behind everything since its the last priority. - return "zz" + str(score).rjust(10, "0") + return "zzzzz" + str(score).rjust(10, "0") @hookimpl def pylsp_initialize(config: Config, workspace: Workspace): + """Initialize AutoImport. Generates the cache for local and global items.""" rope_config = config.settings().get("rope", {}) rope_project = workspace._rope_project_builder(rope_config) autoimport = AutoImport(rope_project, memory=False) @@ -169,9 +193,11 @@ def pylsp_initialize(config: Config, workspace: Workspace): @hookimpl def pylsp_document_did_save(config: Config, workspace: Workspace, document: Document): + """Update the names associated with this document. Doesn't work because this hook isn't called.""" rope_config = config.settings().get("rope", {}) rope_doucment: Resource = document._rope_resource(rope_config) rope_project = workspace._rope_project_builder(rope_config) autoimport = AutoImport(rope_project, memory=False) autoimport.generate_cache(resources=[rope_doucment]) + autoimport.generate_modules_cache() autoimport.close() diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index 230e5f3b..29641740 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -3,16 +3,18 @@ import pytest from pylsp import lsp, uris +from pylsp.config.config import Config from pylsp.plugins.rope_autoimport import _sort_import, get_names from pylsp.plugins.rope_autoimport import ( pylsp_completions as pylsp_autoimport_completions, ) +from pylsp.workspace import Workspace DOC_URI = uris.from_fs_path(__file__) @pytest.fixture -def completions(config, workspace, request): +def completions(config: Config, workspace: Workspace, request): document, position = request.param com_position = {"line": 0, "character": position} workspace.put_document(DOC_URI, source=document) @@ -41,12 +43,36 @@ def test_autoimport_import(completions): assert len(completions) == 0 +@pytest.mark.parametrize("completions", [("""import test\n""", 10)], indirect=True) +def test_autoimport_import_with_name(completions): + assert len(completions) == 0 + + @pytest.mark.parametrize("completions", [("""def func(s""", 10)], indirect=True) def test_autoimport_function(completions): assert len(completions) == 0 +@pytest.mark.parametrize("completions", [("""class Test""", 10)], indirect=True) +def test_autoimport_class(completions): + assert len(completions) == 0 + + +@pytest.mark.parametrize( + "completions", [("""class Test(NamedTupl):""", 20)], indirect=True +) +def test_autoimport_class_complete(completions): + assert len(completions) > 0 + + +@pytest.mark.parametrize( + "completions", [("""class Test(NamedTupl""", 20)], indirect=True +) +def test_autoimport_class_incomplete(completions): + assert len(completions) > 0 + + @pytest.mark.parametrize("completions", [("""def func(s:Lis""", 12)], indirect=True) def test_autoimport_function_typing(completions): assert len(completions) > 0 @@ -79,12 +105,39 @@ def test_autoimport_defined_name(config, workspace): assert not check_dict({"label": "List"}, completions) +# This test won't work because pylsp cannot detect changes correctly. +# def test_autoimport_update_module(config: Config, workspace: Workspace): +# document = "SomethingYouShouldntWrite = 1" +# document2 = """SomethingYouShouldntWrit""" +# com_position = { +# "line": 0, +# "character": 3, +# } +# DOC2_URI = uris.from_fs_path(workspace.root_path + "/document1.py") +# 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 +# workspace.put_document(DOC2_URI, source=document2) +# assert check_dict({"label": "SomethingYouShouldntWrite"}, completions) +# workspace.put_document(DOC2_URI, source="") +# completions = pylsp_autoimport_completions(config, workspace, doc, com_position) +# assert len(completions) == 0 +# workspace.rm_document(DOC_URI) + + @pytest.mark.parametrize("completions", [("""str.""", 4)], indirect=True) def test_autoimport_dot(completions): assert len(completions) == 0 +@pytest.mark.parametrize("completions", [("""str.metho\n""", 9)], indirect=True) +def test_autoimport_dot_partial(completions): + + assert len(completions) == 0 + + @pytest.mark.parametrize("completions", [("""#""", 1)], indirect=True) def test_autoimport_comment(completions): assert len(completions) == 0 From 803455cf698888235e58665488cf78914a22fe91 Mon Sep 17 00:00:00 2001 From: bageljr Date: Tue, 26 Apr 2022 19:07:13 -0500 Subject: [PATCH 08/39] use thresholding for sorting --- pylsp/plugins/rope_autoimport.py | 35 ++++++++++++++++------ test/plugins/test_autoimport.py | 51 ++++++++++++++++++++++++-------- 2 files changed, 64 insertions(+), 22 deletions(-) diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index 36a02727..e739df01 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -104,12 +104,15 @@ def _process_statements(suggestions: List, doc_uri: str, word: str) -> Generator start = {"line": insert_line, "character": 0} edit_range = {"start": start, "end": start} edit = {"range": edit_range, "newText": import_statement + "\n"} + score = _get_score(source, import_statement, name, word) + if score > 1000: + continue yield { "label": name, "kind": itemkind, - "sortText": _sort_import(source, import_statement, name, word), + "sortText": _sort_import(score), "data": {"doc_uri": doc_uri}, - "documentation": _document(import_statement), + "detail": _document(import_statement), "additionalTextEdits": [edit], } @@ -160,24 +163,38 @@ def pylsp_completions( # TODO: update cache suggestions = list(autoimport.search_full(word, ignored_names=ignored_names)) autoimport.close() - return list(_process_statements(suggestions, document.uri, word)) + results = list( + sorted( + _process_statements(suggestions, document.uri, word), + key=lambda statement: statement["sortText"], + ) + ) + if len(results) > 25: + results = results[:25] + return results def _document(import_statement: str) -> str: return "__autoimport__\n" + import_statement -def _sort_import( +def _get_score( source: int, full_statement: str, suggested_name: str, desired_name -) -> str: +) -> int: import_length = len("import") - full_statement_score = 2 * (len(full_statement) - import_length) - suggested_name_score = 5 * (len(suggested_name) - len(desired_name)) + full_statement_score = (len(full_statement) - import_length) ** 2 + suggested_name_score = ((len(suggested_name) - len(desired_name))) ** 2 source_score = 20 * source - score: int = source_score + suggested_name_score + full_statement_score + return source_score + suggested_name_score + full_statement_score + + +def _sort_import(score: int) -> str: + pow = 5 + score = max(min(score, (10**pow) - 1), 0) # Since we are using ints, we need to pad them. # We also want to prioritize autoimport behind everything since its the last priority. - return "zzzzz" + str(score).rjust(10, "0") + # The minimum is to prevent score from overflowing the pad + return "[z" + str(score).rjust(pow, "0") @hookimpl diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index 29641740..6473889d 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -4,7 +4,7 @@ from pylsp import lsp, uris from pylsp.config.config import Config -from pylsp.plugins.rope_autoimport import _sort_import, get_names +from pylsp.plugins.rope_autoimport import _get_score, get_names from pylsp.plugins.rope_autoimport import ( pylsp_completions as pylsp_autoimport_completions, ) @@ -43,6 +43,17 @@ def test_autoimport_import(completions): assert len(completions) == 0 +@pytest.mark.parametrize("completions", [("""pathlib""", 2)], indirect=True) +def test_autoimport_pathlib(completions): + assert completions[0]["label"] == "pathlib" + + start = {"line": 0, "character": 0} + edit_range = {"start": start, "end": start} + assert completions[0]["additionalTextEdits"] == [ + {"range": edit_range, "newText": "import pathlib\n"} + ] + + @pytest.mark.parametrize("completions", [("""import test\n""", 10)], indirect=True) def test_autoimport_import_with_name(completions): assert len(completions) == 0 @@ -105,22 +116,36 @@ def test_autoimport_defined_name(config, workspace): assert not check_dict({"label": "List"}, completions) -# This test won't work because pylsp cannot detect changes correctly. +# This test has several large issues. +# 1. Pylsp does not call the hook pylsp_document_did_save from what I can tell +# 2. autoimport relies on its sources being written to disk. This makes testing harder +# 3. the hook doesn't handle removed files +# 4. The testing framework cannot access the actual autoimport object so it cannot clear the cache # def test_autoimport_update_module(config: Config, workspace: Workspace): -# document = "SomethingYouShouldntWrite = 1" -# document2 = """SomethingYouShouldntWrit""" +# document2 = "SomethingYouShouldntWrite = 1" +# document = """SomethingYouShouldntWrit""" # com_position = { # "line": 0, # "character": 3, # } -# DOC2_URI = uris.from_fs_path(workspace.root_path + "/document1.py") +# 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="") +# 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) @@ -160,24 +185,24 @@ def test_autoimport_from_(completions): def test_sort_sources(): - result1 = _sort_import(1, "import pathlib", "pathlib", "pathli") - result2 = _sort_import(2, "import pathlib", "pathlib", "pathli") + result1 = _get_score(1, "import pathlib", "pathlib", "pathli") + result2 = _get_score(2, "import pathlib", "pathlib", "pathli") assert result1 < result2 def test_sort_statements(): - result1 = _sort_import( + result1 = _get_score( 2, "from importlib_metadata import pathlib", "pathlib", "pathli" ) - result2 = _sort_import(2, "import pathlib", "pathlib", "pathli") + result2 = _get_score(2, "import pathlib", "pathlib", "pathli") assert result1 > result2 def test_sort_both(): - result1 = _sort_import( + result1 = _get_score( 3, "from importlib_metadata import pathlib", "pathlib", "pathli" ) - result2 = _sort_import(2, "import pathlib", "pathlib", "pathli") + result2 = _get_score(2, "import pathlib", "pathlib", "pathli") assert result1 > result2 @@ -187,7 +212,7 @@ def test_get_names(): import blah, bleh hello = "str" a, b = 1, 2 - def someone(): + def someone(): soemthing class sfa: sfiosifo From b1fcbfe7454bdcadb004b155f06dbfb5ba92f858 Mon Sep 17 00:00:00 2001 From: bageljr Date: Wed, 27 Apr 2022 11:34:00 -0500 Subject: [PATCH 09/39] implement document_did_save, adjust sorting --- pylsp/plugins/rope_autoimport.py | 17 +++++++++-------- pylsp/python_lsp.py | 4 ++++ test/plugins/test_autoimport.py | 7 +++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index e739df01..8bac9d0b 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -16,6 +16,8 @@ log = logging.getLogger(__name__) +_score_pow = 5 +_score_max = 10 ** _score_pow @hookimpl def pylsp_settings(): # Default rope_completion to disabled @@ -105,7 +107,7 @@ def _process_statements(suggestions: List, doc_uri: str, word: str) -> Generator edit_range = {"start": start, "end": start} edit = {"range": edit_range, "newText": import_statement + "\n"} score = _get_score(source, import_statement, name, word) - if score > 1000: + if score > _score_max: continue yield { "label": name, @@ -171,7 +173,7 @@ def pylsp_completions( ) if len(results) > 25: results = results[:25] - return results + return results def _document(import_statement: str) -> str: @@ -182,19 +184,18 @@ def _get_score( source: int, full_statement: str, suggested_name: str, desired_name ) -> int: import_length = len("import") - full_statement_score = (len(full_statement) - import_length) ** 2 + full_statement_score = (len(full_statement) - import_length) suggested_name_score = ((len(suggested_name) - len(desired_name))) ** 2 source_score = 20 * source - return source_score + suggested_name_score + full_statement_score + return suggested_name_score + full_statement_score + source_score def _sort_import(score: int) -> str: - pow = 5 - score = max(min(score, (10**pow) - 1), 0) + score = max(min(score, (_score_max) - 1), 0) # Since we are using ints, we need to pad them. # We also want to prioritize autoimport behind everything since its the last priority. # The minimum is to prevent score from overflowing the pad - return "[z" + str(score).rjust(pow, "0") + return "[z" + str(score).rjust(_score_pow, "0") @hookimpl @@ -210,7 +211,7 @@ def pylsp_initialize(config: Config, workspace: Workspace): @hookimpl def pylsp_document_did_save(config: Config, workspace: Workspace, document: Document): - """Update the names associated with this document. Doesn't work because this hook isn't called.""" + """Update the names associated with this document.""" rope_config = config.settings().get("rope", {}) rope_doucment: Resource = document._rope_resource(rope_config) rope_project = workspace._rope_project_builder(rope_config) diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index 81e93bdc..7967e663 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -274,6 +274,9 @@ def definitions(self, doc_uri, position): def document_symbols(self, doc_uri): return flatten(self._hook('pylsp_document_symbols', doc_uri)) + def document_did_save(self, doc_uri): + return self._hook("pylsp_document_did_save", doc_uri) + def execute_command(self, command, arguments): return self._hook('pylsp_execute_command', command=command, arguments=arguments) @@ -340,6 +343,7 @@ def m_text_document__did_change(self, contentChanges=None, textDocument=None, ** def m_text_document__did_save(self, textDocument=None, **_kwargs): self.lint(textDocument['uri'], is_saved=True) + self.document_did_save(textDocument['uri']) def m_text_document__code_action(self, textDocument=None, range=None, context=None, **_kwargs): return self.code_actions(textDocument['uri'], range, context) diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index 6473889d..9b0f2212 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -117,10 +117,9 @@ def test_autoimport_defined_name(config, workspace): # This test has several large issues. -# 1. Pylsp does not call the hook pylsp_document_did_save from what I can tell -# 2. autoimport relies on its sources being written to disk. This makes testing harder -# 3. the hook doesn't handle removed files -# 4. The testing framework cannot access the actual autoimport object so it cannot clear the cache +# 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""" From 8cb971c0b97cd4ac005e1fa143ef6167bd5daece Mon Sep 17 00:00:00 2001 From: bageljr Date: Wed, 27 Apr 2022 14:01:55 -0500 Subject: [PATCH 10/39] update docs, place imports correctly. --- CONFIGURATION.md | 1 + docs/autoimport.md | 17 ++++++ pylsp/plugins/rope_autoimport.py | 99 +++++++++++++++++++------------- test/plugins/test_autoimport.py | 7 ++- 4 files changed, 81 insertions(+), 43 deletions(-) create mode 100644 docs/autoimport.md diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 7ba70cf9..5682c196 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -59,6 +59,7 @@ This server can be configured using `workspace/didChangeConfiguration` method. E | `pylsp.plugins.pylint.args` | `array` of non-unique `string` items | Arguments to pass to pylint. | `null` | | `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_completion.enabled` | `boolean` | Enable or disable the plugin. | `true` | +| `pylsp.plugins.rope_autoimport.enabled` | `boolean` | Enable or disable the plugin. | `true` | | `pylsp.plugins.rope_completion.eager` | `boolean` | Resolve documentation and detail eagerly. | `false` | | `pylsp.plugins.yapf.enabled` | `boolean` | Enable or disable the plugin. | `true` | | `pylsp.rope.extensionModules` | `string` | Builtin and c-extension modules that are allowed to be imported and inspected by rope. | `null` | diff --git a/docs/autoimport.md b/docs/autoimport.md new file mode 100644 index 00000000..daf4160c --- /dev/null +++ b/docs/autoimport.md @@ -0,0 +1,17 @@ +# Autoimport for pylsp +Requirements: +1. rope +2. ``pylsp.plugins.rope_autoimport.enabled`` is enabled +## Startup +Autoimport will generate an autoimport sqllite3 database in .ropefolder/autoimport.db on startup. +This will take a few seconds but should be much quicker on future runs. +## Usage +Autoimport will provide suggestions to import names from everything in ``sys.path``. It will suggest modules, submodules, keywords, functions, and classes. + +Since autoimport inserts everything towards the end of the import group, its recommended you use the isort [plugin](https://github.com/paradoxxxzero/pyls-isort). + +## Credits + - Most of the code was written by me, @bageljrkhanofemus + - [lyz-code](https://github.com/lyz-code/autoimport) for inspiration and some ideas + - [rope](https://github.com/python-rope/rope) + - [pyright](https://github.com/Microsoft/pyright) for details on language server implementation diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index 8bac9d0b..1266ad3e 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -17,7 +17,9 @@ _score_pow = 5 -_score_max = 10 ** _score_pow +_score_max = 10**_score_pow + + @hookimpl def pylsp_settings(): # Default rope_completion to disabled @@ -30,34 +32,45 @@ def deduplicate(input_list): def should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: - if len(expr.children) > 0: - first_child = expr.children[0] - if isinstance(first_child, tree.EndMarker): - if "#" in first_child.prefix: - return False - if first_child == word_node: - return True - if isinstance(first_child, tree.Import): + """ + Check if we should insert the word_node on the given expr. + + Works for both correct and incorrect code. This is because the + user is often working on the code as they write it. + """ + if len(expr.children) == 0: + return True + first_child = expr.children[0] + if isinstance(first_child, tree.EndMarker): + if "#" in first_child.prefix: + return False # Check for single line comment + if first_child == word_node: + return True # If the word is the first word then its fine + if len(expr.children) > 1: + if any(node.type == "trailer" for node in expr.children): + return False # Check if we're on a method of a function + if isinstance(first_child, (tree.PythonErrorNode, tree.PythonNode)): + # The tree will often include error nodes like this to indicate errors + # we want to ignore errors since the code is being written + return should_insert(first_child, word_node) + return handle_first_child(first_child, expr, word_node) + + +def handle_first_child( + first_child: NodeOrLeaf, expr: tree.BaseNode, word_node: tree.Leaf +) -> bool: + """Check if we suggest imports given the following first child.""" + if isinstance(first_child, tree.Import): + return False + if isinstance(first_child, (tree.PythonLeaf, tree.PythonErrorLeaf)): + # Check if the first item is a from or import statement even when incomplete + if first_child.value in ("import", "from"): return False - if len(expr.children) > 1: - if expr.children[1].type == "trailer": - return False - if isinstance( - first_child, - ( - tree.PythonLeaf, - tree.PythonErrorLeaf, - ), - ): - if first_child.value in ("import", "from"): - return False - if isinstance(first_child, (tree.PythonErrorNode, tree.PythonNode)): - return should_insert(first_child, word_node) - if isinstance(first_child, tree.Keyword): - if first_child.value == "def": - return _should_import_function(word_node, expr) - if first_child.value == "class": - return _should_import_class(word_node, expr) + if isinstance(first_child, tree.Keyword): + if first_child.value == "def": + return _should_import_function(word_node, expr) + if first_child.value == "class": + return _should_import_class(word_node, expr) return True @@ -98,11 +111,15 @@ def _handle_argument(node: NodeOrLeaf, word_node: tree.Leaf): return False -def _process_statements(suggestions: List, doc_uri: str, word: str) -> Generator: +def _process_statements( + suggestions: List, + doc_uri: str, + word: str, + autoimport: AutoImport, + document: Document, +) -> Generator: for import_statement, name, source, itemkind in suggestions: - # insert_line = autoimport.find_insertion_line(document) - # TODO: use isort to handle insertion line correctly - insert_line = 0 + insert_line = autoimport.find_insertion_line(document.source) - 1 start = {"line": insert_line, "character": 0} edit_range = {"start": start, "end": start} edit = {"range": edit_range, "newText": import_statement + "\n"} @@ -132,7 +149,7 @@ def _get_names_from_import(node: tree.Import) -> Generator[str, None, None]: @lru_cache(maxsize=100) def get_names(file: str) -> Generator[str, None, None]: - """Gets all names to ignore from the current file.""" + """Get all names to ignore from the current file.""" expr = parso.parse(file) for item in expr.children: if isinstance(item, tree.PythonNode): @@ -152,6 +169,7 @@ def get_names(file: str) -> Generator[str, None, None]: def pylsp_completions( config: Config, workspace: Workspace, document: Document, position ): + """Get autoimport suggestions.""" line = document.lines[position["line"]] expr = parso.parse(line) word_node = expr.get_leaf_for_position((1, position["character"])) @@ -162,17 +180,17 @@ def pylsp_completions( rope_project = workspace._rope_project_builder(rope_config) ignored_names: Set[str] = set(get_names(document.source)) autoimport = AutoImport(rope_project, memory=False) - # TODO: update cache suggestions = list(autoimport.search_full(word, ignored_names=ignored_names)) autoimport.close() results = list( sorted( - _process_statements(suggestions, document.uri, word), + _process_statements(suggestions, document.uri, word, autoimport, document), key=lambda statement: statement["sortText"], ) ) - if len(results) > 25: - results = results[:25] + max_size = 100 + if len(results) > max_size: + results = results[:max_size] return results @@ -184,10 +202,10 @@ def _get_score( source: int, full_statement: str, suggested_name: str, desired_name ) -> int: import_length = len("import") - full_statement_score = (len(full_statement) - import_length) + full_statement_score = len(full_statement) - import_length suggested_name_score = ((len(suggested_name) - len(desired_name))) ** 2 source_score = 20 * source - return suggested_name_score + full_statement_score + source_score + return suggested_name_score + full_statement_score + source_score def _sort_import(score: int) -> str: @@ -217,5 +235,6 @@ def pylsp_document_did_save(config: Config, workspace: Workspace, document: Docu rope_project = workspace._rope_project_builder(rope_config) autoimport = AutoImport(rope_project, memory=False) autoimport.generate_cache(resources=[rope_doucment]) - autoimport.generate_modules_cache() + # Might as well using saving the document as an indicator to regenerate the module cache + autoimport.generate_modules_cache() autoimport.close() diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index 9b0f2212..1e29e6ca 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -5,13 +5,14 @@ from pylsp import lsp, uris from pylsp.config.config import Config from pylsp.plugins.rope_autoimport import _get_score, get_names -from pylsp.plugins.rope_autoimport import ( - pylsp_completions as pylsp_autoimport_completions, -) +from pylsp.plugins.rope_autoimport import \ + pylsp_completions as pylsp_autoimport_completions from pylsp.workspace import Workspace DOC_URI = uris.from_fs_path(__file__) +# pylint: disable=redefined-outer-name + @pytest.fixture def completions(config: Config, workspace: Workspace, request): From 7ef14bd56b666ef2149ffbeb241360a8da13188b Mon Sep 17 00:00:00 2001 From: bageljr Date: Wed, 4 May 2022 11:30:16 -0500 Subject: [PATCH 11/39] update to use sqlite implementation --- pylsp/plugins/rope_autoimport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index 1266ad3e..bcf70bb4 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -7,7 +7,7 @@ from parso.python import tree from parso.tree import NodeOrLeaf from rope.base.resources import Resource -from rope.contrib.autoimport import AutoImport +from rope.contrib.autoimport.sqlite import AutoImport from pylsp import hookimpl from pylsp.config.config import Config From bba1d1628718304ba446427459ab6884e021ebe4 Mon Sep 17 00:00:00 2001 From: bageljr Date: Wed, 25 May 2022 17:06:28 +0530 Subject: [PATCH 12/39] clean up, bump rope to 1.1.1, make default disabled --- CONFIGURATION.md | 2 +- README.md | 1 + docs/autoimport.md | 9 +++++---- pylsp/config/schema.json | 2 +- pylsp/plugins/rope_autoimport.py | 26 ++++++++++---------------- pyproject.toml | 4 ++-- 6 files changed, 20 insertions(+), 24 deletions(-) diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 70e20790..bc796def 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -59,7 +59,7 @@ This server can be configured using `workspace/didChangeConfiguration` method. E | `pylsp.plugins.pylint.args` | `array` of non-unique `string` items | Arguments to pass to pylint. | `null` | | `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_completion.enabled` | `boolean` | Enable or disable the plugin. | `false` | -| `pylsp.plugins.rope_autoimport.enabled` | `boolean` | Enable or disable the plugin. | `true` | +| `pylsp.plugins.rope_autoimport.enabled` | `boolean` | Enable or disable the plugin. | `false` | | `pylsp.plugins.rope_completion.eager` | `boolean` | Resolve documentation and detail eagerly. | `false` | | `pylsp.plugins.yapf.enabled` | `boolean` | Enable or disable the plugin. | `true` | | `pylsp.rope.extensionModules` | `string` | Builtin and c-extension modules that are allowed to be imported and inspected by rope. | `null` | diff --git a/README.md b/README.md index 4fe74032..a18526bf 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ As an example, to change the list of errors that pycodestyle will ignore, assumi ## LSP Server Features * Auto Completion +* [Autoimport](docs/autoimport.md) * Code Linting * Signature Help * Go to definition diff --git a/docs/autoimport.md b/docs/autoimport.md index daf4160c..bc7f372d 100644 --- a/docs/autoimport.md +++ b/docs/autoimport.md @@ -1,17 +1,18 @@ # Autoimport for pylsp Requirements: -1. rope -2. ``pylsp.plugins.rope_autoimport.enabled`` is enabled +1. install ``python-lsp-server[rope]`` +2. set ``pylsp.plugins.rope_autoimport.enabled`` to ``true`` ## Startup Autoimport will generate an autoimport sqllite3 database in .ropefolder/autoimport.db on startup. This will take a few seconds but should be much quicker on future runs. ## Usage -Autoimport will provide suggestions to import names from everything in ``sys.path``. It will suggest modules, submodules, keywords, functions, and classes. +Autoimport will provide suggestions to import names from everything in ``sys.path``. You can change this by changing where pylsp is running or by setting rope's 'python_path' option. +It will suggest modules, submodules, keywords, functions, and classes. Since autoimport inserts everything towards the end of the import group, its recommended you use the isort [plugin](https://github.com/paradoxxxzero/pyls-isort). ## Credits - Most of the code was written by me, @bageljrkhanofemus - [lyz-code](https://github.com/lyz-code/autoimport) for inspiration and some ideas - - [rope](https://github.com/python-rope/rope) + - [rope](https://github.com/python-rope/rope), especially @lieryan - [pyright](https://github.com/Microsoft/pyright) for details on language server implementation diff --git a/pylsp/config/schema.json b/pylsp/config/schema.json index 1061faa9..96b40471 100644 --- a/pylsp/config/schema.json +++ b/pylsp/config/schema.json @@ -337,7 +337,7 @@ "description": "Enable or disable the plugin." },"pylsp.plugins.rope_autoimport.enabled": { "type": "boolean", - "default": true, + "default": false, "description": "Enable or disable the plugin." }, "pylsp.plugins.rope_completion.eager": { diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index bcf70bb4..7e9dad33 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -1,7 +1,6 @@ import logging -from collections import OrderedDict from functools import lru_cache -from typing import Generator, List, Set +from typing import Any, Dict, Generator, List, Set import parso from parso.python import tree @@ -21,17 +20,12 @@ @hookimpl -def pylsp_settings(): +def pylsp_settings() -> Dict[str, Dict[str, Dict[str, Any]]]: # Default rope_completion to disabled - return {"plugins": {"rope_autoimport": {"enabled": True}}} + return {"plugins": {"rope_autoimport": {"enabled": False}}} -def deduplicate(input_list): - """Remove duplicates from list.""" - return list(OrderedDict.fromkeys(input_list)) - - -def should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: +def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: """ Check if we should insert the word_node on the given expr. @@ -52,11 +46,11 @@ def should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: if isinstance(first_child, (tree.PythonErrorNode, tree.PythonNode)): # The tree will often include error nodes like this to indicate errors # we want to ignore errors since the code is being written - return should_insert(first_child, word_node) - return handle_first_child(first_child, expr, word_node) + return _should_insert(first_child, word_node) + return _handle_first_child(first_child, expr, word_node) -def handle_first_child( +def _handle_first_child( first_child: NodeOrLeaf, expr: tree.BaseNode, word_node: tree.Leaf ) -> bool: """Check if we suggest imports given the following first child.""" @@ -117,7 +111,7 @@ def _process_statements( word: str, autoimport: AutoImport, document: Document, -) -> Generator: +) -> Generator[Dict[str, Any], None, None]: for import_statement, name, source, itemkind in suggestions: insert_line = autoimport.find_insertion_line(document.source) - 1 start = {"line": insert_line, "character": 0} @@ -173,7 +167,7 @@ def pylsp_completions( line = document.lines[position["line"]] expr = parso.parse(line) word_node = expr.get_leaf_for_position((1, position["character"])) - if not should_insert(expr, word_node): + if not _should_insert(expr, word_node): return [] word = word_node.value rope_config = config.settings(document_path=document.path).get("rope", {}) @@ -236,5 +230,5 @@ def pylsp_document_did_save(config: Config, workspace: Workspace, document: Docu autoimport = AutoImport(rope_project, memory=False) autoimport.generate_cache(resources=[rope_doucment]) # Might as well using saving the document as an indicator to regenerate the module cache - autoimport.generate_modules_cache() + autoimport.generate_modules_cache() autoimport.close() diff --git a/pyproject.toml b/pyproject.toml index df86680d..a5018c69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ all = [ "pydocstyle>=2.0.0", "pyflakes>=2.4.0,<2.5.0", "pylint>=2.5.0", - "rope>=1.1.0", + "rope>=1.1.1", "yapf", ] autopep8 = ["autopep8>=1.6.0,<1.7.0"] @@ -43,7 +43,7 @@ pycodestyle = ["pycodestyle>=2.8.0,<2.9.0"] pydocstyle = ["pydocstyle>=2.0.0"] pyflakes = ["pyflakes>=2.4.0,<2.5.0"] pylint = ["pylint>=2.5.0"] -rope = ["rope>0.10.5"] +rope = ["rope>1.1.1"] yapf = ["yapf"] test = [ "pylint>=2.5.0", From 8f6f2ce234d6169c0cd92e012256520d153d20ca Mon Sep 17 00:00:00 2001 From: bageljr Date: Wed, 25 May 2022 17:15:45 +0530 Subject: [PATCH 13/39] fix: schema order --- CONFIGURATION.md | 2 +- pylsp/config/schema.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CONFIGURATION.md b/CONFIGURATION.md index bc796def..6636bbee 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -58,8 +58,8 @@ This server can be configured using `workspace/didChangeConfiguration` method. E | `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. | `null` | | `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_completion.enabled` | `boolean` | Enable or disable the plugin. | `false` | | `pylsp.plugins.rope_autoimport.enabled` | `boolean` | Enable or disable the plugin. | `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` | | `pylsp.plugins.yapf.enabled` | `boolean` | Enable or disable the plugin. | `true` | | `pylsp.rope.extensionModules` | `string` | Builtin and c-extension modules that are allowed to be imported and inspected by rope. | `null` | diff --git a/pylsp/config/schema.json b/pylsp/config/schema.json index 96b40471..07a65b44 100644 --- a/pylsp/config/schema.json +++ b/pylsp/config/schema.json @@ -330,12 +330,12 @@ "type": "string", "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_completion.enabled": { + },"pylsp.plugins.rope_autoimport.enabled": { "type": "boolean", "default": false, "description": "Enable or disable the plugin." - },"pylsp.plugins.rope_autoimport.enabled": { + }, + "pylsp.plugins.rope_completion.enabled": { "type": "boolean", "default": false, "description": "Enable or disable the plugin." From a363cc21ff2cc61fe0fb642272fde6e788004cfc Mon Sep 17 00:00:00 2001 From: bageljr Date: Wed, 25 May 2022 17:29:06 +0530 Subject: [PATCH 14/39] redo test suite --- test/plugins/test_autoimport.py | 45 +++++++++++++++------------------ 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index 1e29e6ca..b1dae48c 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -1,10 +1,11 @@ from typing import Dict, List +import parso import pytest from pylsp import lsp, uris from pylsp.config.config import Config -from pylsp.plugins.rope_autoimport import _get_score, get_names +from pylsp.plugins.rope_autoimport import _get_score, _should_insert, get_names from pylsp.plugins.rope_autoimport import \ pylsp_completions as pylsp_autoimport_completions from pylsp.workspace import Workspace @@ -24,6 +25,12 @@ def completions(config: Config, workspace: Workspace, request): workspace.rm_document(DOC_URI) +def should_insert(phrase: str, position: int): + expr = parso.parse(phrase) + word_node = expr.get_leaf_for_position((1, position)) + return _should_insert(expr, word_node) + + def check_dict(query: Dict, results: List[Dict]) -> bool: for result in results: if all(result[key] == query[key] for key in query.keys()): @@ -151,37 +158,25 @@ def test_autoimport_defined_name(config, workspace): # workspace.rm_document(DOC_URI) -@pytest.mark.parametrize("completions", [("""str.""", 4)], indirect=True) -def test_autoimport_dot(completions): - - assert len(completions) == 0 - - -@pytest.mark.parametrize("completions", [("""str.metho\n""", 9)], indirect=True) -def test_autoimport_dot_partial(completions): - - assert len(completions) == 0 +class test_should_insert: + def test_dot(completions): + assert not should_insert("""str.""", 4) -@pytest.mark.parametrize("completions", [("""#""", 1)], indirect=True) -def test_autoimport_comment(completions): - assert len(completions) == 0 + def test_dot_partial(completions): + assert not should_insert("""str.metho\n""", 9) -@pytest.mark.parametrize("completions", [(""" # """, 5)], indirect=True) -def test_autoimport_comment_indent(completions): + def test_comment(completions): + assert not should_insert("""#""", 1) - assert len(completions) == 0 + def test_comment_indent(completions): + assert not should_insert(""" # """, 5) -@pytest.mark.parametrize("completions", [("""from """, 5)], indirect=True) -def test_autoimport_from(completions): - assert len(completions) == 0 - - -@pytest.mark.parametrize("completions", [("""from """, 4)], indirect=True) -def test_autoimport_from_(completions): - assert len(completions) > 0 + def test_from(completions): + assert not should_insert("""from """, 5) + assert should_insert("""from """, 4) def test_sort_sources(): From a5304ab1dee6bf23e0fe0ab764419c1b98bfabad Mon Sep 17 00:00:00 2001 From: bageljr Date: Wed, 25 May 2022 17:29:31 +0530 Subject: [PATCH 15/39] use type hint --- pylsp/plugins/rope_autoimport.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index 7e9dad33..52cd6019 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -6,6 +6,7 @@ from parso.python import tree from parso.tree import NodeOrLeaf from rope.base.resources import Resource +from rope.contrib.autoimport.defs import SearchResult from rope.contrib.autoimport.sqlite import AutoImport from pylsp import hookimpl @@ -106,7 +107,7 @@ def _handle_argument(node: NodeOrLeaf, word_node: tree.Leaf): def _process_statements( - suggestions: List, + suggestions: List[SearchResult], doc_uri: str, word: str, autoimport: AutoImport, From e63cddd94c1ecf57ada6b5ef1119a0665834b63d Mon Sep 17 00:00:00 2001 From: bageljr Date: Thu, 26 May 2022 07:34:01 +0530 Subject: [PATCH 16/39] Move autoimport object into workspace. Other smaller changes: - Allow memory only database for testing. - Configuration parameter for memory database --- CONFIGURATION.md | 1 + pylsp/config/schema.json | 4 ++ pylsp/plugins/rope_autoimport.py | 50 +++++++++---------- pylsp/python_lsp.py | 2 + pylsp/workspace.py | 81 +++++++++++++++++++++++-------- test/plugins/test_autoimport.py | 83 ++++++++++++++++++-------------- 6 files changed, 137 insertions(+), 84 deletions(-) diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 6636bbee..967f286b 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -59,6 +59,7 @@ This server can be configured using `workspace/didChangeConfiguration` method. E | `pylsp.plugins.pylint.args` | `array` of non-unique `string` items | Arguments to pass to pylint. | `null` | | `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 the plugin. | `false` | +| `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` | | `pylsp.plugins.yapf.enabled` | `boolean` | Enable or disable the plugin. | `true` | diff --git a/pylsp/config/schema.json b/pylsp/config/schema.json index 07a65b44..4088c177 100644 --- a/pylsp/config/schema.json +++ b/pylsp/config/schema.json @@ -334,6 +334,10 @@ "type": "boolean", "default": false, "description": "Enable or disable the plugin." + },"pylsp.plugins.rope_autoimport.memory": { + "type": "boolean", + "default": false, + "description": "Make the autoimport database memory only. Drastically increases startup time." }, "pylsp.plugins.rope_completion.enabled": { "type": "boolean", diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index 52cd6019..cfd70fe1 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -15,7 +15,6 @@ log = logging.getLogger(__name__) - _score_pow = 5 _score_max = 10**_score_pow @@ -23,7 +22,7 @@ @hookimpl def pylsp_settings() -> Dict[str, Dict[str, Dict[str, Any]]]: # Default rope_completion to disabled - return {"plugins": {"rope_autoimport": {"enabled": False}}} + return {"plugins": {"rope_autoimport": {"enabled": False, "memory": True}}} def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: @@ -51,9 +50,8 @@ def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: return _handle_first_child(first_child, expr, word_node) -def _handle_first_child( - first_child: NodeOrLeaf, expr: tree.BaseNode, word_node: tree.Leaf -) -> bool: +def _handle_first_child(first_child: NodeOrLeaf, expr: tree.BaseNode, + word_node: tree.Leaf) -> bool: """Check if we suggest imports given the following first child.""" if isinstance(first_child, tree.Import): return False @@ -125,7 +123,9 @@ def _process_statements( "label": name, "kind": itemkind, "sortText": _sort_import(score), - "data": {"doc_uri": doc_uri}, + "data": { + "doc_uri": doc_uri + }, "detail": _document(import_statement), "additionalTextEdits": [edit], } @@ -161,9 +161,8 @@ def get_names(file: str) -> Generator[str, None, None]: @hookimpl -def pylsp_completions( - config: Config, workspace: Workspace, document: Document, position -): +def pylsp_completions(config: Config, workspace: Workspace, document: Document, + position): """Get autoimport suggestions.""" line = document.lines[position["line"]] expr = parso.parse(line) @@ -171,18 +170,18 @@ def pylsp_completions( if not _should_insert(expr, word_node): return [] word = word_node.value + log.debug(f"autoimport: searching for word: {word}") rope_config = config.settings(document_path=document.path).get("rope", {}) - rope_project = workspace._rope_project_builder(rope_config) ignored_names: Set[str] = set(get_names(document.source)) - autoimport = AutoImport(rope_project, memory=False) - suggestions = list(autoimport.search_full(word, ignored_names=ignored_names)) - autoimport.close() + autoimport = workspace._rope_autoimport(rope_config) + 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), key=lambda statement: statement["sortText"], - ) - ) + )) max_size = 100 if len(results) > max_size: results = results[:max_size] @@ -193,12 +192,11 @@ def _document(import_statement: str) -> str: return "__autoimport__\n" + import_statement -def _get_score( - source: int, full_statement: str, suggested_name: str, desired_name -) -> int: +def _get_score(source: int, full_statement: str, suggested_name: str, + desired_name) -> int: import_length = len("import") full_statement_score = len(full_statement) - import_length - suggested_name_score = ((len(suggested_name) - len(desired_name))) ** 2 + suggested_name_score = ((len(suggested_name) - len(desired_name)))**2 source_score = 20 * source return suggested_name_score + full_statement_score + source_score @@ -214,22 +212,20 @@ def _sort_import(score: int) -> str: @hookimpl def pylsp_initialize(config: Config, workspace: Workspace): """Initialize AutoImport. Generates the cache for local and global items.""" + memory: bool = config.settings().get("rope_autoimport", {})["memory"] rope_config = config.settings().get("rope", {}) - rope_project = workspace._rope_project_builder(rope_config) - autoimport = AutoImport(rope_project, memory=False) + autoimport = workspace._rope_autoimport(rope_config, memory) autoimport.generate_modules_cache() autoimport.generate_cache() - autoimport.close() @hookimpl -def pylsp_document_did_save(config: Config, workspace: Workspace, document: Document): +def pylsp_document_did_save(config: Config, workspace: Workspace, + document: Document): """Update the names associated with this document.""" rope_config = config.settings().get("rope", {}) rope_doucment: Resource = document._rope_resource(rope_config) - rope_project = workspace._rope_project_builder(rope_config) - autoimport = AutoImport(rope_project, memory=False) + autoimport = workspace._rope_autoimport(rope_config) autoimport.generate_cache(resources=[rope_doucment]) # Might as well using saving the document as an indicator to regenerate the module cache autoimport.generate_modules_cache() - autoimport.close() diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index 7967e663..7f73d3d0 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -140,6 +140,8 @@ def m_shutdown(self, **_kwargs): self._shutdown = True def m_exit(self, **_kwargs): + for workspace in self.workspaces: + workspace.close() self._endpoint.shutdown() self._jsonrpc_stream_reader.close() self._jsonrpc_stream_writer.close() diff --git a/pylsp/workspace.py b/pylsp/workspace.py index 0a448c7b..aba27ba7 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -1,16 +1,16 @@ # Copyright 2017-2020 Palantir Technologies, Inc. # Copyright 2021- Python Language Server Contributors. +import functools import io import logging import os import re -import functools from threading import RLock - +from typing import Optional import jedi -from . import lsp, uris, _utils +from . import _utils, lsp, uris log = logging.getLogger(__name__) @@ -21,10 +21,12 @@ def lock(method): """Define an atomic region over a method.""" + @functools.wraps(method) def wrapper(self, *args, **kwargs): with self._lock: return method(self, *args, **kwargs) + return wrapper @@ -48,6 +50,14 @@ 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): + # pylint: disable=import-outside-toplevel + from rope.contrib.autoimport.sqlite import AutoImport + if self.__rope_autoimport is None: + project = self._rope_project_builder(rope_config) + self.__rope_autoimport = AutoImport(project, memory) + return self.__rope_autoimport def _rope_project_builder(self, rope_config): # pylint: disable=import-outside-toplevel @@ -60,7 +70,8 @@ def _rope_project_builder(self, rope_config): self.__rope = Project(self._root_path, ropefolder=rope_folder) else: self.__rope = Project(self._root_path) - self.__rope.prefs.set('extension_modules', rope_config.get('extensionModules', [])) + self.__rope.prefs.set('extension_modules', + rope_config.get('extensionModules', [])) self.__rope.prefs.set('ignore_syntax_errors', True) self.__rope.prefs.set('ignore_bad_imports', True) self.__rope.validate() @@ -79,7 +90,8 @@ def root_uri(self): return self._root_uri def is_local(self): - return (self._root_uri_scheme in ['', 'file']) and os.path.exists(self._root_path) + return (self._root_uri_scheme in ['', 'file']) and os.path.exists( + self._root_path) def get_document(self, doc_uri): """Return a managed document if-present, else create one pointing at disk. @@ -92,7 +104,9 @@ def get_maybe_document(self, doc_uri): return self._docs.get(doc_uri) def put_document(self, doc_uri, source, version=None): - self._docs[doc_uri] = self._create_document(doc_uri, source=source, version=version) + self._docs[doc_uri] = self._create_document(doc_uri, + source=source, + version=version) def rm_document(self, doc_uri): self._docs.pop(doc_uri) @@ -110,15 +124,25 @@ def apply_edit(self, edit): return self._endpoint.request(self.M_APPLY_EDIT, {'edit': edit}) def publish_diagnostics(self, doc_uri, diagnostics): - self._endpoint.notify(self.M_PUBLISH_DIAGNOSTICS, params={'uri': doc_uri, 'diagnostics': diagnostics}) + self._endpoint.notify(self.M_PUBLISH_DIAGNOSTICS, + params={ + 'uri': doc_uri, + 'diagnostics': diagnostics + }) def show_message(self, message, msg_type=lsp.MessageType.Info): - self._endpoint.notify(self.M_SHOW_MESSAGE, params={'type': msg_type, 'message': message}) + self._endpoint.notify(self.M_SHOW_MESSAGE, + params={ + 'type': msg_type, + 'message': message + }) def source_roots(self, document_path): """Return the source roots for the given document.""" - files = _utils.find_parents(self._root_path, document_path, ['setup.py', 'pyproject.toml']) or [] - return list({os.path.dirname(project_file) for project_file in files}) or [self._root_path] + files = _utils.find_parents(self._root_path, document_path, + ['setup.py', 'pyproject.toml']) or [] + return list({os.path.dirname(project_file) + for project_file in files}) or [self._root_path] def _create_document(self, doc_uri, source=None, version=None): path = uris.to_fs_path(doc_uri) @@ -130,11 +154,20 @@ def _create_document(self, doc_uri, source=None, version=None): extra_sys_path=self.source_roots(path), rope_project_builder=self._rope_project_builder, ) + def close(self): + if self._rope_autoimport is not None: + self.__rope_autoimport.close() class Document: - def __init__(self, uri, workspace, source=None, version=None, local=True, extra_sys_path=None, + def __init__(self, + uri, + workspace, + source=None, + version=None, + local=True, + extra_sys_path=None, rope_project_builder=None): self.uri = uri self.version = version @@ -157,7 +190,8 @@ def __str__(self): def _rope_resource(self, rope_config): # pylint: disable=import-outside-toplevel from rope.base import libutils - return libutils.path_to_resource(self._rope_project_builder(rope_config), self.path) + return libutils.path_to_resource( + self._rope_project_builder(rope_config), self.path) @property @lock @@ -221,7 +255,8 @@ def apply_change(self, change): def offset_at_position(self, position): """Return the byte-offset pointed at by the given position.""" - return position['character'] + len(''.join(self.lines[:position['line']])) + return position['character'] + len(''.join( + self.lines[:position['line']])) def word_at_position(self, position): """Get the word under the cursor returning the start and end positions.""" @@ -244,7 +279,8 @@ def word_at_position(self, position): @lock def jedi_names(self, all_scopes=False, definitions=True, references=False): script = self.jedi_script() - return script.get_names(all_scopes=all_scopes, definitions=definitions, + return script.get_names(all_scopes=all_scopes, + definitions=definitions, references=references) @lock @@ -254,7 +290,8 @@ def jedi_script(self, position=None, use_document_path=False): env_vars = None if self._config: - jedi_settings = self._config.plugin_settings('jedi', document_path=self.path) + jedi_settings = self._config.plugin_settings( + 'jedi', document_path=self.path) environment_path = jedi_settings.get('environment') extra_paths = jedi_settings.get('extra_paths') or [] env_vars = jedi_settings.get('env_vars') @@ -265,8 +302,10 @@ def jedi_script(self, position=None, use_document_path=False): env_vars = os.environ.copy() env_vars.pop('PYTHONPATH', None) - environment = self.get_enviroment(environment_path, env_vars=env_vars) if environment_path else None - sys_path = self.sys_path(environment_path, env_vars=env_vars) + extra_paths + environment = self.get_enviroment( + environment_path, env_vars=env_vars) if environment_path else None + sys_path = self.sys_path(environment_path, + env_vars=env_vars) + extra_paths project_path = self._workspace.root_path # Extend sys_path with document's path if requested @@ -294,9 +333,8 @@ def get_enviroment(self, environment_path=None, env_vars=None): if environment_path in self._workspace._environments: environment = self._workspace._environments[environment_path] else: - environment = jedi.api.environment.create_environment(path=environment_path, - safe=False, - env_vars=env_vars) + environment = jedi.api.environment.create_environment( + path=environment_path, safe=False, env_vars=env_vars) self._workspace._environments[environment_path] = environment return environment @@ -305,6 +343,7 @@ def sys_path(self, environment_path=None, env_vars=None): # Copy our extra sys path # TODO: when safe to break API, use env_vars explicitly to pass to create_environment path = list(self._extra_sys_path) - environment = self.get_enviroment(environment_path=environment_path, env_vars=env_vars) + environment = self.get_enviroment(environment_path=environment_path, + env_vars=env_vars) path.extend(environment.get_sys_path()) return path diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index b1dae48c..b0c98a53 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -8,21 +8,24 @@ from pylsp.plugins.rope_autoimport import _get_score, _should_insert, get_names from pylsp.plugins.rope_autoimport import \ pylsp_completions as pylsp_autoimport_completions +from pylsp.plugins.rope_autoimport import pylsp_initialize from pylsp.workspace import Workspace DOC_URI = uris.from_fs_path(__file__) -# pylint: disable=redefined-outer-name - +# pylint: disable=redefined-outer-name @pytest.fixture def completions(config: Config, workspace: Workspace, request): + config.update({"rope_autoimport": {"memory": True, "enabled": True}}) + pylsp_initialize(config, workspace) document, position = request.param com_position = {"line": 0, "character": position} workspace.put_document(DOC_URI, source=document) doc = workspace.get_document(DOC_URI) yield pylsp_autoimport_completions(config, workspace, doc, com_position) workspace.rm_document(DOC_URI) + workspace.close() def should_insert(phrase: str, position: int): @@ -42,8 +45,10 @@ def check_dict(query: Dict, results: List[Dict]) -> bool: def test_autoimport_completion(completions): assert completions assert check_dict( - {"label": "pathlib", "kind": lsp.CompletionItemKind.Module}, completions - ) + { + "label": "pathlib", + "kind": lsp.CompletionItemKind.Module + }, completions) @pytest.mark.parametrize("completions", [("""import """, 7)], indirect=True) @@ -57,58 +62,62 @@ def test_autoimport_pathlib(completions): start = {"line": 0, "character": 0} edit_range = {"start": start, "end": start} - assert completions[0]["additionalTextEdits"] == [ - {"range": edit_range, "newText": "import pathlib\n"} - ] + assert completions[0]["additionalTextEdits"] == [{ + "range": + edit_range, + "newText": + "import pathlib\n" + }] -@pytest.mark.parametrize("completions", [("""import test\n""", 10)], indirect=True) +@pytest.mark.parametrize("completions", [("""import test\n""", 10)], + indirect=True) def test_autoimport_import_with_name(completions): assert len(completions) == 0 -@pytest.mark.parametrize("completions", [("""def func(s""", 10)], indirect=True) +@pytest.mark.parametrize("completions", [("""def func(s""", 10)], + indirect=True) def test_autoimport_function(completions): assert len(completions) == 0 -@pytest.mark.parametrize("completions", [("""class Test""", 10)], indirect=True) +@pytest.mark.parametrize("completions", [("""class Test""", 10)], + indirect=True) def test_autoimport_class(completions): assert len(completions) == 0 -@pytest.mark.parametrize( - "completions", [("""class Test(NamedTupl):""", 20)], indirect=True -) +@pytest.mark.parametrize("completions", [("""class Test(NamedTupl):""", 20)], + indirect=True) def test_autoimport_class_complete(completions): assert len(completions) > 0 -@pytest.mark.parametrize( - "completions", [("""class Test(NamedTupl""", 20)], indirect=True -) +@pytest.mark.parametrize("completions", [("""class Test(NamedTupl""", 20)], + indirect=True) def test_autoimport_class_incomplete(completions): assert len(completions) > 0 -@pytest.mark.parametrize("completions", [("""def func(s:Lis""", 12)], indirect=True) +@pytest.mark.parametrize("completions", [("""def func(s:Lis""", 12)], + indirect=True) def test_autoimport_function_typing(completions): assert len(completions) > 0 assert check_dict({"label": "List"}, completions) -@pytest.mark.parametrize( - "completions", [("""def func(s : Lis ):""", 16)], indirect=True -) +@pytest.mark.parametrize("completions", [("""def func(s : Lis ):""", 16)], + indirect=True) def test_autoimport_function_typing_complete(completions): assert len(completions) > 0 assert check_dict({"label": "List"}, completions) -@pytest.mark.parametrize( - "completions", [("""def func(s : Lis ) -> Generat:""", 29)], indirect=True -) +@pytest.mark.parametrize("completions", + [("""def func(s : Lis ) -> Generat:""", 29)], + indirect=True) def test_autoimport_function_typing_return(completions): assert len(completions) > 0 assert check_dict({"label": "Generator"}, completions) @@ -119,7 +128,8 @@ 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) workspace.rm_document(DOC_URI) assert not check_dict({"label": "List"}, completions) @@ -158,23 +168,25 @@ def test_autoimport_defined_name(config, workspace): # workspace.rm_document(DOC_URI) +# pylint: disable=no-self-use class test_should_insert: - def test_dot(completions): + + def test_dot(self): assert not should_insert("""str.""", 4) - def test_dot_partial(completions): + def test_dot_partial(self): assert not should_insert("""str.metho\n""", 9) - def test_comment(completions): + def test_comment(self): assert not should_insert("""#""", 1) - def test_comment_indent(completions): + def test_comment_indent(self): assert not should_insert(""" # """, 5) - def test_from(completions): + def test_from(self): assert not should_insert("""from """, 5) assert should_insert("""from """, 4) @@ -186,17 +198,15 @@ def test_sort_sources(): def test_sort_statements(): - result1 = _get_score( - 2, "from importlib_metadata import pathlib", "pathlib", "pathli" - ) + result1 = _get_score(2, "from importlib_metadata import pathlib", + "pathlib", "pathli") result2 = _get_score(2, "import pathlib", "pathlib", "pathli") assert result1 > result2 def test_sort_both(): - result1 = _get_score( - 3, "from importlib_metadata import pathlib", "pathlib", "pathli" - ) + result1 = _get_score(3, "from importlib_metadata import pathlib", + "pathlib", "pathli") result2 = _get_score(2, "import pathlib", "pathlib", "pathli") assert result1 > result2 @@ -213,4 +223,5 @@ class sfa: sfiosifo """ results = set(get_names(source)) - assert results == set(["blah", "bleh", "e", "hello", "someone", "sfa", "a", "b"]) + assert results == set( + ["blah", "bleh", "e", "hello", "someone", "sfa", "a", "b"]) From a54c86a3e3f0e58def184a51ad5f0eafe8a8d9ba Mon Sep 17 00:00:00 2001 From: bageljr Date: Thu, 26 May 2022 07:39:20 +0530 Subject: [PATCH 17/39] format --- pylsp/workspace.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pylsp/workspace.py b/pylsp/workspace.py index aba27ba7..b3eac12c 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -51,6 +51,7 @@ def __init__(self, root_uri, endpoint, config=None): self.__rope = None self.__rope_config = None self.__rope_autoimport = None + def _rope_autoimport(self, rope_config: Optional, memory: bool = False): # pylint: disable=import-outside-toplevel from rope.contrib.autoimport.sqlite import AutoImport @@ -154,6 +155,7 @@ def _create_document(self, doc_uri, source=None, version=None): extra_sys_path=self.source_roots(path), rope_project_builder=self._rope_project_builder, ) + def close(self): if self._rope_autoimport is not None: self.__rope_autoimport.close() From 0c6b645c9d2f83fc4eba2cd0dcc764f6672d719f Mon Sep 17 00:00:00 2001 From: bageljr Date: Thu, 26 May 2022 11:11:44 +0530 Subject: [PATCH 18/39] fix closing issues --- pylsp/python_lsp.py | 4 ++-- pylsp/workspace.py | 2 +- pyproject.toml | 4 ++++ test/fixtures.py | 3 ++- test/plugins/test_autoimport.py | 1 - 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index 7f73d3d0..2220d51b 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -137,11 +137,11 @@ def __getitem__(self, item): raise KeyError() def m_shutdown(self, **_kwargs): + for workspace in self.workspaces.values(): + workspace.close() self._shutdown = True def m_exit(self, **_kwargs): - for workspace in self.workspaces: - workspace.close() self._endpoint.shutdown() self._jsonrpc_stream_reader.close() self._jsonrpc_stream_writer.close() diff --git a/pylsp/workspace.py b/pylsp/workspace.py index b3eac12c..7cfab70a 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -157,7 +157,7 @@ def _create_document(self, doc_uri, source=None, version=None): ) def close(self): - if self._rope_autoimport is not None: + if self.__rope_autoimport is not None: self.__rope_autoimport.close() diff --git a/pyproject.toml b/pyproject.toml index a5018c69..9d7c2a5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,3 +98,7 @@ write_to_template = "__version__ = \"{version}\"\n" # VERSION_INFO is populated [tool.pytest.ini_options] testpaths = ["test"] addopts = "--cov-report html --cov-report term --junitxml=pytest.xml --cov pylsp --cov test" + +[tool.coverage.run] +concurrency = ["multiprocessing", "thread"] + diff --git a/test/fixtures.py b/test/fixtures.py index e57bda6b..ad1f8ce3 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -67,7 +67,8 @@ def workspace(tmpdir): """Return a workspace.""" ws = Workspace(uris.from_fs_path(str(tmpdir)), Mock()) ws._config = Config(ws.root_uri, {}, 0, {}) - return ws + yield ws + ws.close() @pytest.fixture diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index b0c98a53..1e62003d 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -25,7 +25,6 @@ def completions(config: Config, workspace: Workspace, request): doc = workspace.get_document(DOC_URI) yield pylsp_autoimport_completions(config, workspace, doc, com_position) workspace.rm_document(DOC_URI) - workspace.close() def should_insert(phrase: str, position: int): From 20d36de97b6019aca2926413a47a7933de850de0 Mon Sep 17 00:00:00 2001 From: Bagel Jr <57874654+bageljrkhanofemus@users.noreply.github.com> Date: Wed, 22 Jun 2022 09:07:23 -0500 Subject: [PATCH 19/39] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9d7c2a5d..44799873 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ pycodestyle = ["pycodestyle>=2.8.0,<2.9.0"] pydocstyle = ["pydocstyle>=2.0.0"] pyflakes = ["pyflakes>=2.4.0,<2.5.0"] pylint = ["pylint>=2.5.0"] -rope = ["rope>1.1.1"] +rope = ["rope>1.2.0"] yapf = ["yapf"] test = [ "pylint>=2.5.0", From f951c487bc6a9c0c9154b348fbfe60683d2861bb Mon Sep 17 00:00:00 2001 From: bageljr Date: Sun, 3 Jul 2022 14:24:47 -0500 Subject: [PATCH 20/39] fix: config --- pylsp/plugins/rope_autoimport.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index cfd70fe1..962940cd 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -22,7 +22,7 @@ @hookimpl def pylsp_settings() -> Dict[str, Dict[str, Dict[str, Any]]]: # Default rope_completion to disabled - return {"plugins": {"rope_autoimport": {"enabled": False, "memory": True}}} + return {"plugins": {"rope_autoimport": {"enabled": True, "memory": False}}} def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: @@ -212,7 +212,7 @@ def _sort_import(score: int) -> str: @hookimpl def pylsp_initialize(config: Config, workspace: Workspace): """Initialize AutoImport. Generates the cache for local and global items.""" - memory: bool = config.settings().get("rope_autoimport", {})["memory"] + memory: bool = config.plugin_settings("rope_autoimport").get("memory", False) rope_config = config.settings().get("rope", {}) autoimport = workspace._rope_autoimport(rope_config, memory) autoimport.generate_modules_cache() From 3214d8fac12729021ae8e99ce9222b2acf6528ca Mon Sep 17 00:00:00 2001 From: bageljr Date: Sun, 3 Jul 2022 14:34:42 -0500 Subject: [PATCH 21/39] fix: respect memory preference --- pylsp/workspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylsp/workspace.py b/pylsp/workspace.py index 7cfab70a..d3b0f810 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -57,7 +57,7 @@ def _rope_autoimport(self, rope_config: Optional, memory: bool = False): from rope.contrib.autoimport.sqlite import AutoImport if self.__rope_autoimport is None: project = self._rope_project_builder(rope_config) - self.__rope_autoimport = AutoImport(project, memory) + self.__rope_autoimport = AutoImport(project, memory=memory) return self.__rope_autoimport def _rope_project_builder(self, rope_config): From 6009cfa2c9dd60d799562ebcecc0f0f5ba78cb55 Mon Sep 17 00:00:00 2001 From: bageljr Date: Mon, 4 Jul 2022 12:36:04 -0500 Subject: [PATCH 22/39] fix: pylint errors --- pylsp/config/config.py | 1 - pylsp/python_lsp.py | 3 +-- test/plugins/test_autoimport.py | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pylsp/config/config.py b/pylsp/config/config.py index 5637ca60..27a76bde 100644 --- a/pylsp/config/config.py +++ b/pylsp/config/config.py @@ -81,7 +81,6 @@ def __init__(self, root_uri, init_opts, process_id, capabilities): if plugin is not None: log.info("Loaded pylsp plugin %s from %s", name, plugin) - # pylint: disable=no-member for plugin_conf in self._pm.hook.pylsp_settings(config=self): self._plugin_settings = _utils.merge_dicts(self._plugin_settings, plugin_conf) diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index 8969ca6e..ff7efda5 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -34,7 +34,6 @@ class _StreamHandlerWrapper(socketserver.StreamRequestHandler): def setup(self): super().setup() - # pylint: disable=no-member self.delegate = self.DELEGATE_CLASS(self.rfile, self.wfile) def handle(self): @@ -48,7 +47,6 @@ def handle(self): if isinstance(e, WindowsError) and e.winerror == 10054: pass - # pylint: disable=no-member self.SHUTDOWN_CALL() @@ -140,6 +138,7 @@ def send_message(message, websocket): log.exception("Failed to write message %s, %s", message, str(e)) async def run_server(): + # pylint: disable=no-member async with websockets.serve(pylsp_ws, port=port): # runs forever await asyncio.Future() diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index 1e62003d..11101c8c 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -167,7 +167,6 @@ def test_autoimport_defined_name(config, workspace): # workspace.rm_document(DOC_URI) -# pylint: disable=no-self-use class test_should_insert: def test_dot(self): From 2f09dd665d042a5c6e56947d40341185125e2983 Mon Sep 17 00:00:00 2001 From: bageljr Date: Mon, 4 Jul 2022 12:48:30 -0500 Subject: [PATCH 23/39] Make test data persist --- test/plugins/test_autoimport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index 11101c8c..1dbf97eb 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -17,7 +17,7 @@ # pylint: disable=redefined-outer-name @pytest.fixture def completions(config: Config, workspace: Workspace, request): - config.update({"rope_autoimport": {"memory": True, "enabled": True}}) + config.update({"rope_autoimport": {"memory": False, "enabled": True}}) pylsp_initialize(config, workspace) document, position = request.param com_position = {"line": 0, "character": position} From e640c53df18a6956c23a968e7d7a86a026b1fc0e Mon Sep 17 00:00:00 2001 From: bagel897 Date: Sat, 27 Aug 2022 14:07:39 -0500 Subject: [PATCH 24/39] Switch to jedi get_names --- pylsp/plugins/rope_autoimport.py | 34 ++++++-------------------------- test/plugins/test_autoimport.py | 6 +++--- 2 files changed, 9 insertions(+), 31 deletions(-) diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index 962940cd..33fce9a0 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -1,8 +1,8 @@ import logging -from functools import lru_cache from typing import Any, Dict, Generator, List, Set import parso +from jedi import Script from parso.python import tree from parso.tree import NodeOrLeaf from rope.base.resources import Resource @@ -131,33 +131,11 @@ def _process_statements( } -def _get_names_from_import(node: tree.Import) -> Generator[str, None, None]: - if not node.is_star_import(): - for name in node.children: - if isinstance(name, tree.PythonNode): - for sub_name in name.children: - if isinstance(sub_name, tree.Name): - yield sub_name.value - elif isinstance(name, tree.Name): - yield name.value - - -@lru_cache(maxsize=100) -def get_names(file: str) -> Generator[str, None, None]: +def get_names(script: Script) -> Set[str]: """Get all names to ignore from the current file.""" - expr = parso.parse(file) - for item in expr.children: - if isinstance(item, tree.PythonNode): - for child in item.children: - if isinstance(child, (tree.ImportFrom, tree.ExprStmt)): - for name in child.get_defined_names(): - yield name.value - elif isinstance(child, tree.Import): - for name in _get_names_from_import(child): - yield name - - if isinstance(item, (tree.Function, tree.Class)): - yield item.name.value + raw_names = script.get_names(definitions=True) + log.debug(raw_names) + return set(name.name for name in raw_names) @hookimpl @@ -172,7 +150,7 @@ def pylsp_completions(config: Config, workspace: Workspace, document: Document, 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] = set(get_names(document.source)) + ignored_names: Set[str] = 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)) diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index 1dbf97eb..56867ab1 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -10,14 +10,14 @@ pylsp_completions as pylsp_autoimport_completions from pylsp.plugins.rope_autoimport import pylsp_initialize from pylsp.workspace import Workspace - +import jedi DOC_URI = uris.from_fs_path(__file__) # pylint: disable=redefined-outer-name @pytest.fixture def completions(config: Config, workspace: Workspace, request): - config.update({"rope_autoimport": {"memory": False, "enabled": True}}) + config.update({"rope_autoimport": {"memory": True, "enabled": True}}) pylsp_initialize(config, workspace) document, position = request.param com_position = {"line": 0, "character": position} @@ -220,6 +220,6 @@ def someone(): class sfa: sfiosifo """ - results = set(get_names(source)) + results = get_names(jedi.Script(code=source)) assert results == set( ["blah", "bleh", "e", "hello", "someone", "sfa", "a", "b"]) From c3901bad0d97bf1c25fd8fe2ce53522e060d99f3 Mon Sep 17 00:00:00 2001 From: bagel897 Date: Sat, 27 Aug 2022 14:24:46 -0500 Subject: [PATCH 25/39] tests: use session scoped workspace --- test/plugins/test_autoimport.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index 56867ab1..bc187e00 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -11,20 +11,29 @@ from pylsp.plugins.rope_autoimport import pylsp_initialize from pylsp.workspace import Workspace import jedi +from unittest.mock import Mock DOC_URI = uris.from_fs_path(__file__) -# pylint: disable=redefined-outer-name +@pytest.fixture(scope="session") +def autoimport_workspace(tmp_path_factory) -> Workspace: + "Special autoimport workspace. Persists across sessions to make in-memory sqlite3 database fast." + workspace = 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}}) + pylsp_initialize(workspace._config, workspace) + yield workspace + workspace.close() + + @pytest.fixture -def completions(config: Config, workspace: Workspace, request): - config.update({"rope_autoimport": {"memory": True, "enabled": True}}) - pylsp_initialize(config, workspace) +def completions(config: Config, autoimport_workspace: Workspace, request): document, position = request.param com_position = {"line": 0, "character": position} - workspace.put_document(DOC_URI, source=document) - doc = workspace.get_document(DOC_URI) - yield pylsp_autoimport_completions(config, workspace, doc, com_position) - workspace.rm_document(DOC_URI) + 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) + autoimport_workspace.rm_document(DOC_URI) def should_insert(phrase: str, position: int): From 36dee07998cbaee79b1f48d8c8b2543acd38daae Mon Sep 17 00:00:00 2001 From: bagel897 Date: Sat, 27 Aug 2022 14:27:27 -0500 Subject: [PATCH 26/39] fix pylint errors --- pylsp/python_lsp.py | 1 - test/plugins/test_autoimport.py | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index ff7efda5..e663a8a0 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -138,7 +138,6 @@ def send_message(message, websocket): log.exception("Failed to write message %s, %s", message, str(e)) async def run_server(): - # pylint: disable=no-member async with websockets.serve(pylsp_ws, port=port): # runs forever await asyncio.Future() diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index bc187e00..2a72b1a2 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -1,5 +1,7 @@ from typing import Dict, List +from unittest.mock import Mock +import jedi import parso import pytest @@ -10,8 +12,7 @@ pylsp_completions as pylsp_autoimport_completions from pylsp.plugins.rope_autoimport import pylsp_initialize from pylsp.workspace import Workspace -import jedi -from unittest.mock import Mock + DOC_URI = uris.from_fs_path(__file__) @@ -26,6 +27,7 @@ def autoimport_workspace(tmp_path_factory) -> Workspace: workspace.close() +# pylint: disable=redefined-outer-name @pytest.fixture def completions(config: Config, autoimport_workspace: Workspace, request): document, position = request.param From 332217f19e769545c2b1fd4726bc28dd47316189 Mon Sep 17 00:00:00 2001 From: bagel897 Date: Sat, 27 Aug 2022 15:03:17 -0500 Subject: [PATCH 27/39] Use MAX_SIZE, don't use tuple unpacking --- pylsp/plugins/rope_autoimport.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index 33fce9a0..edb580a7 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -17,6 +17,7 @@ _score_pow = 5 _score_max = 10**_score_pow +MAX_RESULTS = 1000 @hookimpl @@ -111,23 +112,25 @@ def _process_statements( autoimport: AutoImport, document: Document, ) -> Generator[Dict[str, Any], None, None]: - for import_statement, name, source, itemkind in suggestions: + for suggestion in suggestions: insert_line = autoimport.find_insertion_line(document.source) - 1 start = {"line": insert_line, "character": 0} edit_range = {"start": start, "end": start} - edit = {"range": edit_range, "newText": import_statement + "\n"} - score = _get_score(source, import_statement, name, word) + edit = {"range": edit_range, "newText": suggestion.import_statement + "\n"} + score = _get_score(suggestion.source, suggestion.import_statement, suggestion.name, word) if score > _score_max: continue + # TODO make this markdown yield { - "label": name, - "kind": itemkind, + "label": suggestion.name, + "kind": suggestion.itemkind, "sortText": _sort_import(score), "data": { "doc_uri": doc_uri }, - "detail": _document(import_statement), + "detail": _document(suggestion.import_statement), "additionalTextEdits": [edit], + } @@ -160,14 +163,13 @@ def pylsp_completions(config: Config, workspace: Workspace, document: Document, document), key=lambda statement: statement["sortText"], )) - max_size = 100 - if len(results) > max_size: - results = results[:max_size] + if len(results) > MAX_RESULTS: + results = results[:MAX_RESULTS] return results def _document(import_statement: str) -> str: - return "__autoimport__\n" + import_statement + return """# Auto-Import\n""" + import_statement def _get_score(source: int, full_statement: str, suggested_name: str, From bb529ff68bf0f0fc1efbd838c8dc793865ce6e8d Mon Sep 17 00:00:00 2001 From: Bagel <57874654+bagel897@users.noreply.github.com> Date: Wed, 2 Nov 2022 14:00:48 -0500 Subject: [PATCH 28/39] Use snake-cased name Co-authored-by: Carlos Cordoba --- test/plugins/test_autoimport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index 2a72b1a2..cb81ba31 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -178,7 +178,7 @@ def test_autoimport_defined_name(config, workspace): # workspace.rm_document(DOC_URI) -class test_should_insert: +class TestShouldInsert: def test_dot(self): From 66642584836f3a326726d4591513f1f85cdadc1c Mon Sep 17 00:00:00 2001 From: Bagel <57874654+bagel897@users.noreply.github.com> Date: Wed, 2 Nov 2022 14:01:35 -0500 Subject: [PATCH 29/39] Update pylsp/config/schema.json Co-authored-by: Carlos Cordoba --- pylsp/config/schema.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pylsp/config/schema.json b/pylsp/config/schema.json index 1a38882f..aa3d3178 100644 --- a/pylsp/config/schema.json +++ b/pylsp/config/schema.json @@ -353,11 +353,13 @@ "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": { + }, + "pylsp.plugins.rope_autoimport.enabled": { "type": "boolean", "default": false, - "description": "Enable or disable the plugin." - },"pylsp.plugins.rope_autoimport.memory": { + "description": "Enable or disable autoimport." + }, + "pylsp.plugins.rope_autoimport.memory": { "type": "boolean", "default": false, "description": "Make the autoimport database memory only. Drastically increases startup time." From f5a2992d57c96e66191a04691f7947b01e28af27 Mon Sep 17 00:00:00 2001 From: bagel897 Date: Wed, 2 Nov 2022 23:01:00 -0500 Subject: [PATCH 30/39] Formatting changes --- docs/autoimport.md | 25 +++++++----- pylsp/plugins/rope_autoimport.py | 1 - pylsp/workspace.py | 67 ++++++++++---------------------- 3 files changed, 36 insertions(+), 57 deletions(-) diff --git a/docs/autoimport.md b/docs/autoimport.md index bc7f372d..832de652 100644 --- a/docs/autoimport.md +++ b/docs/autoimport.md @@ -1,18 +1,25 @@ # Autoimport for pylsp + Requirements: -1. install ``python-lsp-server[rope]`` -2. set ``pylsp.plugins.rope_autoimport.enabled`` to ``true`` + +1. install `python-lsp-server[rope]` +2. set `pylsp.plugins.rope_autoimport.enabled` to `true` + ## Startup + Autoimport will generate an autoimport sqllite3 database in .ropefolder/autoimport.db on startup. This will take a few seconds but should be much quicker on future runs. -## Usage -Autoimport will provide suggestions to import names from everything in ``sys.path``. You can change this by changing where pylsp is running or by setting rope's 'python_path' option. -It will suggest modules, submodules, keywords, functions, and classes. + +## Usage + +Autoimport will provide suggestions to import names from everything in `sys.path`. You can change this by changing where pylsp is running or by setting rope's 'python_path' option. +It will suggest modules, submodules, keywords, functions, and classes. Since autoimport inserts everything towards the end of the import group, its recommended you use the isort [plugin](https://github.com/paradoxxxzero/pyls-isort). ## Credits - - Most of the code was written by me, @bageljrkhanofemus - - [lyz-code](https://github.com/lyz-code/autoimport) for inspiration and some ideas - - [rope](https://github.com/python-rope/rope), especially @lieryan - - [pyright](https://github.com/Microsoft/pyright) for details on language server implementation + +- Most of the code was written by me, @bageljrkhanofemus +- [lyz-code](https://github.com/lyz-code/autoimport) for inspiration and some ideas +- [rope](https://github.com/python-rope/rope), especially @lieryan +- [pyright](https://github.com/Microsoft/pyright) for details on language server implementation diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index edb580a7..0eac490e 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -25,7 +25,6 @@ def pylsp_settings() -> Dict[str, Dict[str, Dict[str, Any]]]: # Default rope_completion to disabled return {"plugins": {"rope_autoimport": {"enabled": True, "memory": False}}} - def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: """ Check if we should insert the word_node on the given expr. diff --git a/pylsp/workspace.py b/pylsp/workspace.py index d3b0f810..fdeb070b 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -1,16 +1,16 @@ # Copyright 2017-2020 Palantir Technologies, Inc. # Copyright 2021- Python Language Server Contributors. -import functools import io import logging import os import re +import functools from threading import RLock -from typing import Optional + import jedi -from . import _utils, lsp, uris +from . import lsp, uris, _utils log = logging.getLogger(__name__) @@ -21,12 +21,10 @@ def lock(method): """Define an atomic region over a method.""" - @functools.wraps(method) def wrapper(self, *args, **kwargs): with self._lock: return method(self, *args, **kwargs) - return wrapper @@ -91,8 +89,7 @@ def root_uri(self): return self._root_uri def is_local(self): - return (self._root_uri_scheme in ['', 'file']) and os.path.exists( - self._root_path) + return (self._root_uri_scheme in ['', 'file']) and os.path.exists(self._root_path) def get_document(self, doc_uri): """Return a managed document if-present, else create one pointing at disk. @@ -105,9 +102,7 @@ def get_maybe_document(self, doc_uri): return self._docs.get(doc_uri) def put_document(self, doc_uri, source, version=None): - self._docs[doc_uri] = self._create_document(doc_uri, - source=source, - version=version) + self._docs[doc_uri] = self._create_document(doc_uri, source=source, version=version) def rm_document(self, doc_uri): self._docs.pop(doc_uri) @@ -125,25 +120,15 @@ def apply_edit(self, edit): return self._endpoint.request(self.M_APPLY_EDIT, {'edit': edit}) def publish_diagnostics(self, doc_uri, diagnostics): - self._endpoint.notify(self.M_PUBLISH_DIAGNOSTICS, - params={ - 'uri': doc_uri, - 'diagnostics': diagnostics - }) + self._endpoint.notify(self.M_PUBLISH_DIAGNOSTICS, params={'uri': doc_uri, 'diagnostics': diagnostics}) def show_message(self, message, msg_type=lsp.MessageType.Info): - self._endpoint.notify(self.M_SHOW_MESSAGE, - params={ - 'type': msg_type, - 'message': message - }) + self._endpoint.notify(self.M_SHOW_MESSAGE, params={'type': msg_type, 'message': message}) def source_roots(self, document_path): """Return the source roots for the given document.""" - files = _utils.find_parents(self._root_path, document_path, - ['setup.py', 'pyproject.toml']) or [] - return list({os.path.dirname(project_file) - for project_file in files}) or [self._root_path] + files = _utils.find_parents(self._root_path, document_path, ['setup.py', 'pyproject.toml']) or [] + return list({os.path.dirname(project_file) for project_file in files}) or [self._root_path] def _create_document(self, doc_uri, source=None, version=None): path = uris.to_fs_path(doc_uri) @@ -163,13 +148,7 @@ def close(self): class Document: - def __init__(self, - uri, - workspace, - source=None, - version=None, - local=True, - extra_sys_path=None, + def __init__(self, uri, workspace, source=None, version=None, local=True, extra_sys_path=None, rope_project_builder=None): self.uri = uri self.version = version @@ -192,8 +171,7 @@ def __str__(self): def _rope_resource(self, rope_config): # pylint: disable=import-outside-toplevel from rope.base import libutils - return libutils.path_to_resource( - self._rope_project_builder(rope_config), self.path) + return libutils.path_to_resource(self._rope_project_builder(rope_config), self.path) @property @lock @@ -257,8 +235,7 @@ def apply_change(self, change): def offset_at_position(self, position): """Return the byte-offset pointed at by the given position.""" - return position['character'] + len(''.join( - self.lines[:position['line']])) + return position['character'] + len(''.join(self.lines[:position['line']])) def word_at_position(self, position): """Get the word under the cursor returning the start and end positions.""" @@ -281,8 +258,7 @@ def word_at_position(self, position): @lock def jedi_names(self, all_scopes=False, definitions=True, references=False): script = self.jedi_script() - return script.get_names(all_scopes=all_scopes, - definitions=definitions, + return script.get_names(all_scopes=all_scopes, definitions=definitions, references=references) @lock @@ -292,8 +268,7 @@ def jedi_script(self, position=None, use_document_path=False): env_vars = None if self._config: - jedi_settings = self._config.plugin_settings( - 'jedi', document_path=self.path) + jedi_settings = self._config.plugin_settings('jedi', document_path=self.path) environment_path = jedi_settings.get('environment') extra_paths = jedi_settings.get('extra_paths') or [] env_vars = jedi_settings.get('env_vars') @@ -304,10 +279,8 @@ def jedi_script(self, position=None, use_document_path=False): env_vars = os.environ.copy() env_vars.pop('PYTHONPATH', None) - environment = self.get_enviroment( - environment_path, env_vars=env_vars) if environment_path else None - sys_path = self.sys_path(environment_path, - env_vars=env_vars) + extra_paths + environment = self.get_enviroment(environment_path, env_vars=env_vars) if environment_path else None + sys_path = self.sys_path(environment_path, env_vars=env_vars) + extra_paths project_path = self._workspace.root_path # Extend sys_path with document's path if requested @@ -335,8 +308,9 @@ def get_enviroment(self, environment_path=None, env_vars=None): if environment_path in self._workspace._environments: environment = self._workspace._environments[environment_path] else: - environment = jedi.api.environment.create_environment( - path=environment_path, safe=False, env_vars=env_vars) + environment = jedi.api.environment.create_environment(path=environment_path, + safe=False, + env_vars=env_vars) self._workspace._environments[environment_path] = environment return environment @@ -345,7 +319,6 @@ def sys_path(self, environment_path=None, env_vars=None): # Copy our extra sys path # TODO: when safe to break API, use env_vars explicitly to pass to create_environment path = list(self._extra_sys_path) - environment = self.get_enviroment(environment_path=environment_path, - env_vars=env_vars) + environment = self.get_enviroment(environment_path=environment_path, env_vars=env_vars) path.extend(environment.get_sys_path()) return path From 76f68de3a9b56c9f3ee014bee0da0bd141f8547a Mon Sep 17 00:00:00 2001 From: bagel897 Date: Wed, 2 Nov 2022 23:02:27 -0500 Subject: [PATCH 31/39] Add copyright headers --- pylsp/plugins/rope_autoimport.py | 3 +++ test/plugins/test_autoimport.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index 0eac490e..c9c083e3 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -1,3 +1,5 @@ +# Copyright 2022- Python Language Server Contributors. + import logging from typing import Any, Dict, Generator, List, Set @@ -25,6 +27,7 @@ def pylsp_settings() -> Dict[str, Dict[str, Dict[str, Any]]]: # Default rope_completion to disabled return {"plugins": {"rope_autoimport": {"enabled": True, "memory": False}}} + def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: """ Check if we should insert the word_node on the given expr. diff --git a/test/plugins/test_autoimport.py b/test/plugins/test_autoimport.py index 2a72b1a2..4570d5e1 100644 --- a/test/plugins/test_autoimport.py +++ b/test/plugins/test_autoimport.py @@ -1,3 +1,5 @@ +# Copyright 2022- Python Language Server Contributors. + from typing import Dict, List from unittest.mock import Mock From 4bc83c9f2417ad893cddc32275ffb8d0b64a53a5 Mon Sep 17 00:00:00 2001 From: bagel897 Date: Wed, 2 Nov 2022 23:06:17 -0500 Subject: [PATCH 32/39] Restore Optional Import --- pylsp/workspace.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pylsp/workspace.py b/pylsp/workspace.py index fdeb070b..eae1a767 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -7,6 +7,7 @@ import re import functools from threading import RLock +from typing import Optional import jedi From bdbe5b1d33083989e3cd1d664c2ae51614e09d83 Mon Sep 17 00:00:00 2001 From: bagel897 Date: Wed, 2 Nov 2022 23:08:11 -0500 Subject: [PATCH 33/39] update configuration --- CONFIGURATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 6e96dbd7..91553698 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -60,7 +60,7 @@ This server can be configured using `workspace/didChangeConfiguration` method. E | `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 the plugin. | `false` | +| `pylsp.plugins.rope_autoimport.enabled` | `boolean` | Enable or disable autoimport. | `false` | | `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` | From a8814981c6447dc6b352bbbb05db18c3df82aeb0 Mon Sep 17 00:00:00 2001 From: bagel897 Date: Wed, 2 Nov 2022 23:10:41 -0500 Subject: [PATCH 34/39] Update dep names --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0b329a46..d07cfc4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ all = [ "pydocstyle>=2.0.0", "pyflakes>=2.4.0,<2.5.0", "pylint>=2.5.0", - "rope>=1.1.1", + "rope>1.2.0", "yapf", "whatthepatch" ] From 7f40c74e7087d8c5ee8c58040489d623d488c744 Mon Sep 17 00:00:00 2001 From: bagel897 Date: Wed, 2 Nov 2022 23:12:12 -0500 Subject: [PATCH 35/39] style: remove extra line --- pylsp/plugins/rope_autoimport.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index c9c083e3..2aef7aa9 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -132,7 +132,6 @@ def _process_statements( }, "detail": _document(suggestion.import_statement), "additionalTextEdits": [edit], - } From 72f3a7c4cc9f3b9adeeb1cfa50c3d992c5a9038d Mon Sep 17 00:00:00 2001 From: bagel897 Date: Wed, 2 Nov 2022 23:32:48 -0500 Subject: [PATCH 36/39] fix: single . handling --- pylsp/plugins/rope_autoimport.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index 2aef7aa9..7cbe57ef 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -44,7 +44,8 @@ def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: if first_child == word_node: return True # If the word is the first word then its fine if len(expr.children) > 1: - if any(node.type == "trailer" for node in expr.children): + if any(node.type == "operator" and "." in node.value + or node.type == "trailer" for node in expr.children): return False # Check if we're on a method of a function if isinstance(first_child, (tree.PythonErrorNode, tree.PythonNode)): # The tree will often include error nodes like this to indicate errors @@ -118,8 +119,12 @@ def _process_statements( insert_line = autoimport.find_insertion_line(document.source) - 1 start = {"line": insert_line, "character": 0} edit_range = {"start": start, "end": start} - edit = {"range": edit_range, "newText": suggestion.import_statement + "\n"} - score = _get_score(suggestion.source, suggestion.import_statement, suggestion.name, word) + edit = { + "range": edit_range, + "newText": suggestion.import_statement + "\n" + } + score = _get_score(suggestion.source, suggestion.import_statement, + suggestion.name, word) if score > _score_max: continue # TODO make this markdown @@ -154,7 +159,8 @@ def pylsp_completions(config: Config, workspace: Workspace, document: Document, 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] = 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)) @@ -193,7 +199,8 @@ def _sort_import(score: int) -> str: @hookimpl def pylsp_initialize(config: Config, workspace: Workspace): """Initialize AutoImport. Generates the cache for local and global items.""" - memory: bool = config.plugin_settings("rope_autoimport").get("memory", False) + memory: bool = config.plugin_settings("rope_autoimport").get( + "memory", False) rope_config = config.settings().get("rope", {}) autoimport = workspace._rope_autoimport(rope_config, memory) autoimport.generate_modules_cache() From 9a461ba3d08c0e1bd782bbb1ae8b8ca29858c0a7 Mon Sep 17 00:00:00 2001 From: bagel897 Date: Wed, 2 Nov 2022 23:37:02 -0500 Subject: [PATCH 37/39] style: reformat file --- pylsp/plugins/rope_autoimport.py | 55 +++++++++++++++----------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index 7cbe57ef..99497cf7 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -44,8 +44,10 @@ def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: if first_child == word_node: return True # If the word is the first word then its fine if len(expr.children) > 1: - if any(node.type == "operator" and "." in node.value - or node.type == "trailer" for node in expr.children): + if any( + node.type == "operator" and "." in node.value or node.type == "trailer" + for node in expr.children + ): return False # Check if we're on a method of a function if isinstance(first_child, (tree.PythonErrorNode, tree.PythonNode)): # The tree will often include error nodes like this to indicate errors @@ -54,8 +56,9 @@ def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: return _handle_first_child(first_child, expr, word_node) -def _handle_first_child(first_child: NodeOrLeaf, expr: tree.BaseNode, - word_node: tree.Leaf) -> bool: +def _handle_first_child( + first_child: NodeOrLeaf, expr: tree.BaseNode, word_node: tree.Leaf +) -> bool: """Check if we suggest imports given the following first child.""" if isinstance(first_child, tree.Import): return False @@ -119,12 +122,10 @@ def _process_statements( insert_line = autoimport.find_insertion_line(document.source) - 1 start = {"line": insert_line, "character": 0} edit_range = {"start": start, "end": start} - edit = { - "range": edit_range, - "newText": suggestion.import_statement + "\n" - } - score = _get_score(suggestion.source, suggestion.import_statement, - suggestion.name, word) + edit = {"range": edit_range, "newText": suggestion.import_statement + "\n"} + score = _get_score( + suggestion.source, suggestion.import_statement, suggestion.name, word + ) if score > _score_max: continue # TODO make this markdown @@ -132,9 +133,7 @@ def _process_statements( "label": suggestion.name, "kind": suggestion.itemkind, "sortText": _sort_import(score), - "data": { - "doc_uri": doc_uri - }, + "data": {"doc_uri": doc_uri}, "detail": _document(suggestion.import_statement), "additionalTextEdits": [edit], } @@ -148,8 +147,9 @@ def get_names(script: Script) -> Set[str]: @hookimpl -def pylsp_completions(config: Config, workspace: Workspace, document: Document, - position): +def pylsp_completions( + config: Config, workspace: Workspace, document: Document, position +): """Get autoimport suggestions.""" line = document.lines[position["line"]] expr = parso.parse(line) @@ -159,17 +159,15 @@ def pylsp_completions(config: Config, workspace: Workspace, document: Document, 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] = 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)) + 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), key=lambda statement: statement["sortText"], - )) + ) + ) if len(results) > MAX_RESULTS: results = results[:MAX_RESULTS] return results @@ -179,11 +177,12 @@ def _document(import_statement: str) -> str: return """# Auto-Import\n""" + import_statement -def _get_score(source: int, full_statement: str, suggested_name: str, - desired_name) -> int: +def _get_score( + source: int, full_statement: str, suggested_name: str, desired_name +) -> int: import_length = len("import") full_statement_score = len(full_statement) - import_length - suggested_name_score = ((len(suggested_name) - len(desired_name)))**2 + suggested_name_score = ((len(suggested_name) - len(desired_name))) ** 2 source_score = 20 * source return suggested_name_score + full_statement_score + source_score @@ -199,8 +198,7 @@ def _sort_import(score: int) -> str: @hookimpl def pylsp_initialize(config: Config, workspace: Workspace): """Initialize AutoImport. Generates the cache for local and global items.""" - memory: bool = config.plugin_settings("rope_autoimport").get( - "memory", False) + memory: bool = config.plugin_settings("rope_autoimport").get("memory", False) rope_config = config.settings().get("rope", {}) autoimport = workspace._rope_autoimport(rope_config, memory) autoimport.generate_modules_cache() @@ -208,8 +206,7 @@ def pylsp_initialize(config: Config, workspace: Workspace): @hookimpl -def pylsp_document_did_save(config: Config, workspace: Workspace, - document: Document): +def pylsp_document_did_save(config: Config, workspace: Workspace, document: Document): """Update the names associated with this document.""" rope_config = config.settings().get("rope", {}) rope_doucment: Resource = document._rope_resource(rope_config) From 6ecf97a503ae4efd182c34e761d287eee2374ed9 Mon Sep 17 00:00:00 2001 From: bagel897 Date: Wed, 2 Nov 2022 23:43:06 -0500 Subject: [PATCH 38/39] Fix another line length issue --- pylsp/plugins/rope_autoimport.py | 55 +++++++++++++++++--------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index 99497cf7..7cbe57ef 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -44,10 +44,8 @@ def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: if first_child == word_node: return True # If the word is the first word then its fine if len(expr.children) > 1: - if any( - node.type == "operator" and "." in node.value or node.type == "trailer" - for node in expr.children - ): + if any(node.type == "operator" and "." in node.value + or node.type == "trailer" for node in expr.children): return False # Check if we're on a method of a function if isinstance(first_child, (tree.PythonErrorNode, tree.PythonNode)): # The tree will often include error nodes like this to indicate errors @@ -56,9 +54,8 @@ def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: return _handle_first_child(first_child, expr, word_node) -def _handle_first_child( - first_child: NodeOrLeaf, expr: tree.BaseNode, word_node: tree.Leaf -) -> bool: +def _handle_first_child(first_child: NodeOrLeaf, expr: tree.BaseNode, + word_node: tree.Leaf) -> bool: """Check if we suggest imports given the following first child.""" if isinstance(first_child, tree.Import): return False @@ -122,10 +119,12 @@ def _process_statements( insert_line = autoimport.find_insertion_line(document.source) - 1 start = {"line": insert_line, "character": 0} edit_range = {"start": start, "end": start} - edit = {"range": edit_range, "newText": suggestion.import_statement + "\n"} - score = _get_score( - suggestion.source, suggestion.import_statement, suggestion.name, word - ) + edit = { + "range": edit_range, + "newText": suggestion.import_statement + "\n" + } + score = _get_score(suggestion.source, suggestion.import_statement, + suggestion.name, word) if score > _score_max: continue # TODO make this markdown @@ -133,7 +132,9 @@ def _process_statements( "label": suggestion.name, "kind": suggestion.itemkind, "sortText": _sort_import(score), - "data": {"doc_uri": doc_uri}, + "data": { + "doc_uri": doc_uri + }, "detail": _document(suggestion.import_statement), "additionalTextEdits": [edit], } @@ -147,9 +148,8 @@ def get_names(script: Script) -> Set[str]: @hookimpl -def pylsp_completions( - config: Config, workspace: Workspace, document: Document, position -): +def pylsp_completions(config: Config, workspace: Workspace, document: Document, + position): """Get autoimport suggestions.""" line = document.lines[position["line"]] expr = parso.parse(line) @@ -159,15 +159,17 @@ 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] = 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)) + 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), key=lambda statement: statement["sortText"], - ) - ) + )) if len(results) > MAX_RESULTS: results = results[:MAX_RESULTS] return results @@ -177,12 +179,11 @@ def _document(import_statement: str) -> str: return """# Auto-Import\n""" + import_statement -def _get_score( - source: int, full_statement: str, suggested_name: str, desired_name -) -> int: +def _get_score(source: int, full_statement: str, suggested_name: str, + desired_name) -> int: import_length = len("import") full_statement_score = len(full_statement) - import_length - suggested_name_score = ((len(suggested_name) - len(desired_name))) ** 2 + suggested_name_score = ((len(suggested_name) - len(desired_name)))**2 source_score = 20 * source return suggested_name_score + full_statement_score + source_score @@ -198,7 +199,8 @@ def _sort_import(score: int) -> str: @hookimpl def pylsp_initialize(config: Config, workspace: Workspace): """Initialize AutoImport. Generates the cache for local and global items.""" - memory: bool = config.plugin_settings("rope_autoimport").get("memory", False) + memory: bool = config.plugin_settings("rope_autoimport").get( + "memory", False) rope_config = config.settings().get("rope", {}) autoimport = workspace._rope_autoimport(rope_config, memory) autoimport.generate_modules_cache() @@ -206,7 +208,8 @@ def pylsp_initialize(config: Config, workspace: Workspace): @hookimpl -def pylsp_document_did_save(config: Config, workspace: Workspace, document: Document): +def pylsp_document_did_save(config: Config, workspace: Workspace, + document: Document): """Update the names associated with this document.""" rope_config = config.settings().get("rope", {}) rope_doucment: Resource = document._rope_resource(rope_config) From df80961b88a7e55a2ab6294f10652be77c8e6d88 Mon Sep 17 00:00:00 2001 From: Bagel <57874654+bagel897@users.noreply.github.com> Date: Thu, 3 Nov 2022 08:57:42 -0500 Subject: [PATCH 39/39] Fix style issue in pylsp/plugins/rope_autoimport.py Co-authored-by: Carlos Cordoba --- pylsp/plugins/rope_autoimport.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index 7cbe57ef..c598f42e 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -44,8 +44,8 @@ def _should_insert(expr: tree.BaseNode, word_node: tree.Leaf) -> bool: if first_child == word_node: return True # If the word is the first word then its fine if len(expr.children) > 1: - if any(node.type == "operator" and "." in node.value - or node.type == "trailer" for node in expr.children): + if any(node.type == "operator" and "." in node.value or + node.type == "trailer" for node in expr.children): return False # Check if we're on a method of a function if isinstance(first_child, (tree.PythonErrorNode, tree.PythonNode)): # The tree will often include error nodes like this to indicate errors