From 12b2010e1cd5da051162c5001381e12a3a4f9409 Mon Sep 17 00:00:00 2001 From: bageljr Date: Sun, 3 Jul 2022 14:25:00 -0500 Subject: [PATCH 1/6] feat: autoimport-progress --- pylsp/plugins/_rope_task_handle.py | 94 ++++++++++++++++++++++++++++++ pylsp/plugins/rope_autoimport.py | 12 ++-- 2 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 pylsp/plugins/_rope_task_handle.py diff --git a/pylsp/plugins/_rope_task_handle.py b/pylsp/plugins/_rope_task_handle.py new file mode 100644 index 00000000..57ecb6bf --- /dev/null +++ b/pylsp/plugins/_rope_task_handle.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import logging +from typing import List, Optional, Sequence +from rope.base.taskhandle import BaseJobSet, BaseTaskHandle + +from pylsp.workspace import Workspace + + +log = logging.getLogger(__name__) + + +class PylspJobSet(BaseJobSet): + name: str + count: int + done: int = 0 + job_name: str = "" + handle: PylspTaskHandle + token: str + + def __init__(self, handle: PylspTaskHandle, token: str, count: Optional[int]): + self.token = token + self.handle = handle + self.count = 0 if count is None else count + + def started_job(self, name: Optional[str]) -> None: + if name: + self.job_name = name + + def finished_job(self) -> None: + self.done += 1 + log.warn(f"{self.done}/{self.count}") + if self.get_percent_done() is not None and self.get_percent_done() >= 100: + self._workspace.progress_end(self.token, self.name) + self._workspace.progress_report(self.token, None, self.get_percent_done()) + + def check_status(self) -> None: + pass + + def get_percent_done(self) -> Optional[float]: + if self.count == 0: + return 0 + return (self.done / self.count) * 100 + + def increment(self) -> None: + """ + Increment the number of tasks to complete. + This is used if the number is not known ahead of time. + """ + self.count += 1 + self._workspace.progress_report(self.token, self.name, self.get_percent_done()) + + @property + def _workspace(self) -> Workspace: + return self.handle.workspace + + +class PylspTaskHandle(BaseTaskHandle): + name: str + observers: List + job_sets: List[PylspJobSet] + stopped: bool + workspace: Workspace + + def __init__(self, workspace: Workspace): + self.workspace = workspace + self.job_sets = [] + self.observers = [] + + def create_jobset(self, name="JobSet", count: Optional[int] = None): + token = self.workspace.progress_begin(name, name, 0) + result = PylspJobSet(self, token, count) + self.job_sets.append(result) + self._inform_observers() + return result + + def stop(self) -> None: + pass + + def current_jobset(self) -> Optional[BaseJobSet]: + pass + + def add_observer(self) -> None: + pass + + def is_stopped(self) -> bool: + pass + + def get_jobsets(self) -> Sequence[BaseJobSet]: + pass + + def _inform_observers(self) -> None: + for observer in self.observers: + observer() diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index f6366989..ec6e18b9 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -14,7 +14,7 @@ from pylsp import hookimpl from pylsp.config.config import Config from pylsp.workspace import Document, Workspace - +from ._rope_task_handle import PylspTaskHandle log = logging.getLogger(__name__) _score_pow = 5 @@ -203,8 +203,9 @@ def pylsp_initialize(config: Config, workspace: Workspace): "memory", False) rope_config = config.settings().get("rope", {}) autoimport = workspace._rope_autoimport(rope_config, memory) - autoimport.generate_modules_cache() - autoimport.generate_cache() + task_handle = PylspTaskHandle(workspace) + autoimport.generate_modules_cache(task_handle=task_handle) + autoimport.generate_cache(task_handle=task_handle) @hookimpl @@ -213,7 +214,8 @@ def pylsp_document_did_save(config: Config, workspace: Workspace, """Update the names associated with this document.""" rope_config = config.settings().get("rope", {}) rope_doucment: Resource = document._rope_resource(rope_config) + task_handle = PylspTaskHandle(workspace) autoimport = workspace._rope_autoimport(rope_config) - autoimport.generate_cache(resources=[rope_doucment]) + autoimport.generate_cache(resources=[rope_doucment], task_handle=task_handle) # Might as well using saving the document as an indicator to regenerate the module cache - autoimport.generate_modules_cache() + autoimport.generate_modules_cache(task_handle=task_handle) From 7eb3092940bce671510b0df002b15b318e81d61e Mon Sep 17 00:00:00 2001 From: bagel897 Date: Wed, 23 Nov 2022 11:27:25 -0600 Subject: [PATCH 2/6] Update task handle --- pylsp/plugins/_rope_task_handle.py | 44 ++++++++++++++++-------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/pylsp/plugins/_rope_task_handle.py b/pylsp/plugins/_rope_task_handle.py index 57ecb6bf..120e3293 100644 --- a/pylsp/plugins/_rope_task_handle.py +++ b/pylsp/plugins/_rope_task_handle.py @@ -1,27 +1,27 @@ from __future__ import annotations import logging -from typing import List, Optional, Sequence +from typing import List, Optional, Sequence, Callable, ContextManager from rope.base.taskhandle import BaseJobSet, BaseTaskHandle from pylsp.workspace import Workspace log = logging.getLogger(__name__) +Report = Callable[[str, int], None] class PylspJobSet(BaseJobSet): - name: str - count: int + count: int = 0 done: int = 0 - job_name: str = "" - handle: PylspTaskHandle - token: str + _reporter: Report + _report_iter: ContextManager - def __init__(self, handle: PylspTaskHandle, token: str, count: Optional[int]): - self.token = token - self.handle = handle - self.count = 0 if count is None else count + def __init__(self, count: Optional[int], report_iter: ContextManager): + if count is not None: + self.count = count + self._reporter = report_iter.__enter__() + self._report_iter = report_iter def started_job(self, name: Optional[str]) -> None: if name: @@ -29,10 +29,14 @@ def started_job(self, name: Optional[str]) -> None: def finished_job(self) -> None: self.done += 1 - log.warn(f"{self.done}/{self.count}") - if self.get_percent_done() is not None and self.get_percent_done() >= 100: - self._workspace.progress_end(self.token, self.name) - self._workspace.progress_report(self.token, None, self.get_percent_done()) + log.debug(f"{self.done}/{self.count} {self.get_percent_done()}") + if self.get_percent_done() is not None and int(self.get_percent_done()) >= 100: + if self._report_iter is None: + return + self._report_iter.__exit__(None, None, None) + self._report_iter = None + else: + self._report() def check_status(self) -> None: pass @@ -48,11 +52,10 @@ def increment(self) -> None: This is used if the number is not known ahead of time. """ self.count += 1 - self._workspace.progress_report(self.token, self.name, self.get_percent_done()) + self._report() - @property - def _workspace(self) -> Workspace: - return self.handle.workspace + def _report(self): + self._reporter(self.job_name, int(self.get_percent_done())) class PylspTaskHandle(BaseTaskHandle): @@ -61,6 +64,7 @@ class PylspTaskHandle(BaseTaskHandle): job_sets: List[PylspJobSet] stopped: bool workspace: Workspace + _report: Callable[[str, str], None] def __init__(self, workspace: Workspace): self.workspace = workspace @@ -68,8 +72,8 @@ def __init__(self, workspace: Workspace): self.observers = [] def create_jobset(self, name="JobSet", count: Optional[int] = None): - token = self.workspace.progress_begin(name, name, 0) - result = PylspJobSet(self, token, count) + report_iter = self.workspace.report_progress(name, name, 0) + result = PylspJobSet(count, report_iter) self.job_sets.append(result) self._inform_observers() return result From f070678f189bc89dc4072b574c7d21ace8bb5860 Mon Sep 17 00:00:00 2001 From: bagel897 Date: Wed, 23 Nov 2022 13:15:10 -0600 Subject: [PATCH 3/6] Refactor and improve --- pylsp/plugins/_rope_task_handle.py | 8 +++++--- pylsp/plugins/rope_autoimport.py | 30 ++++++++++++++++++------------ 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/pylsp/plugins/_rope_task_handle.py b/pylsp/plugins/_rope_task_handle.py index 120e3293..f0ca6595 100644 --- a/pylsp/plugins/_rope_task_handle.py +++ b/pylsp/plugins/_rope_task_handle.py @@ -29,7 +29,6 @@ def started_job(self, name: Optional[str]) -> None: def finished_job(self) -> None: self.done += 1 - log.debug(f"{self.done}/{self.count} {self.get_percent_done()}") if self.get_percent_done() is not None and int(self.get_percent_done()) >= 100: if self._report_iter is None: return @@ -55,7 +54,10 @@ def increment(self) -> None: self._report() def _report(self): - self._reporter(self.job_name, int(self.get_percent_done())) + percent = int(self.get_percent_done()) + message = f"{self.job_name} {self.done}/{self.count}" + log.debug(f"Reporting {message=} {percent=}") + self._reporter(message, percent) class PylspTaskHandle(BaseTaskHandle): @@ -72,7 +74,7 @@ def __init__(self, workspace: Workspace): self.observers = [] def create_jobset(self, name="JobSet", count: Optional[int] = None): - report_iter = self.workspace.report_progress(name, name, 0) + report_iter = self.workspace.report_progress(name, None, None) result = PylspJobSet(count, report_iter) self.job_sets.append(result) self._inform_observers() diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index ec6e18b9..58d7bea9 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -1,7 +1,7 @@ # Copyright 2022- Python Language Server Contributors. import logging -from typing import Any, Dict, Generator, List, Set +from typing import Any, Dict, Generator, List, Set, Optional import parso from jedi import Script @@ -196,26 +196,32 @@ def _sort_import(score: int) -> str: return "[z" + str(score).rjust(_score_pow, "0") -@hookimpl -def pylsp_initialize(config: Config, workspace: Workspace): - """Initialize AutoImport. Generates the cache for local and global items.""" +def _reload_cache(config: Config, workspace: Workspace, files: Optional[List[Document]] = None): memory: bool = config.plugin_settings("rope_autoimport").get( "memory", False) rope_config = config.settings().get("rope", {}) autoimport = workspace._rope_autoimport(rope_config, memory) task_handle = PylspTaskHandle(workspace) + resources: Optional[List[Resource]] = None if files is None else [ + document._rope_resource(rope_config) for document in files] + autoimport.generate_cache(task_handle=task_handle, resources=resources) autoimport.generate_modules_cache(task_handle=task_handle) - autoimport.generate_cache(task_handle=task_handle) + + +@hookimpl +def pylsp_initialize(config: Config, workspace: Workspace): + """Initialize AutoImport. Generates the cache for local and global items.""" + _reload_cache(config, workspace) + + +@hookimpl +def pylsp_document_did_open(config: Config, workspace: Workspace): + """Initialize AutoImport. Generates the cache for local and global items.""" + _reload_cache(config, workspace) @hookimpl 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) - task_handle = PylspTaskHandle(workspace) - autoimport = workspace._rope_autoimport(rope_config) - autoimport.generate_cache(resources=[rope_doucment], task_handle=task_handle) - # Might as well using saving the document as an indicator to regenerate the module cache - autoimport.generate_modules_cache(task_handle=task_handle) + _reload_cache(config, workspace, [document]) From 41331681db0c29f18c7e91edbba4affc9de1e171 Mon Sep 17 00:00:00 2001 From: bagel897 Date: Wed, 23 Nov 2022 13:22:12 -0600 Subject: [PATCH 4/6] backwards compatible fstrings --- pylsp/plugins/_rope_task_handle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylsp/plugins/_rope_task_handle.py b/pylsp/plugins/_rope_task_handle.py index f0ca6595..e768a565 100644 --- a/pylsp/plugins/_rope_task_handle.py +++ b/pylsp/plugins/_rope_task_handle.py @@ -56,7 +56,7 @@ def increment(self) -> None: def _report(self): percent = int(self.get_percent_done()) message = f"{self.job_name} {self.done}/{self.count}" - log.debug(f"Reporting {message=} {percent=}") + log.debug(f"Reporting {message} {percent}%") self._reporter(message, percent) From 1898683df43ef778d51be79ae26ecd1ea344ec4e Mon Sep 17 00:00:00 2001 From: bagel897 Date: Wed, 23 Nov 2022 13:24:50 -0600 Subject: [PATCH 5/6] default job_name --- pylsp/plugins/_rope_task_handle.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pylsp/plugins/_rope_task_handle.py b/pylsp/plugins/_rope_task_handle.py index e768a565..627f26fd 100644 --- a/pylsp/plugins/_rope_task_handle.py +++ b/pylsp/plugins/_rope_task_handle.py @@ -16,6 +16,7 @@ class PylspJobSet(BaseJobSet): done: int = 0 _reporter: Report _report_iter: ContextManager + job_name: str = "" def __init__(self, count: Optional[int], report_iter: ContextManager): if count is not None: From a4b985802597fb34fc7626fa52cb956432e75e62 Mon Sep 17 00:00:00 2001 From: bagel897 Date: Wed, 30 Nov 2022 11:43:02 -0600 Subject: [PATCH 6/6] format: reformat --- pylsp/plugins/_rope_task_handle.py | 5 ++- pylsp/plugins/rope_autoimport.py | 66 +++++++++++++++--------------- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/pylsp/plugins/_rope_task_handle.py b/pylsp/plugins/_rope_task_handle.py index 627f26fd..7854bb22 100644 --- a/pylsp/plugins/_rope_task_handle.py +++ b/pylsp/plugins/_rope_task_handle.py @@ -1,12 +1,12 @@ from __future__ import annotations import logging -from typing import List, Optional, Sequence, Callable, ContextManager +from typing import Callable, ContextManager, List, Optional, Sequence + from rope.base.taskhandle import BaseJobSet, BaseTaskHandle from pylsp.workspace import Workspace - log = logging.getLogger(__name__) Report = Callable[[str, int], None] @@ -49,6 +49,7 @@ def get_percent_done(self) -> Optional[float]: def increment(self) -> None: """ Increment the number of tasks to complete. + This is used if the number is not known ahead of time. """ self.count += 1 diff --git a/pylsp/plugins/rope_autoimport.py b/pylsp/plugins/rope_autoimport.py index 58d7bea9..5e9b8f73 100644 --- a/pylsp/plugins/rope_autoimport.py +++ b/pylsp/plugins/rope_autoimport.py @@ -1,7 +1,7 @@ # Copyright 2022- Python Language Server Contributors. import logging -from typing import Any, Dict, Generator, List, Set, Optional +from typing import Any, Dict, Generator, List, Optional, Set import parso from jedi import Script @@ -14,7 +14,9 @@ from pylsp import hookimpl from pylsp.config.config import Config from pylsp.workspace import Document, Workspace + from ._rope_task_handle import PylspTaskHandle + log = logging.getLogger(__name__) _score_pow = 5 @@ -44,8 +46,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 +58,7 @@ 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,8 @@ 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 +131,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 +145,7 @@ 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 +155,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 +173,10 @@ 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 @@ -197,31 +190,36 @@ def _sort_import(score: int) -> str: def _reload_cache(config: Config, workspace: Workspace, files: Optional[List[Document]] = None): - 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) task_handle = PylspTaskHandle(workspace) - resources: Optional[List[Resource]] = None if files is None else [ - document._rope_resource(rope_config) for document in files] + resources: Optional[List[Resource]] = ( + None if files is None else [document._rope_resource(rope_config) for document in files] + ) autoimport.generate_cache(task_handle=task_handle, resources=resources) autoimport.generate_modules_cache(task_handle=task_handle) @hookimpl def pylsp_initialize(config: Config, workspace: Workspace): - """Initialize AutoImport. Generates the cache for local and global items.""" + """Initialize AutoImport. + + Generates the cache for local and global items. + """ _reload_cache(config, workspace) @hookimpl def pylsp_document_did_open(config: Config, workspace: Workspace): - """Initialize AutoImport. Generates the cache for local and global items.""" + """Initialize AutoImport. + + Generates the cache for local and global items. + """ _reload_cache(config, 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.""" _reload_cache(config, workspace, [document])