Skip to content
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

feat: Format signature help when valid docstring style #6170

Closed
1 change: 1 addition & 0 deletions docker/server-jetty/src/main/server-jetty/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ adbc-driver-manager==1.1.0
adbc-driver-postgresql==1.1.0
connectorx==0.3.3; platform.machine == 'x86_64'
deephaven-plugin==0.6.0
docstring_parser==0.16
wusteven815 marked this conversation as resolved.
Show resolved Hide resolved
importlib_resources==6.4.3
java-utilities==0.3.0
jedi==0.19.1
Expand Down
1 change: 1 addition & 0 deletions docker/server/src/main/server-netty/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ adbc-driver-manager==1.1.0
adbc-driver-postgresql==1.1.0
connectorx==0.3.3; platform.machine == 'x86_64'
deephaven-plugin==0.6.0
docstring_parser==0.16
importlib_resources==6.4.3
java-utilities==0.3.0
jedi==0.19.1
Expand Down
13 changes: 5 additions & 8 deletions py/server/deephaven_internal/auto_completer/_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
#
from __future__ import annotations
from enum import Enum
from docstring_parser import parse
from typing import Any, Union, List
from jedi import Interpreter, Script
from jedi.api.classes import Completion, Signature
from importlib.metadata import version
from ._signature_help import _get_signature_result
import sys
import warnings

