1
0
Fork 0
ptpython/ptpython/signatures.py
Daniel Baumann 51331ec610
Adding upstream version 3.0.27.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-02-09 18:34:08 +01:00

271 lines
8.1 KiB
Python

"""
Helpers for retrieving the function signature of the function call that we are
editing.
Either with the Jedi library, or using `inspect.signature` if Jedi fails and we
can use `eval()` to evaluate the function object.
"""
from __future__ import annotations
import inspect
from inspect import Signature as InspectSignature
from inspect import _ParameterKind as ParameterKind
from typing import TYPE_CHECKING, Any, Sequence
from prompt_toolkit.document import Document
from .completer import DictionaryCompleter
from .utils import get_jedi_script_from_document
if TYPE_CHECKING:
import jedi.api.classes
__all__ = ["Signature", "get_signatures_using_jedi", "get_signatures_using_eval"]
class Parameter:
def __init__(
self,
name: str,
annotation: str | None,
default: str | None,
kind: ParameterKind,
) -> None:
self.name = name
self.kind = kind
self.annotation = annotation
self.default = default
def __repr__(self) -> str:
return f"Parameter(name={self.name!r})"
@property
def description(self) -> str:
"""
Name + annotation.
"""
description = self.name
if self.annotation is not None:
description += f": {self.annotation}"
return description
class Signature:
"""
Signature definition used wrap around both Jedi signatures and
python-inspect signatures.
:param index: Parameter index of the current cursor position.
:param bracket_start: (line, column) tuple for the open bracket that starts
the function call.
"""
def __init__(
self,
name: str,
docstring: str,
parameters: Sequence[Parameter],
index: int | None = None,
returns: str = "",
bracket_start: tuple[int, int] = (0, 0),
) -> None:
self.name = name
self.docstring = docstring
self.parameters = parameters
self.index = index
self.returns = returns
self.bracket_start = bracket_start
@classmethod
def from_inspect_signature(
cls,
name: str,
docstring: str,
signature: InspectSignature,
index: int,
) -> Signature:
parameters = []
def get_annotation_name(annotation: object) -> str:
"""
Get annotation as string from inspect signature.
"""
try:
# In case the annotation is a class like "int", "float", ...
return str(annotation.__name__) # type: ignore
except AttributeError:
pass # No attribute `__name__`, e.g., in case of `List[int]`.
annotation = str(annotation)
if annotation.startswith("typing."):
annotation = annotation[len("typing:") :]
return annotation
for p in signature.parameters.values():
parameters.append(
Parameter(
name=p.name,
annotation=get_annotation_name(p.annotation),
default=repr(p.default)
if p.default is not inspect.Parameter.empty
else None,
kind=p.kind,
)
)
return cls(
name=name,
docstring=docstring,
parameters=parameters,
index=index,
returns="",
)
@classmethod
def from_jedi_signature(cls, signature: jedi.api.classes.Signature) -> Signature:
parameters = []
for p in signature.params:
if p is None:
# We just hit the "*".
continue
parameters.append(
Parameter(
name=p.to_string(), # p.name, (`to_string()` already includes the annotation).
annotation=None, # p.infer_annotation()
default=None, # p.infer_default()
kind=p.kind,
)
)
docstring = signature.docstring()
if not isinstance(docstring, str):
docstring = docstring.decode("utf-8")
return cls(
name=signature.name,
docstring=docstring,
parameters=parameters,
index=signature.index,
returns="",
bracket_start=signature.bracket_start,
)
def __repr__(self) -> str:
return f"Signature({self.name!r}, parameters={self.parameters!r})"
def get_signatures_using_jedi(
document: Document, locals: dict[str, Any], globals: dict[str, Any]
) -> list[Signature]:
script = get_jedi_script_from_document(document, locals, globals)
# Show signatures in help text.
if not script:
return []
try:
signatures = script.get_signatures()
except ValueError:
# e.g. in case of an invalid \\x escape.
signatures = []
except Exception:
# Sometimes we still get an exception (TypeError), because
# of probably bugs in jedi. We can silence them.
# See: https://github.com/davidhalter/jedi/issues/492
signatures = []
else:
# Try to access the params attribute just once. For Jedi
# signatures containing the keyword-only argument star,
# this will crash when retrieving it the first time with
# AttributeError. Every following time it works.
# See: https://github.com/jonathanslenders/ptpython/issues/47
# https://github.com/davidhalter/jedi/issues/598
try:
if signatures:
signatures[0].params
except AttributeError:
pass
return [Signature.from_jedi_signature(sig) for sig in signatures]
def get_signatures_using_eval(
document: Document, locals: dict[str, Any], globals: dict[str, Any]
) -> list[Signature]:
"""
Look for the signature of the function before the cursor position without
use of Jedi. This uses a similar approach as the `DictionaryCompleter` of
running `eval()` over the detected function name.
"""
# Look for open parenthesis, before cursor position.
pos = document.cursor_position - 1
paren_mapping = {")": "(", "}": "{", "]": "["}
paren_stack = [
")"
] # Start stack with closing ')'. We are going to look for the matching open ')'.
comma_count = 0 # Number of comma's between start of function call and cursor pos.
found_start = False # Found something.
while pos >= 0:
char = document.text[pos]
if char in ")]}":
paren_stack.append(char)
elif char in "([{":
if not paren_stack:
# Open paren, while no closing paren was found. Mouse cursor is
# positioned in nested parentheses. Not at the "top-level" of a
# function call.
break
if paren_mapping[paren_stack[-1]] != char:
# Unmatching parentheses: syntax error?
break
paren_stack.pop()
if len(paren_stack) == 0:
found_start = True
break
elif char == "," and len(paren_stack) == 1:
comma_count += 1
pos -= 1
if not found_start:
return []
# We found the start of the function call. Now look for the object before
# this position on which we can do an 'eval' to retrieve the function
# object.
obj = DictionaryCompleter(lambda: globals, lambda: locals).eval_expression(
Document(document.text, cursor_position=pos), locals
)
if obj is None:
return []
try:
name = obj.__name__ # type:ignore
except Exception:
name = obj.__class__.__name__
try:
signature = inspect.signature(obj) # type: ignore
except TypeError:
return [] # Not a callable object.
except ValueError:
return [] # No signature found, like for build-ins like "print".
try:
doc = obj.__doc__ or ""
except:
doc = ""
# TODO: `index` is not yet correct when dealing with keyword-only arguments.
return [Signature.from_inspect_signature(name, doc, signature, index=comma_count)]