|
1 | | -from enum import IntEnum |
| 1 | +import io |
2 | 2 | import json |
3 | | -from typing_extensions import TypedDict, NotRequired |
| 3 | +import re |
| 4 | +from enum import IntEnum |
| 5 | + |
| 6 | +import docstring_parser |
4 | 7 | 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 |
6 | 10 |
|
7 | 11 | # Packages included in Pybricks firmware that ships with Pybricks Code. |
8 | 12 | PYBRICKS_CODE_PACKAGES = { |
@@ -132,6 +136,23 @@ class Command(TypedDict): |
132 | 136 | arguments: NotRequired[list] |
133 | 137 |
|
134 | 138 |
|
| 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 | + |
135 | 156 | class CompletionItemKind(IntEnum): |
136 | 157 | Method = 0 |
137 | 158 | Function = 1 |
@@ -251,6 +272,66 @@ def _is_pybricks(c: Completion) -> bool: |
251 | 272 | return True |
252 | 273 |
|
253 | 274 |
|
| 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 | + |
254 | 335 | def _map_completion_kind(type: str) -> CompletionItemKind: |
255 | 336 | match type: |
256 | 337 | case "module": |
@@ -294,22 +375,23 @@ def _map_completion_item( |
294 | 375 | endLineNumber=line, |
295 | 376 | endColumn=column, |
296 | 377 | ), |
297 | | - documentation=completion.docstring(), |
| 378 | + documentation=_parse_docstring(_get_docstring(completion))[0], |
298 | 379 | ) |
299 | 380 |
|
300 | 381 |
|
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) |
304 | 384 |
|
305 | 385 |
|
306 | 386 | def _map_signature(signature: Signature) -> SignatureInformation: |
307 | 387 | optional = {} if signature.index is None else dict(activeParameter=signature.index) |
308 | 388 |
|
| 389 | + docstr, param_docstr = _parse_docstring(_get_docstring(signature)) |
| 390 | + |
309 | 391 | return SignatureInformation( |
310 | 392 | 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)], |
313 | 395 | **optional, |
314 | 396 | ) |
315 | 397 |
|
|
0 commit comments