Expand Down Expand Up @@ -78,6 +80,8 @@ class Completer:
def __init__(self):
self._docs = {}
self._versions = {}
# Cache for signature markdown
self.signature_cache = {}
# we will replace this w/ top-level globals() when we open the document
self.__scope = globals()
# might want to make this a {uri: []} instead of []
Expand Down Expand Up @@ -214,14 +218,7 @@ def do_signature_help(
# keep checking the latest version as we run, so updated doc can cancel us
if not self._versions[uri] == version:
return []

result: list = [
signature.to_string(),
signature.docstring(raw=True),
[[param.to_string().strip(), param.docstring(raw=True).strip()] for param in signature.params],
signature.index if signature.index is not None else -1
]
results.append(result)
results.append(_get_signature_result(signature))

return results

Expand Down
179 changes: 179 additions & 0 deletions py/server/deephaven_internal/auto_completer/_signature_help.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#
# Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
#
from __future__ import annotations
from inspect import Parameter
from typing import Any, List
from docstring_parser import parse, Docstring
from jedi.api.classes import Signature

from pprint import pprint

result_cache = {}
mofojed marked this conversation as resolved.
Show resolved Hide resolved


def _hash(signature: Signature) -> str:
"""A simple way to identify signatures"""
mofojed marked this conversation as resolved.
Show resolved Hide resolved
return f"{signature.to_string()}\n{signature.docstring(raw=True)}"
mofojed marked this conversation as resolved.
Show resolved Hide resolved


def _generate_param_markdown(param: dict) -> List[Any]:
description = f"##### **{param['name']}**"
if param['type'] is not None:
description += f": *{param['type']}*"
description += "\n\n"

if param['default'] is not None:
description += f"Default: {param['default']}\n\n"

if param['description'] is not None:
description += f"{param['description']}\n\n"

return description + "---"


def _get_params(signature: Signature, docs: Docstring) -> List[Any]:
"""
Combines all available parameter information from the signature and docstring.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vague and unclear


Args:
signature: The signature from `jedi`
docs: The parsed docstring from `docstring_parser`
Comment on lines +32 to +33
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sphinx has a way to link to the methods / modules. @jmao-denver Is that appropriate here, or should it not be done because these are external modules?


Returns:
A list of dictionaries that contain the parameter name, description, type, and default value.
"""

params = []
params_info = {}

# Take information from docs first
for param in docs.params:
params_info[param.arg_name.replace("*", "")] = {
"description": param.description.strip(),
"type": param.type_name,
}

for param in signature.params:
param_str = param.to_string().strip()

# Add back * or ** for display purposes only
if param.kind == Parameter.VAR_POSITIONAL:
name = f"*{param.name}"
elif param.kind == Parameter.VAR_KEYWORD:
name = f"**{param.name}"
else:
name = param.name

# Use type in signature first, then type in docs, then None
if ":" in param_str:
type_ = param_str.split(":")[1].split("=")[0].strip()
elif param.name in params_info:
type_ = params_info[param.name]["type"]
else:
type_ = None

params.append({
"name": name,
"description": params_info.get(param.name, {}).get("description"),
"type": type_,
"default": param_str.split("=")[1] if "=" in param_str else None,
})

return params


def _get_raises(docs: Docstring) -> List[Any]:
raises = []
for raise_ in docs.raises:
raises.append({
"name": raise_.type_name,
"description": raise_.description
})

return raises


def _get_returns(docs: Docstring) -> List[Any]:
returns = []
for return_ in docs.many_returns:
returns.append({
"name": return_.type_name,
"description": return_.description
})

return returns


def _get_signature_result(signature: Signature) -> List[Any]:
""" Gets the result of a signature to be used by `do_signature_help`

Returns:
A list that contains [signature name, docstring, param docstrings, index]
Comment on lines +199 to +200
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be returning something more structured/organized, like a class?

"""

docstring = signature.docstring(raw=True)
cache_key = _hash(signature)

if cache_key in result_cache:
result = result_cache[cache_key].copy() # deep copy not needed since only index is different
result.append(signature.index if signature.index is not None else -1)
return result

docs = parse(docstring)

# Nothing parsed, revert to plaintext
if docstring == docs.description:
return [
signature.to_string(),
signature.docstring(raw=True).replace(" ", " ").replace("\n", " \n"),
[[param.to_string().strip(), ""] for param in signature.params],
signature.index if signature.index is not None else -1,
]

params = _get_params(signature, docs)
raises = _get_raises(docs)
returns = _get_returns(docs)
description = docs.description.strip().replace("\n", " \n") + "\n\n"

if len(params) > 0:
description += "#### **Parameters**\n\n"
for param in params:
description += f"> **{param['name']}**"
if param['type'] is not None:
description += f": *{param['type']}*"
if param['default'] is not None:
description += f" ⋅ (default: *{param['default']}*)"
description += " \n"

if param['description'] is not None:
description += f"> {param['description']}"
description += "\n\n"

if len(returns) > 0:
description += "#### **Returns**\n\n"
for return_ in returns:
if return_["name"] is not None:
description += f"> **{return_['name']}** \n"
if return_["description"] is not None:
description += f"> {return_['description']}"
description += "\n\n"

if len(raises) > 0:
description += "#### **Raises**\n\n"
for raises_ in raises:
if raises_["name"] is not None:
description += f"> **{raises_['name']}** \n"
if raises_["description"] is not None:
description += f"> {raises_['description']}"
description += "\n\n"

result = [
f"{signature.to_string().split('(')[0]}(...)" if len(signature.to_string()) > 20 else signature.to_string(),
description.strip(),
[[signature.params[i].to_string().strip(), _generate_param_markdown(params[i])] for i in range(len(signature.params))],
]
result_cache[cache_key] = result.copy() # deep copy not needed since only index is different
result.append(signature.index if signature.index is not None else -1)

return result
2 changes: 1 addition & 1 deletion py/server/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def _compute_version():
'numba; python_version < "3.13"',
],
extras_require={
"autocomplete": ["jedi==0.19.1"],
"autocomplete": ["jedi==0.19.1", "docstring_parser==0.16"],
wusteven815 marked this conversation as resolved.
Show resolved Hide resolved
},
entry_points={
'deephaven.plugin': ['registration_cls = deephaven.pandasplugin:PandasPluginRegistration']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,14 +257,14 @@ private GetSignatureHelpResponse getSignatureHelp(GetSignatureHelpRequest reques

final SignatureInformation.Builder item = SignatureInformation.newBuilder();
item.setLabel(label);
item.setDocumentation(MarkupContent.newBuilder().setValue(docstring).setKind("plaintext").build());
item.setDocumentation(MarkupContent.newBuilder().setValue(docstring).setKind("markdown").build());
item.setActiveParameter(activeParam);

signature.get(2).asList().forEach(obj -> {
final List<PyObject> param = obj.asList();
item.addParameters(ParameterInformation.newBuilder().setLabel(param.get(0).getStringValue())
.setDocumentation(MarkupContent.newBuilder().setValue(param.get(1).getStringValue())
.setKind("plaintext").build()));
.setKind("markdown").build()));
});

finalItems.add(item.build());
Expand Down
Loading