Repository navigation
gh-81678: Add inspect.Signature.from_text() #14579
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
Changes from all commits
149c16c
a4096f9
23816a0
31858a1
3878a52
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1955,18 +1955,32 @@ def _signature_strip_non_python_syntax(signature): | |
| return clean_signature, self_parameter, last_positional_only | ||
|
|
||
|
|
||
| def _signature_fromstr(cls, obj, s, skip_bound_arg=True): | ||
| def _signature_fromstr(cls, obj, text, skip_bound_arg=True): | ||
| """Private helper to parse content of '__text_signature__' | ||
| and return a Signature based on it. | ||
| """ | ||
| modname = getattr(obj, '__module__', None) | ||
| _self = getattr(obj, '__self__', None) | ||
| try: | ||
| return _signature_from_text(cls, text, modname, _self, skip_bound_arg) | ||
| except ValueError as exc: | ||
| if getattr(exc, '_syntax', False): | ||
| raise ValueError("{!r} builtin has invalid signature".format(obj)) | ||
| raise | ||
|
|
||
|
|
||
| def _signature_from_text(cls, text, module_name=None, _self=None, skip_bound_arg=True): | ||
| """Private helper to parse a signature string in Argument Clinic's | ||
| extended signature format. | ||
| """ | ||
| # Lazy import ast because it's relatively heavy and | ||
| # it's not used for other than this function. | ||
| import ast | ||
|
|
||
| Parameter = cls._parameter_cls | ||
|
|
||
| clean_signature, self_parameter, last_positional_only = \ | ||
| _signature_strip_non_python_syntax(s) | ||
| (clean_signature, self_parameter, last_positional_only | ||
| ) = _signature_strip_non_python_syntax(text) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I'd keep the |
||
|
|
||
| program = "def foo" + clean_signature + ": pass" | ||
|
|
||
|
|
@@ -1976,7 +1990,9 @@ def _signature_fromstr(cls, obj, s, skip_bound_arg=True): | |
| module = None | ||
|
|
||
| if not isinstance(module, ast.Module): | ||
| raise ValueError("{!r} builtin has invalid signature".format(obj)) | ||
| exc = ValueError(f"invalid signature {text!r}") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Commenting the if block on master and in this PR has no test failure so I guess this code was not tested. Perhaps a test could be added where def test_python_function_invalid_text_signature(self):
def func(*args, **kwargs):
pass
func.__text_signature__ = '($self, **kwargs, a)'
with self.assertRaises(ValueError) as e:
sig = inspect.signature(func) |
||
| exc._syntax = True | ||
| raise exc | ||
|
|
||
| f = module.body[0] | ||
|
|
||
|
|
@@ -1986,7 +2002,6 @@ def _signature_fromstr(cls, obj, s, skip_bound_arg=True): | |
|
|
||
| module = None | ||
| module_dict = {} | ||
| module_name = getattr(obj, '__module__', None) | ||
| if module_name: | ||
| module = sys.modules.get(module_name, None) | ||
| if module: | ||
|
|
@@ -2080,7 +2095,6 @@ def p(name_node, default_node, default=empty): | |
| # - We don't strip first bound argument if | ||
| # skip_bound_arg is False. | ||
| assert parameters | ||
| _self = getattr(obj, '__self__', None) | ||
| self_isbound = _self is not None | ||
| self_ismodule = ismodule(_self) | ||
| if self_isbound and (self_ismodule or skip_bound_arg): | ||
|
|
@@ -2812,6 +2826,24 @@ def __init__(self, parameters=None, *, return_annotation=_empty, | |
| self._parameters = types.MappingProxyType(params) | ||
| self._return_annotation = return_annotation | ||
|
|
||
| @classmethod | ||
| def from_text(cls, text, *, skip_bound_arg=True): | ||
| """Constructs Signature for the given string. | ||
|
|
||
| The expected format matches what Signature.__str__() returns. | ||
| For example: | ||
|
|
||
| func() | ||
| func(a, b) | ||
| func(a: int, b: str) | ||
| func(a, b=None, /, c, d=None, *args, e, f=None, **kwargs) | ||
|
|
||
| For the most part this will round-trip with Signature.__str__(). | ||
| """ | ||
| # See Argument Clinic's extended signature format (PEP 436). | ||
| return _signature_from_text(cls, text, | ||
| skip_bound_arg=skip_bound_arg) | ||
|
|
||
| @classmethod | ||
| def from_function(cls, func): | ||
| """Constructs Signature for the given python function. | ||
|
|
@@ -3090,7 +3122,10 @@ def __str__(self): | |
|
|
||
| def signature(obj, *, follow_wrapped=True): | ||
| """Get a signature object for the passed callable.""" | ||
| return Signature.from_callable(obj, follow_wrapped=follow_wrapped) | ||
| if isinstance(obj, str): | ||
| return Signature.from_text(obj) | ||
| else: | ||
| return Signature.from_callable(obj, follow_wrapped=follow_wrapped) | ||
|
|
||
|
|
||
| def _main(): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Add inspect.Signature.from_text(). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I don't like the
signature()function to accept both callables and text. This introduces extra complexity to the API for a very narrow use case.It also makes the function too dynamic: currently, if a text value is passed to
signature()by mistake the user would get an exception saying that a callable was expected. With this patch they'll likely get a ParseError.IMO a public documented
Signature.from_text()API is enough.