Skip to content

Commit e95386e

Browse files
committed
jedi: improve signature information
This fixes numerous issues where jedi provided incomplete or poorly formatted signatures and docstrings. Issue: https://github.com/pybricks/pybricks-code/issues/932
1 parent a758eaa commit e95386e

File tree

12 files changed

+1131
-300
lines changed

12 files changed

+1131
-300
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
<!-- refer to https://keepachangelog.com/en/1.0.0/ for guidance -->
44

5+
## Unreleased
6+
7+
### Fixed
8+
- Fixed more type hints and improved compatibility with jedi.
9+
510
## 3.2.0b1-r2 - 2022-06-24
611

712
### Changed

jedi/poetry.lock

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jedi/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ python = ">= 3.10, < 3.11"
1010
pybricks = "^3.2.0b1-r2"
1111
jedi = "^0.18.1"
1212
typing-extensions = "^4.2.0"
13+
docstring-parser = "0.14.1"
1314

1415
[tool.poetry.dev-dependencies]
1516
pytest = "^7.1.2"

jedi/src/pybricks_jedi/__init__.py

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
from enum import IntEnum
1+
import io
22
import json
3-
from typing_extensions import TypedDict, NotRequired
3+
import re
4+
from enum import IntEnum
5+
6+
import docstring_parser
47
import jedi
5-
from jedi.api.classes import Completion, Signature, ParamName
8+
from jedi.api.classes import BaseName, Completion, Name, ParamName, Signature
9+
from typing_extensions import NotRequired, TypedDict
610

711
# Packages included in Pybricks firmware that ships with Pybricks Code.
812
PYBRICKS_CODE_PACKAGES = {
@@ -132,6 +136,23 @@ class Command(TypedDict):
132136
arguments: NotRequired[list]
133137

134138

139+
class UriComponents(TypedDict):
140+
scheme: str
141+
authority: str
142+
path: str
143+
query: str
144+
fragment: str
145+
146+
147+
class IMarkdownString(TypedDict):
148+
value: str
149+
isTrusted: NotRequired[bool]
150+
supportThemeIcons: NotRequired[bool]
151+
supportHtml: NotRequired[bool]
152+
baseUri: NotRequired[UriComponents]
153+
uris: NotRequired[dict[str, UriComponents]]
154+
155+
135156
class CompletionItemKind(IntEnum):
136157
Method = 0
137158
Function = 1
@@ -251,6 +272,66 @@ def _is_pybricks(c: Completion) -> bool:
251272
return True
252273

253274

275+
def _get_docstring(name: BaseName) -> str:
276+
"""
277+
Gets the docstring for a name.
278+
"""
279+
280+
docstring = name.docstring(raw=True)
281+
282+
# jedi does not appear to be smart enough to use __init__ docstring for class
283+
if name.type == "class" and isinstance(name, Name):
284+
n: Name
285+
for n in name.defined_names():
286+
if n.name == "__init__":
287+
docstring = "\n".join([docstring, _get_docstring(n)])
288+
289+
return docstring
290+
291+
292+
def _parse_docstring(text: str) -> tuple[IMarkdownString, list[IMarkdownString]]:
293+
"""
294+
Parses a doc string, removes the overload declarations, performs some
295+
fixups and extracts the individual parameter strings.
296+
297+
Args:
298+
The raw docstring.
299+
300+
Returns:
301+
A tuple with the fixed up doc string and a list of parameter doc strings.
302+
"""
303+
# docstring_parser does not support signatures at the beginning of the
304+
# docstring, so we have to remove them
305+
# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autodoc_docstring_signature
306+
307+
lines, end_of_signatures = [], False
308+
309+
for line in io.StringIO(text).readlines():
310+
# signatures look like: "name(params...)"
311+
if not end_of_signatures and re.match(r"^\w+\(.*\)", line):
312+
continue
313+
314+
end_of_signatures = True
315+
316+
# TODO: we may want to do some restructured text to markdown fixes,
317+
# e.g. strip off ":class:" from ":class:`SomeClass`" and replace
318+
# ".. some-directive::" with an appropriate header
319+
320+
lines.append(line)
321+
322+
text = "".join(lines)
323+
324+
doc = docstring_parser.parse(text, docstring_parser.DocstringStyle.GOOGLE)
325+
326+
# convert to numpy doc for better markdown rendering (section names are underlined)
327+
numpy_doc = docstring_parser.compose(doc, docstring_parser.Style.NUMPYDOC)
328+
329+
docstring = IMarkdownString(value=numpy_doc)
330+
param_docstrings = [IMarkdownString(value=p.description) for p in doc.params]
331+
332+
return docstring, param_docstrings
333+
334+
254335
def _map_completion_kind(type: str) -> CompletionItemKind:
255336
match type:
256337
case "module":
@@ -294,22 +375,23 @@ def _map_completion_item(
294375
endLineNumber=line,
295376
endColumn=column,
296377
),
297-
documentation=completion.docstring(),
378+
documentation=_parse_docstring(_get_docstring(completion))[0],
298379
)
299380

300381

301-
def _map_parameter(param: ParamName) -> ParameterInformation:
302-
# NB: it is not possible to get docstring for individual parameters from jedi
303-
return ParameterInformation(label=param.to_string())
382+
def _map_parameter(param: ParamName, docstr: str) -> ParameterInformation:
383+
return ParameterInformation(label=param.to_string(), documentation=docstr)
304384

305385

306386
def _map_signature(signature: Signature) -> SignatureInformation:
307387
optional = {} if signature.index is None else dict(activeParameter=signature.index)
308388

389+
docstr, param_docstr = _parse_docstring(_get_docstring(signature))
390+
309391
return SignatureInformation(
310392
label=signature.to_string(),
311-
documentation=signature.docstring(),
312-
parameters=[_map_parameter(p) for p in signature.params],
393+
documentation=docstr,
394+
parameters=[_map_parameter(*p) for p in zip(signature.params, param_docstr)],
313395
**optional,
314396
)
315397

0 commit comments

Comments
 (0)