pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/python-lsp/python-lsp-server/commit/c8e4e992a7d0aee00804433a2211a7b3356f5b6d

ink crossorigen="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/global-94620c216484da1f.css" /> Allow to format signatures in docstrings (#631) · python-lsp/python-lsp-server@c8e4e99 · GitHub
Skip to content

Commit c8e4e99

Browse files
authored
Allow to format signatures in docstrings (#631)
1 parent e9776fd commit c8e4e99

File tree

7 files changed

+177
-10
lines changed

7 files changed

+177
-10
lines changed

CONFIGURATION.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,7 @@ This server can be configured using the `workspace/didChangeConfiguration` metho
7575
| `pylsp.plugins.yapf.enabled` | `boolean` | Enable or disable the plugin. | `true` |
7676
| `pylsp.rope.extensionModules` | `string` | Builtin and c-extension modules that are allowed to be imported and inspected by rope. | `null` |
7777
| `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` |
78+
| `pylsp.signature.formatter` | `string` (one of: `'black'`, `'ruff'`, `None`) | Formatter to use for reformatting signatures in docstrings. | `"black"` |
79+
| `pylsp.signature.line_length` | `number` | Maximum line length in signatures. | `88` |
7880

7981
This documentation was generated from `pylsp/config/schema.json`. Please do not edit this file directly.

pylsp/_utils.py

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import os
88
import pathlib
99
import re
10+
import subprocess
11+
import sys
1012
import threading
1113
import time
1214
from typing import Optional
@@ -57,7 +59,7 @@ def run():
5759

5860

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

6264
def decorator(func):
6365
@functools.wraps(func)
@@ -209,8 +211,96 @@ def choose_markup_kind(client_supported_markup_kinds: list[str]):
209211
return "markdown"
210212

211213

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

234324
if signatures:
235-
value = wrap_signature("\n".join(signatures)) + "\n\n" + value
325+
wrapped_signatures = convert_signatures_to_markdown(
326+
signatures, config=signature_config or {}
327+
)
328+
value = wrapped_signatures + "\n\n" + value
236329

237330
return {"kind": "markdown", "value": value}
238331
value = contents

pylsp/config/schema.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,24 @@
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+
},
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": "number",
530+
"default": 88,
531+
"description": "Maximum line length in signatures."
514532
}
515533
}
516534
}

pylsp/plugins/hover.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
@hookimpl
1212
def pylsp_hover(config, document, position):
13+
signature_config = config.settings().get("signature", {})
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,5 +47,6 @@ def pylsp_hover(config, document, position):
4647
definition.docstring(raw=True),
4748
preferred_markup_kind,
4849
signatures=[signature] if signature else None,
50+
signature_config=signature_config,
4951
)
5052
}

pylsp/plugins/jedi_completion.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ def pylsp_completions(config, document, position):
4040
"""Get formatted completions for current code position"""
4141
settings = config.plugin_settings("jedi_completion", document_path=document.path)
4242
resolve_eagerly = settings.get("eager", False)
43-
code_position = _utils.position_to_jedi_linecolumn(document, position)
43+
signature_config = config.settings().get("signature", {})
4444

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

@@ -88,6 +89,7 @@ def pylsp_completions(config, document, position):
8889
resolve=resolve_eagerly,
8990
resolve_label_or_snippet=(i < max_to_resolve),
9091
snippet_support=snippet_support,
92+
signature_config=signature_config,
9193
)
9294
for i, c in enumerate(completions)
9395
]
@@ -103,6 +105,7 @@ def pylsp_completions(config, document, position):
103105
resolve=resolve_eagerly,
104106
resolve_label_or_snippet=(i < max_to_resolve),
105107
snippet_support=snippet_support,
108+
signature_config=signature_config,
106109
)
107110
completion_dict["kind"] = lsp.CompletionItemKind.TypeParameter
108111
completion_dict["label"] += " object"
@@ -118,6 +121,7 @@ def pylsp_completions(config, document, position):
118121
resolve=resolve_eagerly,
119122
resolve_label_or_snippet=(i < max_to_resolve),
120123
snippet_support=snippet_support,
124+
signature_config=signature_config,
121125
)
122126
completion_dict["kind"] = lsp.CompletionItemKind.TypeParameter
123127
completion_dict["label"] += " object"
@@ -137,7 +141,11 @@ def pylsp_completions(config, document, position):
137141

138142

