Skip to content

Allow to format signatures in docstrings #631

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions pylsp/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import re
import threading
import time
from typing import List, Optional
from typing import Callable, List, Optional

import docstring_to_markdown
import jedi
Expand Down Expand Up @@ -210,7 +210,10 @@ def choose_markup_kind(client_supported_markup_kinds: List[str]):


def format_docstring(
contents: str, markup_kind: str, signatures: Optional[List[str]] = None
contents: str,
markup_kind: str,
signatures: Optional[List[str]] = None,
signatures_to_markdown: Optional[Callable[[List[str]], str]] = None,
):
"""Transform the provided docstring into a MarkupContent object.

Expand All @@ -232,7 +235,12 @@ def format_docstring(
value = escape_markdown(contents)

if signatures:
value = wrap_signature("\n".join(signatures)) + "\n\n" + value
if signatures_to_markdown is None:
wrapped_signatures = wrap_signature("\n".join(signatures))
else:
wrapped_signatures = signatures_to_markdown(signatures)

value = wrapped_signatures + "\n\n" + value

return {"kind": "markdown", "value": value}
value = contents
Expand Down
15 changes: 12 additions & 3 deletions pylsp/hookspecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,16 @@ def pylsp_commands(config, workspace) -> None:


@hookspec
def pylsp_completions(config, workspace, document, position, ignored_names) -> None:
def pylsp_completions(
config, workspace, document, position, ignored_names, signatures_to_markdown
) -> None:
pass


@hookspec(firstresult=True)
def pylsp_completion_item_resolve(config, workspace, document, completion_item) -> None:
def pylsp_completion_item_resolve(
config, workspace, document, completion_item, signatures_to_markdown
) -> None:
pass


Expand Down Expand Up @@ -89,7 +93,12 @@ def pylsp_format_range(config, workspace, document, range, options) -> None:


@hookspec(firstresult=True)
def pylsp_hover(config, workspace, document, position) -> None:
def pylsp_hover(config, workspace, document, position, signatures_to_markdown) -> None:
pass


@hookspec(firstresult=True)
def pylsp_signatures_to_markdown(signatures) -> None:
pass


Expand Down
3 changes: 2 additions & 1 deletion pylsp/plugins/hover.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@


@hookimpl
def pylsp_hover(config, document, position):
def pylsp_hover(config, document, position, signatures_to_markdown=None):
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)
Expand Down Expand Up @@ -46,5 +46,6 @@ def pylsp_hover(config, document, position):
definition.docstring(raw=True),
preferred_markup_kind,
signatures=[signature] if signature else None,
signatures_to_markdown=signatures_to_markdown,
)
}
30 changes: 25 additions & 5 deletions pylsp/plugins/jedi_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import logging
import os
from typing import Callable, List, Optional

import parso

Expand Down Expand Up @@ -36,7 +37,7 @@


@hookimpl
def pylsp_completions(config, document, position):
def pylsp_completions(config, document, position, signatures_to_markdown=None):
"""Get formatted completions for current code position"""
settings = config.plugin_settings("jedi_completion", document_path=document.path)
resolve_eagerly = settings.get("eager", False)
Expand Down Expand Up @@ -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,
signatures_to_markdown=signatures_to_markdown,
)
for i, c in enumerate(completions)
]
Expand All @@ -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,
signatures_to_markdown=signatures_to_markdown,
)
completion_dict["kind"] = lsp.CompletionItemKind.TypeParameter
completion_dict["label"] += " object"
Expand All @@ -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,
signatures_to_markdown=signatures_to_markdown,
)
completion_dict["kind"] = lsp.CompletionItemKind.TypeParameter
completion_dict["label"] += " object"
Expand All @@ -137,7 +141,9 @@ 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, signatures_to_markdown=None
):
"""Resolve formatted completion for given non-resolved completion"""
shared_data = document.shared_data["LAST_JEDI_COMPLETIONS"].get(
completion_item["label"]
Expand All @@ -152,7 +158,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,
signatures_to_markdown=signatures_to_markdown,
)
return completion_item


Expand Down Expand Up @@ -207,13 +218,19 @@ 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,
signatures_to_markdown: Optional[Callable[[List[str]], str]] = None,
):
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,
signatures_to_markdown=signatures_to_markdown,
)
except Exception:
docs = ""
Expand All @@ -228,6 +245,7 @@ def _format_completion(
resolve=False,
resolve_label_or_snippet=False,
snippet_support=False,
signatures_to_markdown=None,
):
completion = {
"label": _label(d, resolve_label_or_snippet),
Expand All @@ -237,7 +255,9 @@ def _format_completion(
}

if resolve:
completion = _resolve_completion(completion, d, markup_kind)
completion = _resolve_completion(
completion, d, markup_kind, signatures_to_markdown
)

# Adjustments for file completions
if d.type == "path":
Expand Down
26 changes: 23 additions & 3 deletions pylsp/python_lsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,14 +399,21 @@ def completions(self, doc_uri, position):
notebook_document = workspace.get_maybe_document(document.notebook_uri)
ignored_names = notebook_document.jedi_names(doc_uri)
completions = self._hook(
"pylsp_completions", doc_uri, position=position, ignored_names=ignored_names
"pylsp_completions",
doc_uri,
position=position,
ignored_names=ignored_names,
signatures_to_markdown=self._signatures_to_markdown,
)
return {"isIncomplete": False, "items": flatten(completions)}

def completion_item_resolve(self, completion_item):
doc_uri = completion_item.get("data", {}).get("doc_uri", None)
return self._hook(
"pylsp_completion_item_resolve", doc_uri, completion_item=completion_item
"pylsp_completion_item_resolve",
doc_uri,
completion_item=completion_item,
signatures_to_markdown=self._signatures_to_markdown,
)

def definitions(self, doc_uri, position):
Expand Down Expand Up @@ -434,7 +441,12 @@ def highlight(self, doc_uri, position):
)

def hover(self, doc_uri, position):
return self._hook("pylsp_hover", doc_uri, position=position) or {"contents": ""}
return self._hook(
"pylsp_hover",
doc_uri,
position=position,
signatures_to_markdown=self._signatures_to_markdown,
) or {"contents": ""}

@_utils.debounce(LINT_DEBOUNCE_S, keyed_by="doc_uri")
def lint(self, doc_uri, is_saved) -> None:
Expand Down Expand Up @@ -888,6 +900,14 @@ def m_workspace__did_change_watched_files(self, changes=None, **_kwargs):
def m_workspace__execute_command(self, command=None, arguments=None):
return self.execute_command(command, arguments)

@property
def _signatures_to_markdown(self):
if not hasattr(self, "_signatures_to_markdown_hook"):
self._signatures_to_markdown_hook = self._hook(
"pylsp_signatures_to_markdown"
)
return self._signatures_to_markdown_hook


def flatten(list_of_lists):
return [item for lst in list_of_lists for item in lst]
Expand Down
27 changes: 25 additions & 2 deletions test/plugins/test_hover.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
DOC_URI = uris.from_fs_path(__file__)
DOC = """

def main():
def main(a: float, b: float):
\"\"\"hello world\"\"\"
pass
"""
Expand Down Expand Up @@ -79,13 +79,36 @@ 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_custom_signature(workspace) -> None:
# Over 'main' in def main():
hov_position = {"line": 2, "character": 6}

doc = Document(DOC_URI, workspace, DOC)

contents = {
"kind": "markdown",
"value": "```python\nmain(\n a: float,\n b: float\n)\n```\n\n\nhello world",
}

def signatures_to_markdown(signatures: list):
# dummy implementation for tests
return "```python\nmain(\n a: float,\n b: float\n)\n```\n"

assert {"contents": contents} == pylsp_hover(
doc._config, doc, hov_position, signatures_to_markdown=signatures_to_markdown
)


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.
Expand Down