Skip to content

Commit d5c1e8e

Browse files
committed
Remove the hook, implement formatters natively
1 parent 8758d56 commit d5c1e8e

File tree

8 files changed

+130
-74
lines changed

8 files changed

+130
-74
lines changed

pylsp/_utils.py

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def run():
5757

5858

5959
def throttle(seconds=1):
60-
"""Throttles calls to a function evey `seconds` seconds."""
60+
"""Throttles calls to a function every `seconds` seconds."""
6161

6262
def decorator(func):
6363
@functools.wraps(func)
@@ -209,15 +209,93 @@ def choose_markup_kind(client_supported_markup_kinds: List[str]):
209209
return "markdown"
210210

211211

212-
def convert_signatures_to_markdown(signatures: List[str]) -> str:
212+
class Formatter:
213+
command: List[str]
214+
215+
@property
216+
def is_installed(self) -> bool:
217+
"""Returns whether formatter is available"""
218+
if not hasattr(self, "_is_installed"):
219+
self._is_installed = self._is_available_via_cli()
220+
return self._is_installed
221+
222+
def format(self, code: str, line_length: int) -> str:
223+
"""Formats code"""
224+
return subprocess.check_output(
225+
[
226+
sys.executable,
227+
"-m",
228+
*self.command,
229+
"--line-length",
230+
str(line_length),
231+
"-",
232+
],
233+
input=code,
234+
text=True,
235+
).strip()
236+
237+
def _is_available_via_cli(self) -> bool:
238+
try:
239+
subprocess.check_output(
240+
[
241+
sys.executable,
242+
"-m",
243+
*self.command,
244+
"--help",
245+
],
246+
)
247+
return True
248+
except CalledProcessError:
249+
return False
250+
251+
252+
class RuffFormatter:
253+
command = ["ruff", "format"]
254+
255+
256+
class BlackFormatter:
257+
command = ["black"]
258+
259+
260+
formatters = {"ruff": RuffFormatter(), "black": BlackFormatter()}
261+
262+
263+
def format_signature(signature: str, signature_formatter: str) -> str:
264+
"""Formats signature using ruff or black if either is available."""
265+
as_func = f"def {signature.strip()}:\n pass"
266+
line_length = config.get("line_length", 88)
267+
formatter = formatters[signature_formatter]
268+
if formatter.is_installed:
269+
try:
270+
return (
271+
formatter.format(as_func, line_length=line_length)
272+
.removeprefix("def ")
273+
.removesuffix(":\n pass")
274+
)
275+
except subprocess.CalledProcessError as e:
276+
log.warning("Signature formatter failed %s", e)
277+
else:
278+
log.warning(
279+
"Formatter %s was requested but it does not appear to be installed",
280+
signature_formatter,
281+
)
282+
return signature
283+
284+
285+
def convert_signatures_to_markdown(signatures: List[str], config: dict) -> str:
286+
signature_formatter = config.get("format", "black")
287+
if signature_formatter:
288+
signatures = [
289+
format_signature(signature, config=config) for signature in signatures
290+
]
213291
return wrap_signature("\n".join(signatures))
214292

215293