139143
@hookimpl
140-
def pylsp_completion_item_resolve(config, completion_item, document):
144+
def pylsp_completion_item_resolve(
145+
config,
146+
completion_item,
147+
document,
148+
):
141149
"""Resolve formatted completion for given non-resolved completion"""
142150
shared_data = document.shared_data["LAST_JEDI_COMPLETIONS"].get(
143151
completion_item["label"]
@@ -152,7 +160,12 @@ def pylsp_completion_item_resolve(config, completion_item, document):
152160

153161
if shared_data:
154162
completion, data = shared_data
155-
return _resolve_completion(completion, data, markup_kind=preferred_markup_kind)
163+
return _resolve_completion(
164+
completion,
165+
data,
166+
markup_kind=preferred_markup_kind,
167+
signature_config=config.settings().get("signature", {}),
168+
)
156169
return completion_item
157170

158171

@@ -207,13 +220,14 @@ def use_snippets(document, position):
207220
return expr_type not in _IMPORTS and not (expr_type in _ERRORS and "import" in code)
208221

209222

210-
def _resolve_completion(completion, d, markup_kind: str):
223+
def _resolve_completion(completion, d, markup_kind: str, signature_config: dict):
211224
completion["detail"] = _detail(d)
212225
try:
213226
docs = _utils.format_docstring(
214227
d.docstring(raw=True),
215228
signatures=[signature.to_string() for signature in d.get_signatures()],
216229
markup_kind=markup_kind,
230+
signature_config=signature_config,
217231
)
218232
except Exception:
219233
docs = ""
@@ -228,6 +242,7 @@ def _format_completion(
228242
resolve=False,
229243
resolve_label_or_snippet=False,
230244
snippet_support=False,
245+
signature_config=None,
231246
):
232247
completion = {
233248
"label": _label(d, resolve_label_or_snippet),
@@ -237,7 +252,9 @@ def _format_completion(
237252
}
238253

239254
if resolve:
240-
completion = _resolve_completion(completion, d, markup_kind)
255+
completion = _resolve_completion(
256+
completion, d, markup_kind, signature_config=signature_config
257+
)
241258

242259
# Adjustments for file completions
243260
if d.type == "path":

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ dependencies = [
1919
"pluggy>=1.0.0",
2020
"python-lsp-jsonrpc>=1.1.0,<2.0.0",
2121
"ujson>=3.0.0",
22+
"black"
2223
]
2324
dynamic = ["version"]
2425

test/plugins/test_hover.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
DOC_URI = uris.from_fs_path(__file__)
1111
DOC = """
1212
13-
def main():
13+
def main(a: float, b: float):
1414
\"\"\"hello world\"\"\"
1515
pass
1616
"""
@@ -79,13 +79,47 @@ def test_hover(workspace) -> None:
7979

8080
doc = Document(DOC_URI, workspace, DOC)
8181

82-
contents = {"kind": "markdown", "value": "```python\nmain()\n```\n\n\nhello world"}
82+
contents = {
83+
"kind": "markdown",
84+
"value": "```python\nmain(a: float, b: float)\n```\n\n\nhello world",
85+
}
8386

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

8689
assert {"contents": ""} == pylsp_hover(doc._config, doc, no_hov_position)
8790

8891

92+
def test_hover_signature_formatting(workspace) -> None:
93+
# Over 'main' in def main():
94+
hov_position = {"line": 2, "character": 6}
95+
96+
doc = Document(DOC_URI, workspace, DOC)
97+
# setting low line length should trigger reflow to multiple lines
98+
doc._config.update({"signature": {"line_length": 10}})
99+
100+
contents = {
101+
"kind": "markdown",
102+
"value": "```python\nmain(\n a: float,\n b: float,\n)\n```\n\n\nhello world",
103+
}
104+
105+
assert {"contents": contents} == pylsp_hover(doc._config, doc, hov_position)
106+
107+
108+
def test_hover_signature_formatting_opt_out(workspace) -> None:
109+
# Over 'main' in def main():
110+
hov_position = {"line": 2, "character": 6}
111+
112+
doc = Document(DOC_URI, workspace, DOC)
113+
doc._config.update({"signature": {"line_length": 10, "formatter": None}})
114+
115+
contents = {
116+
"kind": "markdown",
117+
"value": "```python\nmain(a: float, b: float)\n```\n\n\nhello world",
118+
}
119+
120+
assert {"contents": contents} == pylsp_hover(doc._config, doc, hov_position)
121+
122+
89123
def test_document_path_hover(workspace_other_root_path, tmpdir) -> None:
90124
# Create a dummy module out of the workspace's root_path and try to get
91125
# a definition on it in another file placed next to it.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy