diff --git a/external-deps/python-lsp-server/.github/workflows/release.yml b/external-deps/python-lsp-server/.github/workflows/release.yml index c02c5c37722..c42d0fab14a 100644 --- a/external-deps/python-lsp-server/.github/workflows/release.yml +++ b/external-deps/python-lsp-server/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - PYTHON_VERSION: ['3.8'] + PYTHON_VERSION: ['3.9'] timeout-minutes: 10 steps: - uses: actions/cache@v1 diff --git a/external-deps/python-lsp-server/.github/workflows/static.yml b/external-deps/python-lsp-server/.github/workflows/static.yml index 6ec4345df51..881a0aa6022 100644 --- a/external-deps/python-lsp-server/.github/workflows/static.yml +++ b/external-deps/python-lsp-server/.github/workflows/static.yml @@ -30,9 +30,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - # TODO: check with Python 3, but need to fix the - # errors first - python-version: '3.8' + python-version: '3.9' architecture: 'x64' - run: python -m pip install --upgrade pip setuptools jsonschema # If we don't install pycodestyle, pylint will throw an unused-argument error in pylsp/plugins/pycodestyle_lint.py:72 diff --git a/external-deps/python-lsp-server/.github/workflows/test-linux.yml b/external-deps/python-lsp-server/.github/workflows/test-linux.yml index 89277d6793f..7a7f2f6e6e1 100644 --- a/external-deps/python-lsp-server/.github/workflows/test-linux.yml +++ b/external-deps/python-lsp-server/.github/workflows/test-linux.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - PYTHON_VERSION: ['3.10', '3.9', '3.8'] + PYTHON_VERSION: ['3.11', '3.10', '3.9'] timeout-minutes: 10 steps: - uses: actions/cache@v4 diff --git a/external-deps/python-lsp-server/.github/workflows/test-mac.yml b/external-deps/python-lsp-server/.github/workflows/test-mac.yml index d9e4818fb20..a92c82a81c9 100644 --- a/external-deps/python-lsp-server/.github/workflows/test-mac.yml +++ b/external-deps/python-lsp-server/.github/workflows/test-mac.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - PYTHON_VERSION: ['3.10', '3.9', '3.8'] + PYTHON_VERSION: ['3.11', '3.10', '3.9'] timeout-minutes: 10 steps: - uses: actions/cache@v4 diff --git a/external-deps/python-lsp-server/.github/workflows/test-win.yml b/external-deps/python-lsp-server/.github/workflows/test-win.yml index 1db411540b9..8ecd34293e8 100644 --- a/external-deps/python-lsp-server/.github/workflows/test-win.yml +++ b/external-deps/python-lsp-server/.github/workflows/test-win.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - PYTHON_VERSION: ['3.10', '3.9', '3.8'] + PYTHON_VERSION: ['3.11', '3.10', '3.9'] timeout-minutes: 10 steps: - uses: actions/cache@v4 diff --git a/external-deps/python-lsp-server/.gitrepo b/external-deps/python-lsp-server/.gitrepo index ada12497eb5..6d9857871b9 100644 --- a/external-deps/python-lsp-server/.gitrepo +++ b/external-deps/python-lsp-server/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = https://github.com/python-lsp/python-lsp-server.git branch = develop - commit = 04fa3e59e82e05a43759f7d3b5bea2fa7a9b539b - parent = 2193e4d0357f31d3eb70ccadfbb4b25e2e176320 + commit = dd030853641cac8f51b8de2d117b30a67f28423a + parent = cac1dfbb4305707ffc933ef4743d5f2922d3533f method = merge cmdver = 0.4.9 diff --git a/external-deps/python-lsp-server/CONFIGURATION.md b/external-deps/python-lsp-server/CONFIGURATION.md index 0609169b56a..93b828b09e4 100644 --- a/external-deps/python-lsp-server/CONFIGURATION.md +++ b/external-deps/python-lsp-server/CONFIGURATION.md @@ -75,5 +75,7 @@ This server can be configured using the `workspace/didChangeConfiguration` metho | `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` | | `pylsp.rope.ropeFolder` | `array` of unique `string` items | The name of the folder in which rope stores project configurations and data. Pass `null` for not using such a folder at all. | `null` | +| `pylsp.signature.formatter` | `string` (one of: `'black'`, `'ruff'`, `None`) | Formatter to use for reformatting signatures in docstrings. | `"black"` | +| `pylsp.signature.line_length` | `number` | Maximum line length in signatures. | `88` | This documentation was generated from `pylsp/config/schema.json`. Please do not edit this file directly. diff --git a/external-deps/python-lsp-server/pylsp/__main__.py b/external-deps/python-lsp-server/pylsp/__main__.py index 44aa3cfac9d..abc0a0bb0c7 100644 --- a/external-deps/python-lsp-server/pylsp/__main__.py +++ b/external-deps/python-lsp-server/pylsp/__main__.py @@ -20,7 +20,7 @@ start_ws_lang_server, ) -LOG_FORMAT = "%(asctime)s {0} - %(levelname)s - %(name)s - %(message)s".format( +LOG_FORMAT = "%(asctime)s {} - %(levelname)s - %(name)s - %(message)s".format( time.localtime().tm_zone ) @@ -98,7 +98,7 @@ def _configure_logger(verbose=0, log_config=None, log_file=None) -> None: root_logger = logging.root if log_config: - with open(log_config, "r", encoding="utf-8") as f: + with open(log_config, encoding="utf-8") as f: logging.config.dictConfig(json.load(f)) else: formatter = logging.Formatter(LOG_FORMAT) diff --git a/external-deps/python-lsp-server/pylsp/_utils.py b/external-deps/python-lsp-server/pylsp/_utils.py index b96df5a9db3..dfe84b14f0d 100644 --- a/external-deps/python-lsp-server/pylsp/_utils.py +++ b/external-deps/python-lsp-server/pylsp/_utils.py @@ -7,9 +7,11 @@ import os import pathlib import re +import subprocess +import sys import threading import time -from typing import List, Optional +from typing import Optional import docstring_to_markdown import jedi @@ -57,7 +59,7 @@ def run(): def throttle(seconds=1): - """Throttles calls to a function evey `seconds` seconds.""" + """Throttles calls to a function every `seconds` seconds.""" def decorator(func): @functools.wraps(func) @@ -78,7 +80,7 @@ def find_parents(root, path, names): Args: path (str): The file path to start searching up from. - names (List[str]): The file/directory names to look for. + names (list[str]): The file/directory names to look for. root (str): The directory at which to stop recursing upwards. Note: @@ -198,7 +200,7 @@ def wrap_signature(signature): SERVER_SUPPORTED_MARKUP_KINDS = {"markdown", "plaintext"} -def choose_markup_kind(client_supported_markup_kinds: List[str]): +def choose_markup_kind(client_supported_markup_kinds: list[str]): """Choose a markup kind supported by both client and the server. This gives priority to the markup kinds provided earlier on the client preference list. @@ -209,8 +211,96 @@ def choose_markup_kind(client_supported_markup_kinds: List[str]): return "markdown" +class Formatter: + command: list[str] + + @property + def is_installed(self) -> bool: + """Returns whether formatter is available""" + if not hasattr(self, "_is_installed"): + self._is_installed = self._is_available_via_cli() + return self._is_installed + + def format(self, code: str, line_length: int) -> str: + """Formats code""" + return subprocess.check_output( + [ + sys.executable, + "-m", + *self.command, + "--line-length", + str(line_length), + "-", + ], + input=code, + text=True, + ).strip() + + def _is_available_via_cli(self) -> bool: + try: + subprocess.check_output( + [ + sys.executable, + "-m", + *self.command, + "--help", + ], + ) + return True + except subprocess.CalledProcessError: + return False + + +class RuffFormatter(Formatter): + command = ["ruff", "format"] + + +class BlackFormatter(Formatter): + command = ["black"] + + +formatters = {"ruff": RuffFormatter(), "black": BlackFormatter()} + + +def format_signature(signature: str, config: dict, signature_formatter: str) -> str: + """Formats signature using ruff or black if either is available.""" + as_func = f"def {signature.strip()}:\n pass" + line_length = config.get("line_length", 88) + formatter = formatters[signature_formatter] + if formatter.is_installed: + try: + return ( + formatter.format(as_func, line_length=line_length) + .removeprefix("def ") + .removesuffix(":\n pass") + ) + except subprocess.CalledProcessError as e: + log.warning("Signature formatter failed %s", e) + else: + log.warning( + "Formatter %s was requested but it does not appear to be installed", + signature_formatter, + ) + return signature + + +def convert_signatures_to_markdown(signatures: list[str], config: dict) -> str: + signature_formatter = config.get("formatter", "black") + if signature_formatter: + signatures = [ + format_signature( + signature, signature_formatter=signature_formatter, config=config + ) + for signature in signatures + ] + return wrap_signature("\n".join(signatures)) + + def format_docstring( - contents: str, markup_kind: str, signatures: Optional[List[str]] = None + contents: str, + markup_kind: str, + signatures: Optional[list[str]] = None, + signature_config: Optional[dict] = None, ): """Transform the provided docstring into a MarkupContent object. @@ -232,7 +322,10 @@ def format_docstring( value = escape_markdown(contents) if signatures: - value = wrap_signature("\n".join(signatures)) + "\n\n" + value + wrapped_signatures = convert_signatures_to_markdown( + signatures, config=signature_config or {} + ) + value = wrapped_signatures + "\n\n" + value return {"kind": "markdown", "value": value} value = contents diff --git a/external-deps/python-lsp-server/pylsp/config/config.py b/external-deps/python-lsp-server/pylsp/config/config.py index 815f8fd2c04..7b201824a85 100644 --- a/external-deps/python-lsp-server/pylsp/config/config.py +++ b/external-deps/python-lsp-server/pylsp/config/config.py @@ -3,8 +3,9 @@ import logging import sys +from collections.abc import Mapping, Sequence from functools import lru_cache -from typing import List, Mapping, Sequence, Union +from typing import Union import pluggy from pluggy._hooks import HookImpl @@ -32,7 +33,7 @@ def _hookexec( methods: Sequence[HookImpl], kwargs: Mapping[str, object], firstresult: bool, - ) -> Union[object, List[object]]: + ) -> Union[object, list[object]]: # called from all hookcaller instances. # enable_tracing will set its own wrapping function at self._inner_hookexec try: diff --git a/external-deps/python-lsp-server/pylsp/config/schema.json b/external-deps/python-lsp-server/pylsp/config/schema.json index 18248384b3b..c4aec4601bd 100644 --- a/external-deps/python-lsp-server/pylsp/config/schema.json +++ b/external-deps/python-lsp-server/pylsp/config/schema.json @@ -511,6 +511,24 @@ }, "uniqueItems": true, "description": "The name of the folder in which rope stores project configurations and data. Pass `null` for not using such a folder at all." + }, + "pylsp.signature.formatter": { + "type": [ + "string", + "null" + ], + "enum": [ + "black", + "ruff", + null + ], + "default": "black", + "description": "Formatter to use for reformatting signatures in docstrings." + }, + "pylsp.signature.line_length": { + "type": "number", + "default": 88, + "description": "Maximum line length in signatures." } } } diff --git a/external-deps/python-lsp-server/pylsp/plugins/_resolvers.py b/external-deps/python-lsp-server/pylsp/plugins/_resolvers.py index 44d6d882a87..dcfd06abea8 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/_resolvers.py +++ b/external-deps/python-lsp-server/pylsp/plugins/_resolvers.py @@ -88,7 +88,7 @@ def resolve(self, completion): def format_label(completion, sig): if sig and completion.type in ("function", "method"): params = ", ".join(param.name for param in sig[0].params) - label = "{}({})".format(completion.name, params) + label = f"{completion.name}({params})" return label return completion.name @@ -115,7 +115,7 @@ def format_snippet(completion, sig): snippet_completion["insertTextFormat"] = lsp.InsertTextFormat.Snippet snippet = completion.name + "(" for i, param in enumerate(positional_args): - snippet += "${%s:%s}" % (i + 1, param.name) + snippet += "${{{}:{}}}".format(i + 1, param.name) if i < len(positional_args) - 1: snippet += ", " snippet += ")$0" diff --git a/external-deps/python-lsp-server/pylsp/plugins/_rope_task_handle.py b/external-deps/python-lsp-server/pylsp/plugins/_rope_task_handle.py index 8bc13c1de4c..5e278ee5105 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/_rope_task_handle.py +++ b/external-deps/python-lsp-server/pylsp/plugins/_rope_task_handle.py @@ -1,7 +1,8 @@ from __future__ import annotations import logging -from typing import Callable, ContextManager, List, Optional, Sequence +from collections.abc import Sequence +from typing import Callable, ContextManager from rope.base.taskhandle import BaseJobSet, BaseTaskHandle @@ -19,13 +20,13 @@ class PylspJobSet(BaseJobSet): _report_iter: ContextManager job_name: str = "" - def __init__(self, count: Optional[int], report_iter: ContextManager) -> None: + def __init__(self, count: int | None, report_iter: ContextManager) -> None: 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: + def started_job(self, name: str | None) -> None: if name: self.job_name = name @@ -42,7 +43,7 @@ def finished_job(self) -> None: def check_status(self) -> None: pass - def get_percent_done(self) -> Optional[float]: + def get_percent_done(self) -> float | None: if self.count == 0: return 0 return (self.done / self.count) * 100 @@ -66,8 +67,8 @@ def _report(self) -> None: class PylspTaskHandle(BaseTaskHandle): name: str - observers: List - job_sets: List[PylspJobSet] + observers: list + job_sets: list[PylspJobSet] stopped: bool workspace: Workspace _report: Callable[[str, str], None] @@ -77,7 +78,7 @@ def __init__(self, workspace: Workspace) -> None: self.job_sets = [] self.observers = [] - def create_jobset(self, name="JobSet", count: Optional[int] = None): + def create_jobset(self, name="JobSet", count: int | None = None): report_iter = self.workspace.report_progress( name, None, None, skip_token_initialization=True ) @@ -89,7 +90,7 @@ def create_jobset(self, name="JobSet", count: Optional[int] = None): def stop(self) -> None: pass - def current_jobset(self) -> Optional[BaseJobSet]: + def current_jobset(self) -> BaseJobSet | None: pass def add_observer(self) -> None: diff --git a/external-deps/python-lsp-server/pylsp/plugins/definition.py b/external-deps/python-lsp-server/pylsp/plugins/definition.py index 67abfb71410..1ddc03a0099 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/definition.py +++ b/external-deps/python-lsp-server/pylsp/plugins/definition.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any, Dict, List +from typing import TYPE_CHECKING, Any import jedi @@ -23,7 +23,7 @@ def _resolve_definition( - maybe_defn: Name, script: Script, settings: Dict[str, Any] + maybe_defn: Name, script: Script, settings: dict[str, Any] ) -> Name: for _ in range(MAX_JEDI_GOTO_HOPS): if maybe_defn.is_definition() or maybe_defn.module_path != script.path: @@ -43,8 +43,8 @@ def _resolve_definition( @hookimpl def pylsp_definitions( - config: Config, document: Document, position: Dict[str, int] -) -> List[Dict[str, Any]]: + config: Config, document: Document, position: dict[str, int] +) -> list[dict[str, Any]]: settings = config.plugin_settings("jedi_definition") code_position = _utils.position_to_jedi_linecolumn(document, position) script = document.jedi_script(use_document_path=True) diff --git a/external-deps/python-lsp-server/pylsp/plugins/flake8_lint.py b/external-deps/python-lsp-server/pylsp/plugins/flake8_lint.py index 74e2664cc1a..0ac91855c08 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/flake8_lint.py +++ b/external-deps/python-lsp-server/pylsp/plugins/flake8_lint.py @@ -135,7 +135,7 @@ def run_flake8(flake8_executable, args, document, source): cmd = [flake8_executable] cmd.extend(args) p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, **popen_kwargs) - except IOError: + except OSError: log.debug( "Can't execute %s. Trying with '%s -m flake8'", flake8_executable, @@ -165,9 +165,9 @@ def build_args(options): arg = "--{}={}".format(arg_name, ",".join(arg_val)) elif isinstance(arg_val, bool): if arg_val: - arg = "--{}".format(arg_name) + arg = f"--{arg_name}" else: - arg = "--{}={}".format(arg_name, arg_val) + arg = f"--{arg_name}={arg_val}" args.append(arg) return args diff --git a/external-deps/python-lsp-server/pylsp/plugins/hover.py b/external-deps/python-lsp-server/pylsp/plugins/hover.py index ca69d1b3c64..daaae90b9c3 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/hover.py +++ b/external-deps/python-lsp-server/pylsp/plugins/hover.py @@ -10,6 +10,7 @@ @hookimpl def pylsp_hover(config, document, position): + signature_config = config.settings().get("signature", {}) code_position = _utils.position_to_jedi_linecolumn(document, position) definitions = document.jedi_script(use_document_path=True).infer(**code_position) word = document.word_at_position(position) @@ -46,5 +47,6 @@ def pylsp_hover(config, document, position): definition.docstring(raw=True), preferred_markup_kind, signatures=[signature] if signature else None, + signature_config=signature_config, ) } diff --git a/external-deps/python-lsp-server/pylsp/plugins/jedi_completion.py b/external-deps/python-lsp-server/pylsp/plugins/jedi_completion.py index 2796a093896..51c3589ce53 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/jedi_completion.py +++ b/external-deps/python-lsp-server/pylsp/plugins/jedi_completion.py @@ -40,8 +40,9 @@ def pylsp_completions(config, document, position): """Get formatted completions for current code position""" settings = config.plugin_settings("jedi_completion", document_path=document.path) resolve_eagerly = settings.get("eager", False) - code_position = _utils.position_to_jedi_linecolumn(document, position) + signature_config = config.settings().get("signature", {}) + code_position = _utils.position_to_jedi_linecolumn(document, position) code_position["fuzzy"] = settings.get("fuzzy", False) completions = document.jedi_script(use_document_path=True).complete(**code_position) @@ -88,6 +89,7 @@ def pylsp_completions(config, document, position): resolve=resolve_eagerly, resolve_label_or_snippet=(i < max_to_resolve), snippet_support=snippet_support, + signature_config=signature_config, ) for i, c in enumerate(completions) ] @@ -103,6 +105,7 @@ def pylsp_completions(config, document, position): resolve=resolve_eagerly, resolve_label_or_snippet=(i < max_to_resolve), snippet_support=snippet_support, + signature_config=signature_config, ) completion_dict["kind"] = lsp.CompletionItemKind.TypeParameter completion_dict["label"] += " object" @@ -118,6 +121,7 @@ def pylsp_completions(config, document, position): resolve=resolve_eagerly, resolve_label_or_snippet=(i < max_to_resolve), snippet_support=snippet_support, + signature_config=signature_config, ) completion_dict["kind"] = lsp.CompletionItemKind.TypeParameter completion_dict["label"] += " object" @@ -137,7 +141,11 @@ def pylsp_completions(config, document, position): @hookimpl -def pylsp_completion_item_resolve(config, completion_item, document): +def pylsp_completion_item_resolve( + config, + completion_item, + document, +): """Resolve formatted completion for given non-resolved completion""" shared_data = document.shared_data["LAST_JEDI_COMPLETIONS"].get( completion_item["label"] @@ -152,7 +160,12 @@ def pylsp_completion_item_resolve(config, completion_item, document): if shared_data: completion, data = shared_data - return _resolve_completion(completion, data, markup_kind=preferred_markup_kind) + return _resolve_completion( + completion, + data, + markup_kind=preferred_markup_kind, + signature_config=config.settings().get("signature", {}), + ) return completion_item @@ -207,13 +220,14 @@ def use_snippets(document, position): return expr_type not in _IMPORTS and not (expr_type in _ERRORS and "import" in code) -def _resolve_completion(completion, d, markup_kind: str): +def _resolve_completion(completion, d, markup_kind: str, signature_config: dict): completion["detail"] = _detail(d) try: docs = _utils.format_docstring( d.docstring(raw=True), signatures=[signature.to_string() for signature in d.get_signatures()], markup_kind=markup_kind, + signature_config=signature_config, ) except Exception: docs = "" @@ -228,6 +242,7 @@ def _format_completion( resolve=False, resolve_label_or_snippet=False, snippet_support=False, + signature_config=None, ): completion = { "label": _label(d, resolve_label_or_snippet), @@ -237,7 +252,9 @@ def _format_completion( } if resolve: - completion = _resolve_completion(completion, d, markup_kind) + completion = _resolve_completion( + completion, d, markup_kind, signature_config=signature_config + ) # Adjustments for file completions if d.type == "path": diff --git a/external-deps/python-lsp-server/pylsp/plugins/pylint_lint.py b/external-deps/python-lsp-server/pylsp/plugins/pylint_lint.py index 722e831b333..f3415c8a4a6 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/pylint_lint.py +++ b/external-deps/python-lsp-server/pylsp/plugins/pylint_lint.py @@ -287,7 +287,7 @@ def _run_pylint_stdio(pylint_executable, document, flags): cmd.extend(flags) cmd.extend(["--from-stdin", document.path]) p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) - except IOError: + except OSError: log.debug("Can't execute %s. Trying with 'python -m pylint'", pylint_executable) cmd = [sys.executable, "-m", "pylint"] cmd.extend(flags) diff --git a/external-deps/python-lsp-server/pylsp/plugins/rope_autoimport.py b/external-deps/python-lsp-server/pylsp/plugins/rope_autoimport.py index 12f5d80bd18..8ba951f7157 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/rope_autoimport.py +++ b/external-deps/python-lsp-server/pylsp/plugins/rope_autoimport.py @@ -2,7 +2,8 @@ import logging import threading -from typing import Any, Dict, Generator, List, Optional, Set, Union +from collections.abc import Generator +from typing import Any, Optional, Union import parso from jedi import Script @@ -36,7 +37,7 @@ def reload_cache( self, config: Config, workspace: Workspace, - files: Optional[List[Document]] = None, + files: Optional[list[Document]] = None, single_thread: Optional[bool] = True, ): if self.is_blocked(): @@ -45,7 +46,7 @@ def reload_cache( memory: bool = config.plugin_settings("rope_autoimport").get("memory", False) rope_config = config.settings().get("rope", {}) autoimport = workspace._rope_autoimport(rope_config, memory) - resources: Optional[List[Resource]] = ( + resources: Optional[list[Resource]] = ( None if files is None else [document._rope_resource(rope_config) for document in files] @@ -65,7 +66,7 @@ def _reload_cache( self, workspace: Workspace, autoimport: AutoImport, - resources: Optional[List[Resource]] = None, + resources: Optional[list[Resource]] = None, ) -> None: task_handle = PylspTaskHandle(workspace) autoimport.generate_cache(task_handle=task_handle, resources=resources) @@ -76,7 +77,7 @@ def is_blocked(self): @hookimpl -def pylsp_settings() -> Dict[str, Dict[str, Dict[str, Any]]]: +def pylsp_settings() -> dict[str, dict[str, dict[str, Any]]]: # Default rope_completion to disabled return { "plugins": { @@ -180,13 +181,13 @@ def _handle_argument(node: NodeOrLeaf, word_node: tree.Leaf): def _process_statements( - suggestions: List[SearchResult], + suggestions: list[SearchResult], doc_uri: str, word: str, autoimport: AutoImport, document: Document, feature: str = "completions", -) -> Generator[Dict[str, Any], None, None]: +) -> Generator[dict[str, Any], None, None]: for suggestion in suggestions: insert_line = autoimport.find_insertion_line(document.source) - 1 start = {"line": insert_line, "character": 0} @@ -220,7 +221,7 @@ def _process_statements( raise ValueError(f"Unknown feature: {feature}") -def get_names(script: Script) -> Set[str]: +def get_names(script: Script) -> set[str]: """Get all names to ignore from the current file.""" raw_names = script.get_names(definitions=True) log.debug(raw_names) @@ -233,7 +234,7 @@ def pylsp_completions( workspace: Workspace, document: Document, position, - ignored_names: Union[Set[str], None], + ignored_names: Union[set[str], None], ): """Get autoimport suggestions.""" if ( @@ -251,7 +252,7 @@ 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] = ignored_names or get_names( + ignored_names: set[str] = ignored_names or get_names( document.jedi_script(use_document_path=True) ) autoimport = workspace._rope_autoimport(rope_config) @@ -303,9 +304,9 @@ def pylsp_code_actions( config: Config, workspace: Workspace, document: Document, - range: Dict, - context: Dict, -) -> List[Dict]: + range: dict, + context: dict, +) -> list[dict]: """ Provide code actions through rope. @@ -317,9 +318,9 @@ def pylsp_code_actions( Current workspace. document : pylsp.workspace.Document Document to apply code actions on. - range : Dict + range : dict Range argument given by pylsp. Not used here. - context : Dict + context : dict CodeActionContext given as dict. Returns diff --git a/external-deps/python-lsp-server/pylsp/plugins/rope_completion.py b/external-deps/python-lsp-server/pylsp/plugins/rope_completion.py index b3a1f06645e..dc94ddea6a7 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/rope_completion.py +++ b/external-deps/python-lsp-server/pylsp/plugins/rope_completion.py @@ -22,7 +22,7 @@ def _resolve_completion(completion, data, markup_kind): except Exception as e: log.debug("Failed to resolve Rope completion: %s", e) doc = "" - completion["detail"] = "{0} {1}".format(data.scope or "", data.name) + completion["detail"] = "{} {}".format(data.scope or "", data.name) completion["documentation"] = doc return completion diff --git a/external-deps/python-lsp-server/pylsp/plugins/symbols.py b/external-deps/python-lsp-server/pylsp/plugins/symbols.py index 4e1890c1047..3a7beb076b6 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/symbols.py +++ b/external-deps/python-lsp-server/pylsp/plugins/symbols.py @@ -2,6 +2,7 @@ # Copyright 2021- Python Language Server Contributors. import logging +import re from pathlib import Path from pylsp import hookimpl @@ -19,6 +20,9 @@ def pylsp_document_symbols(config, document): symbols = [] exclude = set({}) redefinitions = {} + pattern_import = re.compile( + r"^\s*(?!#)\s*(from\s+[.\w]+(\.[\w]+)*\s+import\s+[\w\s,()*]+|import\s+[\w\s,.*]+)" + ) while definitions != []: d = definitions.pop(0) @@ -27,7 +31,8 @@ def pylsp_document_symbols(config, document): if not add_import_symbols: # Skip if there's an import in the code the symbol is defined. code = d.get_line_code() - if " import " in code or "import " in code: + + if pattern_import.match(code): continue # Skip imported symbols comparing module names. diff --git a/external-deps/python-lsp-server/pylsp/py.typed b/external-deps/python-lsp-server/pylsp/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/external-deps/python-lsp-server/pylsp/python_lsp.py b/external-deps/python-lsp-server/pylsp/python_lsp.py index ba41d6aac68..6dfad9c358d 100644 --- a/external-deps/python-lsp-server/pylsp/python_lsp.py +++ b/external-deps/python-lsp-server/pylsp/python_lsp.py @@ -7,7 +7,7 @@ import threading import uuid from functools import partial -from typing import Any, Dict, List +from typing import Any try: import ujson as json @@ -382,7 +382,7 @@ def watch_parent_process(pid): def m_initialized(self, **_kwargs) -> None: self._hook("pylsp_initialized") - def code_actions(self, doc_uri: str, range: Dict, context: Dict): + def code_actions(self, doc_uri: str, range: dict, context: dict): return flatten( self._hook("pylsp_code_actions", doc_uri, range=range, context=context) ) @@ -471,7 +471,7 @@ def _lint_notebook_document(self, notebook_document, workspace) -> None: random_uri = str(uuid.uuid4()) # cell_list helps us map the diagnostics back to the correct cell later. - cell_list: List[Dict[str, Any]] = [] + cell_list: list[dict[str, Any]] = [] offset = 0 total_source = "" diff --git a/external-deps/python-lsp-server/pylsp/uris.py b/external-deps/python-lsp-server/pylsp/uris.py index cba5b2909d9..8ebd8e315ef 100644 --- a/external-deps/python-lsp-server/pylsp/uris.py +++ b/external-deps/python-lsp-server/pylsp/uris.py @@ -61,7 +61,7 @@ def to_fs_path(uri): if netloc and path and scheme == "file": # unc path: file://shares/c$/far/boo - value = "//{}{}".format(netloc, path) + value = f"//{netloc}{path}" elif RE_DRIVE_LETTER_PATH.match(path): # windows drive letter: file:///C:/far/boo diff --git a/external-deps/python-lsp-server/pylsp/workspace.py b/external-deps/python-lsp-server/pylsp/workspace.py index 005c177d083..290b95ee6ae 100644 --- a/external-deps/python-lsp-server/pylsp/workspace.py +++ b/external-deps/python-lsp-server/pylsp/workspace.py @@ -7,9 +7,10 @@ import os import re import uuid +from collections.abc import Generator from contextlib import contextmanager from threading import RLock -from typing import Callable, Generator, List, Optional +from typing import Callable, Optional import jedi @@ -436,7 +437,7 @@ def lines(self): @lock def source(self): if self._source is None: - with io.open(self.path, "r", encoding="utf-8") as f: + with open(self.path, encoding="utf-8") as f: return f.read() return self._source @@ -625,7 +626,7 @@ def __init__( def __str__(self): return "Notebook with URI '%s'" % str(self.uri) - def add_cells(self, new_cells: List, start: int) -> None: + def add_cells(self, new_cells: list, start: int) -> None: self.cells[start:start] = new_cells def remove_cells(self, start: int, delete_count: int) -> None: diff --git a/external-deps/python-lsp-server/pyproject.toml b/external-deps/python-lsp-server/pyproject.toml index f9c6a5216b4..62f345b0a57 100644 --- a/external-deps/python-lsp-server/pyproject.toml +++ b/external-deps/python-lsp-server/pyproject.toml @@ -2,7 +2,7 @@ # Copyright 2021- Python Language Server Contributors. [build-system] -requires = ["setuptools>=61.2.0", "setuptools_scm[toml]>=3.4.3"] +requires = ["setuptools>=69.0.0", "setuptools_scm[toml]>=3.4.3"] build-backend = "setuptools.build_meta" [project] @@ -11,7 +11,7 @@ authors = [{name = "Python Language Server Contributors"}] description = "Python Language Server for the Language Server Protocol" readme = "README.md" license = {text = "MIT"} -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ "docstring-to-markdown", "importlib_metadata>=4.8.3;python_version<\"3.10\"", @@ -19,6 +19,7 @@ dependencies = [ "pluggy>=1.0.0", "python-lsp-jsonrpc>=1.1.0,<2.0.0", "ujson>=3.0.0", + "black" ] dynamic = ["version"] @@ -120,8 +121,8 @@ exclude = [ line-length = 88 indent-width = 4 -# Assume Python 3.8 -target-version = "py38" +# Assume Python 3.9 +target-version = "py39" [tool.ruff.lint] # https://docs.astral.sh/ruff/rules/ diff --git a/external-deps/python-lsp-server/test/plugins/test_autoimport.py b/external-deps/python-lsp-server/test/plugins/test_autoimport.py index dbad8d02d32..cbe3dde1e68 100644 --- a/external-deps/python-lsp-server/test/plugins/test_autoimport.py +++ b/external-deps/python-lsp-server/test/plugins/test_autoimport.py @@ -1,6 +1,6 @@ # Copyright 2022- Python Language Server Contributors. -from typing import Any, Dict, List +from typing import Any from unittest.mock import Mock, patch import jedi @@ -26,14 +26,14 @@ DOC_URI = uris.from_fs_path(__file__) -def contains_autoimport_completion(suggestion: Dict[str, Any], module: str) -> bool: +def contains_autoimport_completion(suggestion: dict[str, Any], module: str) -> bool: """Checks if `suggestion` contains an autoimport completion for `module`.""" return suggestion.get("label", "") == module and "import" in suggestion.get( "detail", "" ) -def contains_autoimport_quickfix(suggestion: Dict[str, Any], module: str) -> bool: +def contains_autoimport_quickfix(suggestion: dict[str, Any], module: str) -> bool: """Checks if `suggestion` contains an autoimport quick fix for `module`.""" return suggestion.get("title", "") == f"import {module}" @@ -78,7 +78,7 @@ def should_insert(phrase: str, position: int): return _should_insert(expr, word_node) -def check_dict(query: Dict, results: List[Dict]) -> bool: +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 diff --git a/external-deps/python-lsp-server/test/plugins/test_completion.py b/external-deps/python-lsp-server/test/plugins/test_completion.py index b8de89127bf..3ba8dbdd096 100644 --- a/external-deps/python-lsp-server/test/plugins/test_completion.py +++ b/external-deps/python-lsp-server/test/plugins/test_completion.py @@ -5,7 +5,7 @@ import os import sys from pathlib import Path -from typing import Dict, NamedTuple +from typing import NamedTuple import pytest @@ -66,7 +66,7 @@ class TypeCase(NamedTuple): # fmt: off -TYPE_CASES: Dict[str, TypeCase] = { +TYPE_CASES: dict[str, TypeCase] = { "variable": TypeCase( document="test = 1\ntes", position={"line": 1, "character": 3}, diff --git a/external-deps/python-lsp-server/test/plugins/test_flake8_lint.py b/external-deps/python-lsp-server/test/plugins/test_flake8_lint.py index e7b6b001c12..d8199d63481 100644 --- a/external-deps/python-lsp-server/test/plugins/test_flake8_lint.py +++ b/external-deps/python-lsp-server/test/plugins/test_flake8_lint.py @@ -125,20 +125,20 @@ def test_flake8_respecting_configuration(workspace) -> None: def test_flake8_config_param(workspace) -> None: with patch("pylsp.plugins.flake8_lint.Popen") as popen_mock: mock_instance = popen_mock.return_value - mock_instance.communicate.return_value = [bytes(), bytes()] + mock_instance.communicate.return_value = [b"", b""] flake8_conf = "/tmp/some.cfg" workspace._config.update({"plugins": {"flake8": {"config": flake8_conf}}}) _name, doc = temp_document(DOC, workspace) flake8_lint.pylsp_lint(workspace, doc) (call_args,) = popen_mock.call_args[0] assert "flake8" in call_args - assert "--config={}".format(flake8_conf) in call_args + assert f"--config={flake8_conf}" in call_args def test_flake8_executable_param(workspace) -> None: with patch("pylsp.plugins.flake8_lint.Popen") as popen_mock: mock_instance = popen_mock.return_value - mock_instance.communicate.return_value = [bytes(), bytes()] + mock_instance.communicate.return_value = [b"", b""] flake8_executable = "/tmp/flake8" workspace._config.update( @@ -187,7 +187,7 @@ def test_flake8_multiline(workspace) -> None: with patch("pylsp.plugins.flake8_lint.Popen") as popen_mock: mock_instance = popen_mock.return_value - mock_instance.communicate.return_value = [bytes(), bytes()] + mock_instance.communicate.return_value = [b"", b""] doc = workspace.get_document(doc_uri) flake8_lint.pylsp_lint(workspace, doc) diff --git a/external-deps/python-lsp-server/test/plugins/test_hover.py b/external-deps/python-lsp-server/test/plugins/test_hover.py index 9674b872c92..b507acd2c7e 100644 --- a/external-deps/python-lsp-server/test/plugins/test_hover.py +++ b/external-deps/python-lsp-server/test/plugins/test_hover.py @@ -10,7 +10,7 @@ DOC_URI = uris.from_fs_path(__file__) DOC = """ -def main(): +def main(a: float, b: float): \"\"\"hello world\"\"\" pass """ @@ -79,13 +79,47 @@ def test_hover(workspace) -> None: doc = Document(DOC_URI, workspace, DOC) - contents = {"kind": "markdown", "value": "```python\nmain()\n```\n\n\nhello world"} + contents = { + "kind": "markdown", + "value": "```python\nmain(a: float, b: float)\n```\n\n\nhello world", + } assert {"contents": contents} == pylsp_hover(doc._config, doc, hov_position) assert {"contents": ""} == pylsp_hover(doc._config, doc, no_hov_position) +def test_hover_signature_formatting(workspace) -> None: + # Over 'main' in def main(): + hov_position = {"line": 2, "character": 6} + + doc = Document(DOC_URI, workspace, DOC) + # setting low line length should trigger reflow to multiple lines + doc._config.update({"signature": {"line_length": 10}}) + + contents = { + "kind": "markdown", + "value": "```python\nmain(\n a: float,\n b: float,\n)\n```\n\n\nhello world", + } + + assert {"contents": contents} == pylsp_hover(doc._config, doc, hov_position) + + +def test_hover_signature_formatting_opt_out(workspace) -> None: + # Over 'main' in def main(): + hov_position = {"line": 2, "character": 6} + + doc = Document(DOC_URI, workspace, DOC) + doc._config.update({"signature": {"line_length": 10, "formatter": None}}) + + contents = { + "kind": "markdown", + "value": "```python\nmain(a: float, b: float)\n```\n\n\nhello world", + } + + assert {"contents": contents} == pylsp_hover(doc._config, doc, hov_position) + + def test_document_path_hover(workspace_other_root_path, tmpdir) -> None: # Create a dummy module out of the workspace's root_path and try to get # a definition on it in another file placed next to it. diff --git a/external-deps/python-lsp-server/test/plugins/test_symbols.py b/external-deps/python-lsp-server/test/plugins/test_symbols.py index c00ab935dc3..242a38a1cf9 100644 --- a/external-deps/python-lsp-server/test/plugins/test_symbols.py +++ b/external-deps/python-lsp-server/test/plugins/test_symbols.py @@ -30,6 +30,17 @@ def main(x): """ +DOC_IMPORTS = """from . import something +from ..module import something +from module import (a, b) + +def main(): + # import ignored + print("from module import x") # string with import + return something + +""" + def helper_check_symbols_all_scope(symbols): # All eight symbols (import sys, a, B, __init__, x, y, main, y) @@ -73,6 +84,24 @@ def sym(name): assert sym("main")["location"]["range"]["end"] == {"line": 12, "character": 0} +def test_symbols_complex_imports(config, workspace): + doc = Document(DOC_URI, workspace, DOC_IMPORTS) + config.update({"plugins": {"jedi_symbols": {"all_scopes": False}}}) + symbols = pylsp_document_symbols(config, doc) + + import_symbols = [s for s in symbols if s["kind"] == SymbolKind.Module] + + assert len(import_symbols) == 4 + + names = [s["name"] for s in import_symbols] + assert "something" in names + assert "a" in names or "b" in names + + assert any( + s["name"] == "main" and s["kind"] == SymbolKind.Function for s in symbols + ) + + def test_symbols_all_scopes(config, workspace) -> None: doc = Document(DOC_URI, workspace, DOC) symbols = pylsp_document_symbols(config, doc) diff --git a/external-deps/python-lsp-server/test/test_utils.py b/external-deps/python-lsp-server/test/test_utils.py index 966c469e960..7ed6214f75a 100644 --- a/external-deps/python-lsp-server/test/test_utils.py +++ b/external-deps/python-lsp-server/test/test_utils.py @@ -6,7 +6,7 @@ import sys import time from threading import Thread -from typing import Any, Dict, List +from typing import Any from unittest import mock from docstring_to_markdown import UnknownFormatError @@ -19,7 +19,7 @@ CALL_TIMEOUT_IN_SECONDS = 30 -def send_notebook_did_open(client, cells: List[str]) -> None: +def send_notebook_did_open(client, cells: list[str]) -> None: """ Sends a notebookDocument/didOpen notification with the given python cells. @@ -31,7 +31,7 @@ def send_notebook_did_open(client, cells: List[str]) -> None: ) -def notebook_with_python_cells(cells: List[str]): +def notebook_with_python_cells(cells: list[str]): """ Create a notebook document with the given python cells. @@ -61,7 +61,7 @@ def notebook_with_python_cells(cells: List[str]): } -def send_initialize_request(client, initialization_options: Dict[str, Any] = None): +def send_initialize_request(client, initialization_options: dict[str, Any] = None): return client._endpoint.request( "initialize", {