216294
def format_docstring(
217295
contents: str,
218296
markup_kind: str,
219297
signatures: Optional[List[str]] = None,
220-
signatures_to_markdown: Optional[Callable[[List[str]], str]] = None,
298+
signature_config: Optional[dict] = None,
221299
):
222300
"""Transform the provided docstring into a MarkupContent object.
223301
@@ -239,10 +317,8 @@ def format_docstring(
239317
value = escape_markdown(contents)
240318

241319
if signatures:
242-
wrapped_signatures = (
243-
signatures_to_markdown(signatures)
244-
if signatures_to_markdown
245-
else convert_signatures_to_markdown(signatures)
320+
wrapped_signatures = convert_signatures_to_markdown(
321+
signatures, config=signature_config or {}
246322
)
247323
value = wrapped_signatures + "\n\n" + value
248324

pylsp/config/schema.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,26 @@
511511
},
512512
"uniqueItems": true,
513513
"description": "The name of the folder in which rope stores project configurations and data. Pass `null` for not using such a folder at all."
514-
}
514+
},
515+
"pylsp.signature.formatter": {
516+
"type": [
517+
"string",
518+
"null"
519+
],
520+
"enum": [
521+
"black",
522+
"ruff",
523+
null
524+
]
525+
"default": "black",
526+
"description": "Formatter to use for reformatting signatures in docstrings."
527+
},
528+
"pylsp.signature.line_length": {
529+
"type": [
530+
"number",
531+
],
532+
"default": 88,
533+
"description": "Maximum line length in signatures."
534+
},
515535
}
516536
}

pylsp/hookspecs.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,12 @@ def pylsp_commands(config, workspace) -> None:
2424

2525

2626
@hookspec
27-
def pylsp_completions(
28-
config, workspace, document, position, ignored_names, signatures_to_markdown
29-
) -> None:
27+
def pylsp_completions(config, workspace, document, position, ignored_names) -> None:
3028
pass
3129

3230

3331
@hookspec(firstresult=True)
34-
def pylsp_completion_item_resolve(
35-
config, workspace, document, completion_item, signatures_to_markdown
36-
) -> None:
32+
def pylsp_completion_item_resolve(config, workspace, document, completion_item) -> None:
3733
pass
3834

3935

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

9490

9591
@hookspec(firstresult=True)
96-
def pylsp_hover(config, workspace, document, position, signatures_to_markdown) -> None:
97-
pass
98-
99-
100-
@hookspec(firstresult=True)
101-
def pylsp_signatures_to_markdown(signatures) -> None:
92+
def pylsp_hover(config, workspace, document, position) -> None:
10293
pass
10394

10495

pylsp/plugins/hover.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99

1010

1111
@hookimpl
12-
def pylsp_hover(config, document, position, signatures_to_markdown):
12+
def pylsp_hover(config, document, position):
13+
signature_config = config.settings.get("signatures", {})
1314
code_position = _utils.position_to_jedi_linecolumn(document, position)
1415
definitions = document.jedi_script(use_document_path=True).infer(**code_position)
1516
word = document.word_at_position(position)
@@ -46,6 +47,6 @@ def pylsp_hover(config, document, position, signatures_to_markdown):
4647
definition.docstring(raw=True),
4748
preferred_markup_kind,
4849
signatures=[signature] if signature else None,
49-
signatures_to_markdown=signatures_to_markdown,
50+
signature_config=signature_config,
5051
)
5152
}

pylsp/plugins/jedi_completion.py

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,13 @@
3737

3838

3939
@hookimpl
40-
def pylsp_completions(config, document, position, signatures_to_markdown):
40+
def pylsp_completions(config, document, position):
4141
"""Get formatted completions for current code position"""
4242
settings = config.plugin_settings("jedi_completion", document_path=document.path)
4343
resolve_eagerly = settings.get("eager", False)
44-
code_position = _utils.position_to_jedi_linecolumn(document, position)
44+
signature_config = config.settings.get("signatures", {})
4545

46+
code_position = _utils.position_to_jedi_linecolumn(document, position)
4647
code_position["fuzzy"] = settings.get("fuzzy", False)
4748
completions = document.jedi_script(use_document_path=True).complete(**code_position)
4849

@@ -89,7 +90,7 @@ def pylsp_completions(config, document, position, signatures_to_markdown):
8990
resolve=resolve_eagerly,
9091
resolve_label_or_snippet=(i < max_to_resolve),
9192
snippet_support=snippet_support,
92-
signatures_to_markdown=signatures_to_markdown,
93+
signature_config=signature_config,
9394
)
9495
for i, c in enumerate(completions)
9596
]
@@ -105,7 +106,7 @@ def pylsp_completions(config, document, position, signatures_to_markdown):
105106
resolve=resolve_eagerly,
106107
resolve_label_or_snippet=(i < max_to_resolve),
107108
snippet_support=snippet_support,
108-
signatures_to_markdown=signatures_to_markdown,
109+
signature_config=signature_config,
109110
)
110111
completion_dict["kind"] = lsp.CompletionItemKind.TypeParameter
111112
completion_dict["label"] += " object"
@@ -121,7 +122,7 @@ def pylsp_completions(config, document, position, signatures_to_markdown):
121122
resolve=resolve_eagerly,
122123
resolve_label_or_snippet=(i < max_to_resolve),
123124
snippet_support=snippet_support,
124-
signatures_to_markdown=signatures_to_markdown,
125+
signature_config=signature_config,
125126
)
126127
completion_dict["kind"] = lsp.CompletionItemKind.TypeParameter
127128
completion_dict["label"] += " object"
@@ -142,7 +143,9 @@ def pylsp_completions(config, document, position, signatures_to_markdown):
142143

143144
@hookimpl
144145
def pylsp_completion_item_resolve(
145-
config, completion_item, document, signatures_to_markdown
146+
config,
147+
completion_item,
148+
document,
146149
):
147150
"""Resolve formatted completion for given non-resolved completion"""
148151
shared_data = document.shared_data["LAST_JEDI_COMPLETIONS"].get(
@@ -162,7 +165,7 @@ def pylsp_completion_item_resolve(
162165
completion,
163166
data,
164167
markup_kind=preferred_markup_kind,
165-
signatures_to_markdown=signatures_to_markdown,
168+
signature_config=config.settings.get("signatures", {}),
166169
)
167170
return completion_item
168171

@@ -218,19 +221,14 @@ def use_snippets(document, position):
218221
return expr_type not in _IMPORTS and not (expr_type in _ERRORS and "import" in code)
219222

220223

221-
def _resolve_completion(
222-
completion,
223-
d,
224-
markup_kind: str,
225-
signatures_to_markdown: Optional[Callable[[List[str]], str]] = None,
226-
):
224+
def _resolve_completion(completion, d, markup_kind: str, signature_config: dict):
227225
completion["detail"] = _detail(d)
228226
try:
229227
docs = _utils.format_docstring(
230228
d.docstring(raw=True),
231229
signatures=[signature.to_string() for signature in d.get_signatures()],
232230
markup_kind=markup_kind,
233-
signatures_to_markdown=signatures_to_markdown,
231+
signature_config=signature_config,
234232
)
235233
except Exception:
236234
docs = ""
@@ -245,7 +243,7 @@ def _format_completion(
245243
resolve=False,
246244
resolve_label_or_snippet=False,
247245
snippet_support=False,
248-
signatures_to_markdown=None,
246+
signature_config=None,
249247
):
250248
completion = {
251249
"label": _label(d, resolve_label_or_snippet),
@@ -256,7 +254,7 @@ def _format_completion(
256254

257255
if resolve:
258256
completion = _resolve_completion(
259-
completion, d, markup_kind, signatures_to_markdown
257+
completion, d, markup_kind, signature_config=signature_config
260258
)
261259

262260
# Adjustments for file completions

pylsp/python_lsp.py

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -399,21 +399,14 @@ def completions(self, doc_uri, position):
399399
notebook_document = workspace.get_maybe_document(document.notebook_uri)
400400
ignored_names = notebook_document.jedi_names(doc_uri)
401401
completions = self._hook(
402-
"pylsp_completions",
403-
doc_uri,
404-
position=position,
405-
ignored_names=ignored_names,
406-
signatures_to_markdown=self._signatures_to_markdown,
402+
"pylsp_completions", doc_uri, position=position, ignored_names=ignored_names
407403
)
408404
return {"isIncomplete": False, "items": flatten(completions)}
409405

410406
def completion_item_resolve(self, completion_item):
411407
doc_uri = completion_item.get("data", {}).get("doc_uri", None)
412408
return self._hook(
413-
"pylsp_completion_item_resolve",
414-
doc_uri,
415-
completion_item=completion_item,
416-
signatures_to_markdown=self._signatures_to_markdown,
409+
"pylsp_completion_item_resolve", doc_uri, completion_item=completion_item
417410
)
418411

419412
def definitions(self, doc_uri, position):
@@ -441,12 +434,7 @@ def highlight(self, doc_uri, position):
441434
)
442435

443436
def hover(self, doc_uri, position):
444-
return self._hook(
445-
"pylsp_hover",
446-
doc_uri,
447-
position=position,
448-
signatures_to_markdown=self._signatures_to_markdown,
449-
) or {"contents": ""}
437+
return self._hook("pylsp_hover", doc_uri, position=position) or {"contents": ""}
450438

451439
@_utils.debounce(LINT_DEBOUNCE_S, keyed_by="doc_uri")
452440
def lint(self, doc_uri, is_saved) -> None:
@@ -900,11 +888,6 @@ def m_workspace__did_change_watched_files(self, changes=None, **_kwargs):
900888
def m_workspace__execute_command(self, command=None, arguments=None):
901889
return self.execute_command(command, arguments)
902890

903-
def _signatures_to_markdown(self, signatures):
904-
return self._hook(
905-
"pylsp_signatures_to_markdown", signatures=signatures
906-
) or _utils.convert_signatures_to_markdown(signatures=signatures)
907-
908891

909892
def flatten(list_of_lists):
910893
return [item for lst in list_of_lists for item in lst]

test/plugins/test_completion.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
from pylsp.plugins.rope_completion import pylsp_completions as pylsp_rope_completions
2020
from pylsp.workspace import Document
2121

22-
pylsp_jedi_completions = partial(pylsp_jedi_completions, signatures_to_markdown=None)
23-
2422
PY2 = sys.version[0] == "2"
2523
LINUX = sys.platform.startswith("linux")
2624
CI = os.environ.get("CI")
@@ -165,10 +163,7 @@ def test_jedi_completion_item_resolve(config, workspace) -> None:
165163
assert "detail" not in documented_hello_item
166164

167165
resolved_documented_hello = pylsp_jedi_completion_item_resolve(
168-
doc._config,
169-
completion_item=documented_hello_item,
170-
document=doc,
171-
signatures_to_markdown=None,
166+
doc._config, completion_item=documented_hello_item, document=doc
172167
)
173168
expected_doc = {
174169
"kind": "markdown",
@@ -541,9 +536,7 @@ def test_jedi_completion_environment(workspace) -> None:
541536
completions = pylsp_jedi_completions(doc._config, doc, com_position)
542537
assert completions[0]["label"] == "loghub"
543538

544-
resolved = pylsp_jedi_completion_item_resolve(
545-
doc._config, completions[0], doc, signatures_to_markdown=None
546-
)
539+
resolved = pylsp_jedi_completion_item_resolve(doc._config, completions[0], doc)
547540
assert "changelog generator" in resolved["documentation"]["value"].lower()
548541

549542

test/plugins/test_hover.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,7 @@ def test_hover_custom_signature(workspace) -> None:
103103
"value": "```python\nmain(\n a: float,\n b: float\n)\n```\n\n\nhello world",
104104
}
105105

106-
def signatures_to_markdown(signatures: list):
107-
# dummy implementation for tests
108-
return "```python\nmain(\n a: float,\n b: float\n)\n```\n"
109-
110-
assert {"contents": contents} == pylsp_hover(
111-
doc._config, doc, hov_position, signatures_to_markdown=signatures_to_markdown
112-
)
106+
assert {"contents": contents} == pylsp_hover(doc._config, doc, hov_position)
113107

114108

115109
def test_document_path_hover(workspace_other_root_path, tmpdir) -> None:

0 commit comments

Comments
 (0)