From 3e5e0c0af1e726a01379b88dbfff3cfcb95ed10a Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 1 Nov 2022 20:27:52 -0500 Subject: [PATCH 1/6] git subrepo pull external-deps/python-lsp-server subrepo: subdir: "external-deps/python-lsp-server" merged: "5c37673ce" upstream: origin: "https://github.com/python-lsp/python-lsp-server.git" branch: "develop" commit: "5c37673ce" git-subrepo: version: "0.4.3" origin: "https://github.com/ingydotnet/git-subrepo" commit: "2f68596" --- external-deps/python-lsp-server/.gitrepo | 4 +- .../python-lsp-server/CONFIGURATION.md | 21 ++--- .../python-lsp-server/pylsp/_utils.py | 79 +++++++++++++++++-- .../python-lsp-server/pylsp/config/config.py | 2 +- .../pylsp/config/schema.json | 34 +++++--- .../pylsp/plugins/autopep8_format.py | 4 +- .../python-lsp-server/pylsp/plugins/hover.py | 29 +++---- .../pylsp/plugins/jedi_completion.py | 40 +++++++--- .../pylsp/plugins/rope_completion.py | 26 ++++-- .../pylsp/plugins/signature.py | 21 ++++- .../pylsp/plugins/yapf_format.py | 4 +- .../python-lsp-server/pylsp/workspace.py | 4 + .../python-lsp-server/pyproject.toml | 3 +- .../scripts/jsonschema2md.py | 2 +- external-deps/python-lsp-server/setup.py | 1 + .../test/plugins/test_autopep8_format.py | 10 +-- .../test/plugins/test_completion.py | 11 ++- .../test/plugins/test_hover.py | 22 +++--- .../test/plugins/test_signature.py | 10 +-- .../test/plugins/test_yapf_format.py | 12 +-- 20 files changed, 234 insertions(+), 105 deletions(-) diff --git a/external-deps/python-lsp-server/.gitrepo b/external-deps/python-lsp-server/.gitrepo index ae4f2c7880c..830ff5519c3 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 = 6501e9eb80c7c3b35cf93f634834558fc88dee68 - parent = 13da995dc08e14f82c07d018ca7130021fc7ad56 + commit = 5c37673ce1a49651bb0e0866a8602a994f434b45 + parent = ef4a41ee7fb7f0afe03311e856f04650930555c5 method = merge cmdver = 0.4.3 diff --git a/external-deps/python-lsp-server/CONFIGURATION.md b/external-deps/python-lsp-server/CONFIGURATION.md index a1d4773ec95..4cff0c91104 100644 --- a/external-deps/python-lsp-server/CONFIGURATION.md +++ b/external-deps/python-lsp-server/CONFIGURATION.md @@ -1,9 +1,9 @@ # Python Language Server Configuration -This server can be configured using `workspace/didChangeConfiguration` method. Each configuration option is described below: +This server can be configured using the `workspace/didChangeConfiguration` method. Each configuration option is described below. Note, a value of `null` means that we do not set a value and thus use the plugin's default value. | **Configuration Key** | **Type** | **Description** | **Default** |----|----|----|----| -| `pylsp.configurationSources` | `array` of unique `string` (one of: `pycodestyle`, `pyflakes`) items | List of configuration sources to use. | `["pycodestyle"]` | +| `pylsp.configurationSources` | `array` of unique `string` (one of: `'pycodestyle'`, `'flake8'`) items | List of configuration sources to use. | `["pycodestyle"]` | | `pylsp.plugins.autopep8.enabled` | `boolean` | Enable or disable the plugin (disabling required to use `yapf`). | `true` | | `pylsp.plugins.flake8.config` | `string` | Path to the config file that will be the authoritative config source. | `null` | | `pylsp.plugins.flake8.enabled` | `boolean` | Enable or disable the plugin. | `false` | @@ -16,16 +16,17 @@ This server can be configured using `workspace/didChangeConfiguration` method. E | `pylsp.plugins.flake8.indentSize` | `integer` | Set indentation spaces. | `null` | | `pylsp.plugins.flake8.perFileIgnores` | `array` of `string` items | A pairing of filenames and violation codes that defines which violations to ignore in a particular file, for example: `["file_path.py:W305,W304"]`). | `[]` | | `pylsp.plugins.flake8.select` | `array` of unique `string` items | List of errors and warnings to enable. | `null` | +| `pylsp.plugins.jedi.auto_import_modules` | `array` of `string` items | List of module names for jedi.settings.auto_import_modules. | `["numpy"]` | | `pylsp.plugins.jedi.extra_paths` | `array` of `string` items | Define extra paths for jedi.Script. | `[]` | | `pylsp.plugins.jedi.env_vars` | `object` | Define environment variables for jedi.Script and Jedi.names. | `null` | | `pylsp.plugins.jedi.environment` | `string` | Define environment for jedi.Script and Jedi.names. | `null` | | `pylsp.plugins.jedi_completion.enabled` | `boolean` | Enable or disable the plugin. | `true` | | `pylsp.plugins.jedi_completion.include_params` | `boolean` | Auto-completes methods and classes with tabstops for each parameter. | `true` | -| `pylsp.plugins.jedi_completion.include_class_objects` | `boolean` | Adds class objects as a separate completion item. | `true` | -| `pylsp.plugins.jedi_completion.include_function_objects` | `boolean` | Adds function objects as a separate completion item. | `true` | +| `pylsp.plugins.jedi_completion.include_class_objects` | `boolean` | Adds class objects as a separate completion item. | `false` | +| `pylsp.plugins.jedi_completion.include_function_objects` | `boolean` | Adds function objects as a separate completion item. | `false` | | `pylsp.plugins.jedi_completion.fuzzy` | `boolean` | Enable fuzzy when requesting autocomplete. | `false` | | `pylsp.plugins.jedi_completion.eager` | `boolean` | Resolve documentation and detail eagerly. | `false` | -| `pylsp.plugins.jedi_completion.resolve_at_most` | `number` | How many labels and snippets (at most) should be resolved? | `25` | +| `pylsp.plugins.jedi_completion.resolve_at_most` | `integer` | How many labels and snippets (at most) should be resolved? | `25` | | `pylsp.plugins.jedi_completion.cache_for` | `array` of `string` items | Modules for which labels and snippets should be cached. | `["pandas", "numpy", "tensorflow", "matplotlib"]` | | `pylsp.plugins.jedi_definition.enabled` | `boolean` | Enable or disable the plugin. | `true` | | `pylsp.plugins.jedi_definition.follow_imports` | `boolean` | The goto call will follow imports. | `true` | @@ -37,23 +38,23 @@ This server can be configured using `workspace/didChangeConfiguration` method. E | `pylsp.plugins.jedi_symbols.all_scopes` | `boolean` | If True lists the names of all scopes instead of only the module namespace. | `true` | | `pylsp.plugins.jedi_symbols.include_import_symbols` | `boolean` | If True includes symbols imported from other libraries. | `true` | | `pylsp.plugins.mccabe.enabled` | `boolean` | Enable or disable the plugin. | `true` | -| `pylsp.plugins.mccabe.threshold` | `number` | The minimum threshold that triggers warnings about cyclomatic complexity. | `15` | +| `pylsp.plugins.mccabe.threshold` | `integer` | The minimum threshold that triggers warnings about cyclomatic complexity. | `15` | | `pylsp.plugins.preload.enabled` | `boolean` | Enable or disable the plugin. | `true` | | `pylsp.plugins.preload.modules` | `array` of unique `string` items | List of modules to import on startup | `[]` | | `pylsp.plugins.pycodestyle.enabled` | `boolean` | Enable or disable the plugin. | `true` | | `pylsp.plugins.pycodestyle.exclude` | `array` of unique `string` items | Exclude files or directories which match these patterns. | `[]` | | `pylsp.plugins.pycodestyle.filename` | `array` of unique `string` items | When parsing directories, only check filenames matching these patterns. | `[]` | -| `pylsp.plugins.pycodestyle.select` | `array` of unique `string` items | Select errors and warnings | `[]` | +| `pylsp.plugins.pycodestyle.select` | `array` of unique `string` items | Select errors and warnings | `null` | | `pylsp.plugins.pycodestyle.ignore` | `array` of unique `string` items | Ignore errors and warnings | `[]` | | `pylsp.plugins.pycodestyle.hangClosing` | `boolean` | Hang closing bracket instead of matching indentation of opening bracket's line. | `null` | -| `pylsp.plugins.pycodestyle.maxLineLength` | `number` | Set maximum allowed line length. | `null` | +| `pylsp.plugins.pycodestyle.maxLineLength` | `integer` | Set maximum allowed line length. | `null` | | `pylsp.plugins.pycodestyle.indentSize` | `integer` | Set indentation spaces. | `null` | | `pylsp.plugins.pydocstyle.enabled` | `boolean` | Enable or disable the plugin. | `false` | -| `pylsp.plugins.pydocstyle.convention` | `string` (one of: `pep257`, `numpy`, `None`) | Choose the basic list of checked errors by specifying an existing convention. | `null` | +| `pylsp.plugins.pydocstyle.convention` | `string` (one of: `'pep257'`, `'numpy'`, `None`) | Choose the basic list of checked errors by specifying an existing convention. | `null` | | `pylsp.plugins.pydocstyle.addIgnore` | `array` of unique `string` items | Ignore errors and warnings in addition to the specified convention. | `[]` | | `pylsp.plugins.pydocstyle.addSelect` | `array` of unique `string` items | Select errors and warnings in addition to the specified convention. | `[]` | | `pylsp.plugins.pydocstyle.ignore` | `array` of unique `string` items | Ignore errors and warnings | `[]` | -| `pylsp.plugins.pydocstyle.select` | `array` of unique `string` items | Select errors and warnings | `[]` | +| `pylsp.plugins.pydocstyle.select` | `array` of unique `string` items | Select errors and warnings | `null` | | `pylsp.plugins.pydocstyle.match` | `string` | Check only files that exactly match the given regular expression; default is to match files that don't start with 'test_' but end with '.py'. | `"(?!test_).*\\.py"` | | `pylsp.plugins.pydocstyle.matchDir` | `string` | Search only dirs that exactly match the given regular expression; default is to match dirs which do not begin with a dot. | `"[^\\.].*"` | | `pylsp.plugins.pyflakes.enabled` | `boolean` | Enable or disable the plugin. | `true` | diff --git a/external-deps/python-lsp-server/pylsp/_utils.py b/external-deps/python-lsp-server/pylsp/_utils.py index 8c4b54961d3..2c6111d8eb0 100644 --- a/external-deps/python-lsp-server/pylsp/_utils.py +++ b/external-deps/python-lsp-server/pylsp/_utils.py @@ -8,7 +8,9 @@ import pathlib import re import threading +from typing import List, Optional +import docstring_to_markdown import jedi JEDI_VERSION = jedi.__version__ @@ -144,17 +146,84 @@ def _merge_dicts_(a, b): return dict(_merge_dicts_(dict_a, dict_b)) -def format_docstring(contents): - """Python doc strings come in a number of formats, but LSP wants markdown. - - Until we can find a fast enough way of discovering and parsing each format, - we can do a little better by at least preserving indentation. +def escape_plain_text(contents: str) -> str: + """ + Format plain text to display nicely in environments which do not respect whitespaces. """ contents = contents.replace('\t', '\u00A0' * 4) contents = contents.replace(' ', '\u00A0' * 2) return contents +def escape_markdown(contents: str) -> str: + """ + Format plain text to display nicely in Markdown environment. + """ + # escape markdown syntax + contents = re.sub(r'([\\*_#[\]])', r'\\\1', contents) + # preserve white space characters + contents = escape_plain_text(contents) + return contents + + +def wrap_signature(signature): + return '```python\n' + signature + '\n```\n' + + +SERVER_SUPPORTED_MARKUP_KINDS = {'markdown', 'plaintext'} + + +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. + """ + for kind in client_supported_markup_kinds: + if kind in SERVER_SUPPORTED_MARKUP_KINDS: + return kind + return 'markdown' + + +def format_docstring(contents: str, markup_kind: str, signatures: Optional[List[str]] = None): + """Transform the provided docstring into a MarkupContent object. + + If `markup_kind` is 'markdown' the docstring will get converted to + markdown representation using `docstring-to-markdown`; if it is + `plaintext`, it will be returned as plain text. + Call signatures of functions (or equivalent code summaries) + provided in optional `signatures` argument will be prepended + to the provided contents of the docstring if given. + """ + if not isinstance(contents, str): + contents = '' + + if markup_kind == 'markdown': + try: + value = docstring_to_markdown.convert(contents) + return { + 'kind': 'markdown', + 'value': value + } + except docstring_to_markdown.UnknownFormatError: + # try to escape the Markdown syntax instead: + value = escape_markdown(contents) + + if signatures: + value = wrap_signature('\n'.join(signatures)) + '\n\n' + value + + return { + 'kind': 'markdown', + 'value': value + } + value = contents + if signatures: + value = '\n'.join(signatures) + '\n\n' + value + return { + 'kind': 'plaintext', + 'value': escape_plain_text(value) + } + + def clip_column(column, lines, line_number): """ Normalise the position as per the LSP that accepts character positions > line length diff --git a/external-deps/python-lsp-server/pylsp/config/config.py b/external-deps/python-lsp-server/pylsp/config/config.py index 4ddb4988adc..66874961bff 100644 --- a/external-deps/python-lsp-server/pylsp/config/config.py +++ b/external-deps/python-lsp-server/pylsp/config/config.py @@ -71,7 +71,7 @@ def __init__(self, root_uri, init_opts, process_id, capabilities): try: entry_point.load() except Exception as e: # pylint: disable=broad-except - log.warning("Failed to load %s entry point '%s': %s", PYLSP, entry_point.name, e) + log.info("Failed to load %s entry point '%s': %s", PYLSP, entry_point.name, e) self._pm.set_blocked(entry_point.name) # Load the entry points into pluggy, having blocked any failing ones diff --git a/external-deps/python-lsp-server/pylsp/config/schema.json b/external-deps/python-lsp-server/pylsp/config/schema.json index 9e744ac3a98..d210d0d24ff 100644 --- a/external-deps/python-lsp-server/pylsp/config/schema.json +++ b/external-deps/python-lsp-server/pylsp/config/schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Python Language Server Configuration", - "description": "This server can be configured using `workspace/didChangeConfiguration` method. Each configuration option is described below:", + "description": "This server can be configured using the `workspace/didChangeConfiguration` method. Each configuration option is described below. Note, a value of `null` means that we do not set a value and thus use the plugin's default value.", "type": "object", "properties": { "pylsp.configurationSources": { @@ -10,7 +10,7 @@ "description": "List of configuration sources to use.", "items": { "type": "string", - "enum": ["pycodestyle", "pyflakes"] + "enum": ["pycodestyle", "flake8"] }, "uniqueItems": true }, @@ -87,6 +87,14 @@ "uniqueItems": true, "description": "List of errors and warnings to enable." }, + "pylsp.plugins.jedi.auto_import_modules": { + "type": "array", + "default": ["numpy"], + "items": { + "type": "string" + }, + "description": "List of module names for jedi.settings.auto_import_modules." + }, "pylsp.plugins.jedi.extra_paths": { "type": "array", "default": [], @@ -117,12 +125,12 @@ }, "pylsp.plugins.jedi_completion.include_class_objects": { "type": "boolean", - "default": true, + "default": false, "description": "Adds class objects as a separate completion item." }, "pylsp.plugins.jedi_completion.include_function_objects": { "type": "boolean", - "default": true, + "default": false, "description": "Adds function objects as a separate completion item." }, "pylsp.plugins.jedi_completion.fuzzy": { @@ -136,7 +144,7 @@ "description": "Resolve documentation and detail eagerly." }, "pylsp.plugins.jedi_completion.resolve_at_most": { - "type": "number", + "type": "integer", "default": 25, "description": "How many labels and snippets (at most) should be resolved?" }, @@ -199,7 +207,7 @@ "description": "Enable or disable the plugin." }, "pylsp.plugins.mccabe.threshold": { - "type": "number", + "type": "integer", "default": 15, "description": "The minimum threshold that triggers warnings about cyclomatic complexity." }, @@ -241,8 +249,8 @@ "description": "When parsing directories, only check filenames matching these patterns." }, "pylsp.plugins.pycodestyle.select": { - "type": "array", - "default": [], + "type": ["array", "null"], + "default": null, "items": { "type": "string" }, @@ -264,7 +272,7 @@ "description": "Hang closing bracket instead of matching indentation of opening bracket's line." }, "pylsp.plugins.pycodestyle.maxLineLength": { - "type": ["number", "null"], + "type": ["integer", "null"], "default": null, "description": "Set maximum allowed line length." }, @@ -312,8 +320,8 @@ "description": "Ignore errors and warnings" }, "pylsp.plugins.pydocstyle.select": { - "type": "array", - "default": [], + "type": ["array", "null"], + "default": null, "items": { "type": "string" }, @@ -370,12 +378,12 @@ "description": "Enable or disable the plugin." }, "pylsp.rope.extensionModules": { - "type": ["null", "string"], + "type": ["string", "null"], "default": null, "description": "Builtin and c-extension modules that are allowed to be imported and inspected by rope." }, "pylsp.rope.ropeFolder": { - "type": ["null", "array"], + "type": ["array", "null"], "default": null, "items": { "type": "string" diff --git a/external-deps/python-lsp-server/pylsp/plugins/autopep8_format.py b/external-deps/python-lsp-server/pylsp/plugins/autopep8_format.py index f605f830bc8..8d184b7a0c9 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/autopep8_format.py +++ b/external-deps/python-lsp-server/pylsp/plugins/autopep8_format.py @@ -13,13 +13,13 @@ @hookimpl(tryfirst=True) # Prefer autopep8 over YAPF -def pylsp_format_document(config, document, options=None): # pylint: disable=unused-argument +def pylsp_format_document(config, document, options): # pylint: disable=unused-argument log.info("Formatting document %s with autopep8", document) return _format(config, document) @hookimpl(tryfirst=True) # Prefer autopep8 over YAPF -def pylsp_format_range(config, document, range, options=None): # pylint: disable=redefined-builtin,unused-argument +def pylsp_format_range(config, document, range, options): # pylint: disable=redefined-builtin,unused-argument log.info("Formatting document %s in range %s with autopep8", document, range) # First we 'round' the range up/down to full lines only diff --git a/external-deps/python-lsp-server/pylsp/plugins/hover.py b/external-deps/python-lsp-server/pylsp/plugins/hover.py index a4d45d1ce4d..f6ae4d7f99d 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/hover.py +++ b/external-deps/python-lsp-server/pylsp/plugins/hover.py @@ -9,7 +9,7 @@ @hookimpl -def pylsp_hover(document, position): +def pylsp_hover(config, document, position): code_position = _utils.position_to_jedi_linecolumn(document, position) definitions = document.jedi_script(use_document_path=True).infer(**code_position) word = document.word_at_position(position) @@ -26,24 +26,19 @@ def pylsp_hover(document, position): if not definition: return {'contents': ''} - # raw docstring returns only doc, without signature - doc = _utils.format_docstring(definition.docstring(raw=True)) + hover_capabilities = config.capabilities.get('textDocument', {}).get('hover', {}) + supported_markup_kinds = hover_capabilities.get('contentFormat', ['markdown']) + preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) # Find first exact matching signature signature = next((x.to_string() for x in definition.get_signatures() if x.name == word), '') - contents = [] - if signature: - contents.append({ - 'language': 'python', - 'value': signature, - }) - - if doc: - contents.append(doc) - - if not contents: - return {'contents': ''} - - return {'contents': contents} + return { + 'contents': _utils.format_docstring( + # raw docstring returns only doc, without signature + definition.docstring(raw=True), + preferred_markup_kind, + signatures=[signature] if signature else None + ) + } 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 b5e259e4a39..4c79ebf5cea 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/jedi_completion.py +++ b/external-deps/python-lsp-server/pylsp/plugins/jedi_completion.py @@ -50,11 +50,14 @@ def pylsp_completions(config, document, position): return None completion_capabilities = config.capabilities.get('textDocument', {}).get('completion', {}) - snippet_support = completion_capabilities.get('completionItem', {}).get('snippetSupport') + item_capabilities = completion_capabilities.get('completionItem', {}) + snippet_support = item_capabilities.get('snippetSupport') + supported_markup_kinds = item_capabilities.get('documentationFormat', ['markdown']) + preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) should_include_params = settings.get('include_params') - should_include_class_objects = settings.get('include_class_objects', True) - should_include_function_objects = settings.get('include_function_objects', True) + should_include_class_objects = settings.get('include_class_objects', False) + should_include_function_objects = settings.get('include_function_objects', False) max_to_resolve = settings.get('resolve_at_most', 25) modules_to_cache_for = settings.get('cache_for', None) @@ -69,7 +72,8 @@ def pylsp_completions(config, document, position): ready_completions = [ _format_completion( c, - include_params, + markup_kind=preferred_markup_kind, + include_params=include_params if c.type in ["class", "function"] else False, resolve=resolve_eagerly, resolve_label_or_snippet=(i < max_to_resolve) ) @@ -82,7 +86,8 @@ def pylsp_completions(config, document, position): if c.type == 'class': completion_dict = _format_completion( c, - False, + markup_kind=preferred_markup_kind, + include_params=False, resolve=resolve_eagerly, resolve_label_or_snippet=(i < max_to_resolve) ) @@ -119,12 +124,18 @@ def pylsp_completions(config, document, position): @hookimpl -def pylsp_completion_item_resolve(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']) + + completion_capabilities = config.capabilities.get('textDocument', {}).get('completion', {}) + item_capabilities = completion_capabilities.get('completionItem', {}) + supported_markup_kinds = item_capabilities.get('documentationFormat', ['markdown']) + preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) + if shared_data: completion, data = shared_data - return _resolve_completion(completion, data) + return _resolve_completion(completion, data, markup_kind=preferred_markup_kind) return completion_item @@ -178,18 +189,25 @@ def use_snippets(document, position): not (expr_type in _ERRORS and 'import' in code)) -def _resolve_completion(completion, d): +def _resolve_completion(completion, d, markup_kind: str): # pylint: disable=broad-except completion['detail'] = _detail(d) try: - docs = _utils.format_docstring(d.docstring()) + docs = _utils.format_docstring( + d.docstring(raw=True), + signatures=[ + signature.to_string() + for signature in d.get_signatures() + ], + markup_kind=markup_kind + ) except Exception: docs = '' completion['documentation'] = docs return completion -def _format_completion(d, include_params=True, resolve=False, resolve_label_or_snippet=False): +def _format_completion(d, markup_kind: str, include_params=True, resolve=False, resolve_label_or_snippet=False): completion = { 'label': _label(d, resolve_label_or_snippet), 'kind': _TYPE_MAP.get(d.type), @@ -198,7 +216,7 @@ def _format_completion(d, include_params=True, resolve=False, resolve_label_or_s } if resolve: - completion = _resolve_completion(completion, d) + completion = _resolve_completion(completion, d, markup_kind) if d.type == 'path': path = osp.normpath(d.name) 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 502d2390c4c..5bb36a5f343 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/rope_completion.py +++ b/external-deps/python-lsp-server/pylsp/plugins/rope_completion.py @@ -4,7 +4,7 @@ import logging from rope.contrib.codeassist import code_assist, sorted_proposals -from pylsp import hookimpl, lsp +from pylsp import _utils, hookimpl, lsp log = logging.getLogger(__name__) @@ -16,10 +16,13 @@ def pylsp_settings(): return {'plugins': {'rope_completion': {'enabled': False, 'eager': False}}} -def _resolve_completion(completion, data): +def _resolve_completion(completion, data, markup_kind): # pylint: disable=broad-except try: - doc = data.get_doc() + doc = _utils.format_docstring( + data.get_doc(), + markup_kind=markup_kind + ) except Exception as e: log.debug("Failed to resolve Rope completion: %s", e) doc = "" @@ -49,6 +52,11 @@ def pylsp_completions(config, workspace, document, position): rope_project = workspace._rope_project_builder(rope_config) document_rope = document._rope_resource(rope_config) + completion_capabilities = config.capabilities.get('textDocument', {}).get('completion', {}) + item_capabilities = completion_capabilities.get('completionItem', {}) + supported_markup_kinds = item_capabilities.get('documentationFormat', ['markdown']) + preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) + try: definitions = code_assist(rope_project, document.source, offset, document_rope, maxfixes=3) except Exception as e: # pylint: disable=broad-except @@ -67,7 +75,7 @@ def pylsp_completions(config, workspace, document, position): } } if resolve_eagerly: - item = _resolve_completion(item, d) + item = _resolve_completion(item, d, preferred_markup_kind) new_definitions.append(item) # most recently retrieved completion items, used for resolution @@ -83,12 +91,18 @@ def pylsp_completions(config, workspace, document, position): @hookimpl -def pylsp_completion_item_resolve(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_ROPE_COMPLETIONS'].get(completion_item['label']) + + completion_capabilities = config.capabilities.get('textDocument', {}).get('completion', {}) + item_capabilities = completion_capabilities.get('completionItem', {}) + supported_markup_kinds = item_capabilities.get('documentationFormat', ['markdown']) + preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) + if shared_data: completion, data = shared_data - return _resolve_completion(completion, data) + return _resolve_completion(completion, data, preferred_markup_kind) return completion_item diff --git a/external-deps/python-lsp-server/pylsp/plugins/signature.py b/external-deps/python-lsp-server/pylsp/plugins/signature.py index c4c3048f612..4907a6e3c58 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/signature.py +++ b/external-deps/python-lsp-server/pylsp/plugins/signature.py @@ -15,28 +15,41 @@ @hookimpl -def pylsp_signature_help(document, position): +def pylsp_signature_help(config, document, position): code_position = _utils.position_to_jedi_linecolumn(document, position) signatures = document.jedi_script().get_signatures(**code_position) if not signatures: return {'signatures': []} + signature_capabilities = config.capabilities.get('textDocument', {}).get('signatureHelp', {}) + signature_information_support = signature_capabilities.get('signatureInformation', {}) + supported_markup_kinds = signature_information_support.get('documentationFormat', ['markdown']) + preferred_markup_kind = _utils.choose_markup_kind(supported_markup_kinds) + s = signatures[0] + docstring = s.docstring() + # Docstring contains one or more lines of signature, followed by empty line, followed by docstring - function_sig_lines = (s.docstring().split('\n\n') or [''])[0].splitlines() + function_sig_lines = (docstring.split('\n\n') or [''])[0].splitlines() function_sig = ' '.join([line.strip() for line in function_sig_lines]) sig = { 'label': function_sig, - 'documentation': _utils.format_docstring(s.docstring(raw=True)) + 'documentation': _utils.format_docstring( + s.docstring(raw=True), + markup_kind=preferred_markup_kind + ) } # If there are params, add those if s.params: sig['parameters'] = [{ 'label': p.name, - 'documentation': _param_docs(s.docstring(), p.name) + 'documentation': _utils.format_docstring( + _param_docs(docstring, p.name), + markup_kind=preferred_markup_kind + ) } for p in s.params] # We only return a single signature because Python doesn't allow overloading diff --git a/external-deps/python-lsp-server/pylsp/plugins/yapf_format.py b/external-deps/python-lsp-server/pylsp/plugins/yapf_format.py index c1b89051cb2..827aeb2d6e3 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/yapf_format.py +++ b/external-deps/python-lsp-server/pylsp/plugins/yapf_format.py @@ -16,12 +16,12 @@ @hookimpl -def pylsp_format_document(document, options=None): +def pylsp_format_document(document, options): return _format(document, options=options) @hookimpl -def pylsp_format_range(document, range, options=None): # pylint: disable=redefined-builtin +def pylsp_format_range(document, range, options): # pylint: disable=redefined-builtin # First we 'round' the range up/down to full lines only range['start']['character'] = 0 range['end']['line'] += 1 diff --git a/external-deps/python-lsp-server/pylsp/workspace.py b/external-deps/python-lsp-server/pylsp/workspace.py index bf312f6285e..5e91221714a 100644 --- a/external-deps/python-lsp-server/pylsp/workspace.py +++ b/external-deps/python-lsp-server/pylsp/workspace.py @@ -14,6 +14,8 @@ log = logging.getLogger(__name__) +DEFAULT_AUTO_IMPORT_MODULES = ["numpy"] + # TODO: this is not the best e.g. we capture numbers RE_START_WORD = re.compile('[A-Za-z_0-9]*$') RE_END_WORD = re.compile('^[A-Za-z_0-9]*') @@ -252,6 +254,8 @@ def jedi_script(self, position=None, use_document_path=False): if self._config: jedi_settings = self._config.plugin_settings('jedi', document_path=self.path) + jedi.settings.auto_import_modules = jedi_settings.get('auto_import_modules', + DEFAULT_AUTO_IMPORT_MODULES) environment_path = jedi_settings.get('environment') extra_paths = jedi_settings.get('extra_paths') or [] env_vars = jedi_settings.get('env_vars') diff --git a/external-deps/python-lsp-server/pyproject.toml b/external-deps/python-lsp-server/pyproject.toml index f12a6b76a61..8cce90ec1ef 100644 --- a/external-deps/python-lsp-server/pyproject.toml +++ b/external-deps/python-lsp-server/pyproject.toml @@ -16,6 +16,7 @@ dependencies = [ "jedi>=0.17.2,<0.19.0", "python-lsp-jsonrpc>=1.0.0", "pluggy>=1.0.0", + "docstring-to-markdown", "ujson>=3.0.0", "setuptools>=39.0.0", ] @@ -52,7 +53,7 @@ test = [ "pytest", "pytest-cov", "coverage", - "numpy<1.23", + "numpy", "pandas", "matplotlib", "pyqt5", diff --git a/external-deps/python-lsp-server/scripts/jsonschema2md.py b/external-deps/python-lsp-server/scripts/jsonschema2md.py index 3939a134664..3707e00d0ee 100644 --- a/external-deps/python-lsp-server/scripts/jsonschema2md.py +++ b/external-deps/python-lsp-server/scripts/jsonschema2md.py @@ -41,7 +41,7 @@ def describe_type(prop: dict) -> str: if option in EXTRA_DESCRIPTORS: parts.append(EXTRA_DESCRIPTORS[option](prop)) if "enum" in prop: - allowed_values = [f"`{value}`" for value in prop["enum"]] + allowed_values = [f"`{value!r}`" for value in prop["enum"]] parts.append("(one of: " + ", ".join(allowed_values) + ")") return " ".join(parts) diff --git a/external-deps/python-lsp-server/setup.py b/external-deps/python-lsp-server/setup.py index 04cfa069afc..b419c0f5620 100755 --- a/external-deps/python-lsp-server/setup.py +++ b/external-deps/python-lsp-server/setup.py @@ -5,6 +5,7 @@ from setuptools import setup, find_packages + if __name__ == "__main__": setup( name="python-lsp-server", # to allow GitHub dependency tracking work diff --git a/external-deps/python-lsp-server/test/plugins/test_autopep8_format.py b/external-deps/python-lsp-server/test/plugins/test_autopep8_format.py index bb5bc31bbb8..19a8cbb66f9 100644 --- a/external-deps/python-lsp-server/test/plugins/test_autopep8_format.py +++ b/external-deps/python-lsp-server/test/plugins/test_autopep8_format.py @@ -41,7 +41,7 @@ def func(): def test_format(config, workspace): doc = Document(DOC_URI, workspace, DOC) - res = pylsp_format_document(config, doc) + res = pylsp_format_document(config, doc, options=None) assert len(res) == 1 assert res[0]['newText'] == "a = 123\n\n\ndef func():\n pass\n" @@ -54,7 +54,7 @@ def test_range_format(config, workspace): 'start': {'line': 0, 'character': 0}, 'end': {'line': 2, 'character': 0} } - res = pylsp_format_range(config, doc, def_range) + res = pylsp_format_range(config, doc, def_range, options=None) assert len(res) == 1 @@ -64,12 +64,12 @@ def test_range_format(config, workspace): def test_no_change(config, workspace): doc = Document(DOC_URI, workspace, GOOD_DOC) - assert not pylsp_format_document(config, doc) + assert not pylsp_format_document(config, doc, options=None) def test_hanging_indentation(config, workspace): doc = Document(DOC_URI, workspace, INDENTED_DOC) - res = pylsp_format_document(config, doc) + res = pylsp_format_document(config, doc, options=None) assert len(res) == 1 assert res[0]['newText'] == CORRECT_INDENTED_DOC @@ -78,6 +78,6 @@ def test_hanging_indentation(config, workspace): @pytest.mark.parametrize('newline', ['\r\n', '\r']) def test_line_endings(config, workspace, newline): doc = Document(DOC_URI, workspace, f'import os;import sys{2 * newline}dict(a=1)') - res = pylsp_format_document(config, doc) + res = pylsp_format_document(config, doc, options=None) assert res[0]['newText'] == f'import os{newline}import sys{2 * newline}dict(a=1){newline}' 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 0211cc1d2b3..16e278e0578 100644 --- a/external-deps/python-lsp-server/test/plugins/test_completion.py +++ b/external-deps/python-lsp-server/test/plugins/test_completion.py @@ -160,10 +160,15 @@ def test_jedi_completion_item_resolve(config, workspace): assert 'detail' not in documented_hello_item resolved_documented_hello = pylsp_jedi_completion_item_resolve( + doc._config, completion_item=documented_hello_item, document=doc ) - assert 'Sends a polite greeting' in resolved_documented_hello['documentation'] + expected_doc = { + 'kind': 'markdown', + 'value': '```python\ndocumented_hello()\n```\n\n\nSends a polite greeting' + } + assert resolved_documented_hello['documentation'] == expected_doc def test_jedi_completion_with_fuzzy_enabled(config, workspace): @@ -498,8 +503,8 @@ def test_jedi_completion_environment(workspace): completions = pylsp_jedi_completions(doc._config, doc, com_position) assert completions[0]['label'] == 'loghub' - resolved = pylsp_jedi_completion_item_resolve(completions[0], doc) - assert 'changelog generator' in resolved['documentation'].lower() + resolved = pylsp_jedi_completion_item_resolve(doc._config, completions[0], doc) + assert 'changelog generator' in resolved['documentation']['value'].lower() def test_document_path_completions(tmpdir, workspace_other_root_path): 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 7ac6e071377..89040247489 100644 --- a/external-deps/python-lsp-server/test/plugins/test_hover.py +++ b/external-deps/python-lsp-server/test/plugins/test_hover.py @@ -38,16 +38,16 @@ def test_numpy_hover(workspace): doc = Document(DOC_URI, workspace, NUMPY_DOC) contents = '' - assert contents in pylsp_hover(doc, no_hov_position)['contents'] + assert contents in pylsp_hover(doc._config, doc, no_hov_position)['contents'] contents = 'NumPy\n=====\n\nProvides\n' - assert contents in pylsp_hover(doc, numpy_hov_position_1)['contents'][0] + assert contents in pylsp_hover(doc._config, doc, numpy_hov_position_1)['contents']['value'] contents = 'NumPy\n=====\n\nProvides\n' - assert contents in pylsp_hover(doc, numpy_hov_position_2)['contents'][0] + assert contents in pylsp_hover(doc._config, doc, numpy_hov_position_2)['contents']['value'] contents = 'NumPy\n=====\n\nProvides\n' - assert contents in pylsp_hover(doc, numpy_hov_position_3)['contents'][0] + assert contents in pylsp_hover(doc._config, doc, numpy_hov_position_3)['contents']['value'] # https://github.com/davidhalter/jedi/issues/1746 # pylint: disable=import-outside-toplevel @@ -55,8 +55,8 @@ def test_numpy_hover(workspace): if np.lib.NumpyVersion(np.__version__) < '1.20.0': contents = 'Trigonometric sine, element-wise.\n\n' - assert contents in pylsp_hover( - doc, numpy_sin_hov_position)['contents'][0] + assert contents in pylsp_hover(doc._config, + doc, numpy_sin_hov_position)['contents']['value'] def test_hover(workspace): @@ -67,13 +67,13 @@ def test_hover(workspace): doc = Document(DOC_URI, workspace, DOC) - contents = [{'language': 'python', 'value': 'main()'}, 'hello world'] + contents = {'kind': 'markdown', 'value': '```python\nmain()\n```\n\n\nhello world'} assert { 'contents': contents - } == pylsp_hover(doc, hov_position) + } == pylsp_hover(doc._config, doc, hov_position) - assert {'contents': ''} == pylsp_hover(doc, no_hov_position) + assert {'contents': ''} == pylsp_hover(doc._config, doc, no_hov_position) def test_document_path_hover(workspace_other_root_path, tmpdir): @@ -96,6 +96,6 @@ def foo(): doc = Document(doc_uri, workspace_other_root_path, doc_content) cursor_pos = {'line': 1, 'character': 3} - contents = pylsp_hover(doc, cursor_pos)['contents'] + contents = pylsp_hover(doc._config, doc, cursor_pos)['contents'] - assert contents[1] == 'A docstring for foo.' + assert 'A docstring for foo.' in contents['value'] diff --git a/external-deps/python-lsp-server/test/plugins/test_signature.py b/external-deps/python-lsp-server/test/plugins/test_signature.py index 51cecb56b6a..d9dbb8d25c2 100644 --- a/external-deps/python-lsp-server/test/plugins/test_signature.py +++ b/external-deps/python-lsp-server/test/plugins/test_signature.py @@ -46,7 +46,7 @@ def test_no_signature(workspace): sig_position = {'line': 9, 'character': 0} doc = Document(DOC_URI, workspace, DOC) - sigs = signature.pylsp_signature_help(doc, sig_position)['signatures'] + sigs = signature.pylsp_signature_help(doc._config, doc, sig_position)['signatures'] assert not sigs @@ -55,13 +55,13 @@ def test_signature(workspace): sig_position = {'line': 10, 'character': 5} doc = Document(DOC_URI, workspace, DOC) - sig_info = signature.pylsp_signature_help(doc, sig_position) + sig_info = signature.pylsp_signature_help(doc._config, doc, sig_position) sigs = sig_info['signatures'] assert len(sigs) == 1 assert sigs[0]['label'] == 'main(param1, param2)' assert sigs[0]['parameters'][0]['label'] == 'param1' - assert sigs[0]['parameters'][0]['documentation'] == 'Docs for param1' + assert sigs[0]['parameters'][0]['documentation'] == {'kind': 'markdown', 'value': 'Docs for param1'} assert sig_info['activeParameter'] == 0 @@ -71,7 +71,7 @@ def test_multi_line_signature(workspace): sig_position = {'line': 17, 'character': 5} doc = Document(DOC_URI, workspace, MULTI_LINE_DOC) - sig_info = signature.pylsp_signature_help(doc, sig_position) + sig_info = signature.pylsp_signature_help(doc._config, doc, sig_position) sigs = sig_info['signatures'] assert len(sigs) == 1 @@ -80,7 +80,7 @@ def test_multi_line_signature(workspace): 'param5=None, param6=None, param7=None, param8=None)' ) assert sigs[0]['parameters'][0]['label'] == 'param1' - assert sigs[0]['parameters'][0]['documentation'] == 'Docs for param1' + assert sigs[0]['parameters'][0]['documentation'] == {'kind': 'markdown', 'value': 'Docs for param1'} assert sig_info['activeParameter'] == 0 diff --git a/external-deps/python-lsp-server/test/plugins/test_yapf_format.py b/external-deps/python-lsp-server/test/plugins/test_yapf_format.py index 1a965a27cf2..92bd8ed5faa 100644 --- a/external-deps/python-lsp-server/test/plugins/test_yapf_format.py +++ b/external-deps/python-lsp-server/test/plugins/test_yapf_format.py @@ -29,7 +29,7 @@ def test_format(workspace): doc = Document(DOC_URI, workspace, DOC) - res = pylsp_format_document(doc) + res = pylsp_format_document(doc, None) assert apply_text_edits(doc, res) == "A = ['h', 'w', 'a']\n\nB = ['h', 'w']\n" @@ -41,7 +41,7 @@ def test_range_format(workspace): 'start': {'line': 0, 'character': 0}, 'end': {'line': 4, 'character': 10} } - res = pylsp_format_range(doc, def_range) + res = pylsp_format_range(doc, def_range, None) # Make sure B is still badly formatted assert apply_text_edits(doc, res) == "A = ['h', 'w', 'a']\n\nB = ['h',\n\n\n'w']\n" @@ -49,7 +49,7 @@ def test_range_format(workspace): def test_no_change(workspace): doc = Document(DOC_URI, workspace, GOOD_DOC) - assert not pylsp_format_document(doc) + assert not pylsp_format_document(doc, options=None) def test_config_file(tmpdir, workspace): @@ -59,7 +59,7 @@ def test_config_file(tmpdir, workspace): src = tmpdir.join('test.py') doc = Document(uris.from_fs_path(src.strpath), workspace, DOC) - res = pylsp_format_document(doc) + res = pylsp_format_document(doc, options=None) # A was split on multiple lines because of column_limit from config file assert apply_text_edits(doc, res) == "A = [\n 'h', 'w',\n 'a'\n]\n\nB = ['h', 'w']\n" @@ -68,7 +68,7 @@ def test_config_file(tmpdir, workspace): @pytest.mark.parametrize('newline', ['\r\n']) def test_line_endings(workspace, newline): doc = Document(DOC_URI, workspace, f'import os;import sys{2 * newline}dict(a=1)') - res = pylsp_format_document(doc) + res = pylsp_format_document(doc, options=None) assert apply_text_edits(doc, res) == f'import os{newline}import sys{2 * newline}dict(a=1){newline}' @@ -99,7 +99,7 @@ def test_format_returns_text_edit_per_line(workspace): log("x") log("hi")""" doc = Document(DOC_URI, workspace, single_space_indent) - res = pylsp_format_document(doc) + res = pylsp_format_document(doc, options=None) # two removes and two adds assert len(res) == 4 From d3715dd00c8c127ed4578e2a0f56106b3a029b72 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 1 Nov 2022 22:27:23 -0500 Subject: [PATCH 2/6] Completions: Request documentation in plain text to the server That's because we can't handle docs in Markdown. --- spyder/plugins/completion/api.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/spyder/plugins/completion/api.py b/spyder/plugins/completion/api.py index ac218a644b5..1fc0f32ef09 100644 --- a/spyder/plugins/completion/api.py +++ b/spyder/plugins/completion/api.py @@ -229,13 +229,16 @@ class FailureHandlingKind: # Code completion can be turned on/off dynamically. "dynamicRegistration": True, - # Client (Spyder) supports snippets as insert text. - # A snippet can define tab stops and placeholders with `$1`, `$2` - # and `${3:foo}`. `$0` defines the final tab stop, it defaults to - # the end of the snippet. Placeholders with equal identifiers are - # linked, that is typing in one will update others too. "completionItem": { - "snippetSupport": True + # Client (Spyder) supports snippets as insert text. + # A snippet can define tab stops and placeholders with `$1`, `$2` + # and `${3:foo}`. `$0` defines the final tab stop, it defaults to + # the end of the snippet. Placeholders with equal identifiers are + # linked, that is typing in one will update others too. + "snippetSupport": True, + + # Completion item docs can only be handled in plain text + "documentationFormat": ['plaintext'], } }, @@ -243,7 +246,10 @@ class FailureHandlingKind: # hover information at a given text document position. "hover": { # Hover introspection can be turned on/off dynamically. - "dynamicRegistration": True + "dynamicRegistration": True, + + # Hover contents can only be handled in plain text by Spyder + "contentFormat": ['plaintext'], }, # The signature help request is sent from the client to the server to @@ -251,7 +257,12 @@ class FailureHandlingKind: "signatureHelp": { # Function/Class/Method signature hinting can be turned on/off # dynamically. - "dynamicRegistration": True + "dynamicRegistration": True, + + # Signature docs can only be handled in plain text by Spyder + "signatureInformation": { + "documentationFormat": ['plaintext'], + } }, # Editor allows to find references. From 7acd5553199016aba2895cd6cf4b3067f87d880e Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 1 Nov 2022 22:28:05 -0500 Subject: [PATCH 3/6] Editor: Fix error when showing completion hints with the latest PyLSP --- spyder/plugins/editor/widgets/completion.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spyder/plugins/editor/widgets/completion.py b/spyder/plugins/editor/widgets/completion.py index 7b4a84e8ebf..af643fcaebe 100644 --- a/spyder/plugins/editor/widgets/completion.py +++ b/spyder/plugins/editor/widgets/completion.py @@ -543,6 +543,10 @@ def trigger_completion_hint(self, row=None): def augment_completion_info(self, item): if self.current_selected_item_label == item['label']: insert_text = self._get_insert_text(item) + + if isinstance(item['documentation'], dict): + item['documentation'] = item['documentation']['value'] + self.sig_completion_hint.emit( insert_text, item['documentation'], From a5836682f3fbaeaafbb96f719e7088cc428481ed Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 1 Nov 2022 23:37:16 -0500 Subject: [PATCH 4/6] Outline: Update tree if it's empty but it has cached items from the LSP --- spyder/plugins/outlineexplorer/widgets.py | 30 +++++++++++++---------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/spyder/plugins/outlineexplorer/widgets.py b/spyder/plugins/outlineexplorer/widgets.py index 6fb2cbf3c95..d0d2be1c866 100644 --- a/spyder/plugins/outlineexplorer/widgets.py +++ b/spyder/plugins/outlineexplorer/widgets.py @@ -587,6 +587,7 @@ def update_tree(self, items, editor): editor_id = editor.get_id() language = editor.get_language() current_tree = self.editor_tree_cache[editor_id] + root = self.editor_items[editor_id] tree_info = [] # Create tree with items that come from the LSP @@ -613,16 +614,22 @@ def update_tree(self, items, editor): tree = IntervalTree.from_tuples(tree_info) - # Compare with current tree to check if it's necessary to update it. - changes = tree - current_tree - if tree and len(changes) == 0: - logger.debug( - f"Current and new trees for file {editor.fname} are the same, " - f"so no update is necessary" - ) - editor.is_tree_updated = True - self.sig_hide_spinner.emit() - return False + # We must update the tree if the editor's root doesn't have children + # yet but we have symbols for it saved in the cache + must_update = root.node.childCount() == 0 and len(current_tree) > 0 + + if not must_update: + # Compare with current tree to check if it's necessary to update + # it. + changes = tree - current_tree + if tree and len(changes) == 0: + logger.debug( + f"Current and new trees for file {editor.fname} are the " + f"same, so no update is necessary" + ) + editor.is_tree_updated = True + self.sig_hide_spinner.emit() + return False logger.debug(f"Updating tree for file {editor.fname}") @@ -630,9 +637,6 @@ def update_tree(self, items, editor): for entry in sorted(tree): entry.data.create_node() - # Get root before deleting items - root = self.editor_items[editor_id] - # Remove previous tree to create the new one. # NOTE: This is twice as fast as detecting the symbols that changed # and updating only those in current_tree. From 784f668b267678b38a469d498a2a82ebe7ef1b88 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 1 Nov 2022 23:54:15 -0500 Subject: [PATCH 5/6] Testing: Manually install docstring-to-markdown Also add it to the requirements of our Mac app --- .github/scripts/install.sh | 5 +++++ installers/macOS/req-extras.txt | 1 + 2 files changed, 6 insertions(+) diff --git a/.github/scripts/install.sh b/.github/scripts/install.sh index 294fed81e8c..e7cd7301d0f 100755 --- a/.github/scripts/install.sh +++ b/.github/scripts/install.sh @@ -38,6 +38,9 @@ if [ "$USE_CONDA" = "true" ]; then else mamba install 'numpy<1.23' fi + + # Install docstring-to-markdown until we release PyLSP 1.6.0 + mamba install docstring-to-markdown else # Update pip and setuptools python -m pip install -U pip setuptools wheel build @@ -63,6 +66,8 @@ else pip install pyqt5==5.12.* pyqtwebengine==5.12.* fi + # Install docstring-to-markdown until we release PyLSP 1.6.0 + pip install docstring-to-markdown fi # Install subrepos from source diff --git a/installers/macOS/req-extras.txt b/installers/macOS/req-extras.txt index b197acc34eb..f3142db23d2 100644 --- a/installers/macOS/req-extras.txt +++ b/installers/macOS/req-extras.txt @@ -1,5 +1,6 @@ # Spyder extra packages autopep8 +docstring-to-markdown flake8 Paramiko pycodestyle From 90e4f8d5ffe5bd037038a383aaa22f0c693e4116 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 2 Nov 2022 01:42:40 -0500 Subject: [PATCH 6/6] Editor: Fix not showing hovers for objects without docstrings --- .../plugins/editor/widgets/tests/test_hints_and_calltips.py | 1 - spyder/widgets/mixins.py | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/spyder/plugins/editor/widgets/tests/test_hints_and_calltips.py b/spyder/plugins/editor/widgets/tests/test_hints_and_calltips.py index b233d9e7488..beb3a372d6c 100644 --- a/spyder/plugins/editor/widgets/tests/test_hints_and_calltips.py +++ b/spyder/plugins/editor/widgets/tests/test_hints_and_calltips.py @@ -10,7 +10,6 @@ import sys # Third party imports -from qtpy import PYQT_VERSION from qtpy.QtCore import Qt, QPoint from qtpy.QtGui import QTextCursor import pytest diff --git a/spyder/widgets/mixins.py b/spyder/widgets/mixins.py index 783c71638d4..ee968ec2295 100644 --- a/spyder/widgets/mixins.py +++ b/spyder/widgets/mixins.py @@ -465,7 +465,7 @@ def _check_signature_and_format(self, signature_or_text, parameter=None, language = getattr(self, 'language', language).lower() signature_or_text = signature_or_text.replace('\\*', '*') - # Remove special symbols that could itefere with ''.format + # Remove special symbols that could interfere with ''.format signature_or_text = signature_or_text.replace('{', '{') signature_or_text = signature_or_text.replace('}', '}') @@ -521,6 +521,8 @@ def _check_signature_and_format(self, signature_or_text, parameter=None, else: signature = '\n'.join(lines[:i]) extra_text = '\n'.join(lines[i:]) + if extra_text == '\n': + extra_text = None if signature: new_signature = self._format_signature(