-
-
Notifications
You must be signed in to change notification settings - Fork 33.2k
gh-73536: Add support for multi-signatures #117671
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
9247a46
54db6d3
9bbde5f
5c9ddb9
5e7ca6d
53bc04e
1a3536f
6ff7151
2ad265a
57f3abf
2227748
5df505a
087fd94
452bb6f
5a70fb0
5678904
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 |
---|---|---|
|
@@ -67,6 +67,7 @@ | |
"GEN_CREATED", | ||
"GEN_RUNNING", | ||
"GEN_SUSPENDED", | ||
"MultiSignature", | ||
"Parameter", | ||
"Signature", | ||
"TPFLAGS_IS_ABSTRACT", | ||
|
@@ -134,6 +135,7 @@ | |
"istraceback", | ||
"markcoroutinefunction", | ||
"signature", | ||
"signatures", | ||
"stack", | ||
"trace", | ||
"unwrap", | ||
|
@@ -2189,19 +2191,20 @@ def _signature_is_functionlike(obj): | |
(isinstance(annotations, (dict)) or annotations is None) ) | ||
|
||
|
||
def _signature_strip_non_python_syntax(signature): | ||
def _signature_split(signature): | ||
""" | ||
Private helper function. Takes a signature in Argument Clinic's | ||
extended signature format. | ||
|
||
Returns a tuple of two things: | ||
Yields pairs: | ||
* that signature re-rendered in standard Python syntax, and | ||
* the index of the "self" parameter (generally 0), or None if | ||
the function does not have a "self" parameter. | ||
""" | ||
|
||
if not signature: | ||
return signature, None | ||
yield (signature, None) | ||
return | ||
|
||
self_parameter = None | ||
|
||
|
@@ -2214,38 +2217,47 @@ def _signature_strip_non_python_syntax(signature): | |
|
||
current_parameter = 0 | ||
OP = token.OP | ||
ERRORTOKEN = token.ERRORTOKEN | ||
NEWLINE = token.NEWLINE | ||
NL = token.NL | ||
|
||
# token stream always starts with ENCODING token, skip it | ||
t = next(token_stream) | ||
assert t.type == tokenize.ENCODING | ||
|
||
for t in token_stream: | ||
type, string = t.type, t.string | ||
|
||
if type == OP: | ||
if type == NEWLINE: | ||
yield (''.join(text), self_parameter) | ||
text.clear() | ||
self_parameter = None | ||
current_parameter = 0 | ||
continue | ||
elif type == NL: | ||
continue | ||
elif type == OP: | ||
if string == ',': | ||
current_parameter += 1 | ||
|
||
if (type == OP) and (string == '$'): | ||
assert self_parameter is None | ||
self_parameter = current_parameter | ||
continue | ||
|
||
string = ', ' | ||
elif string == '$' and self_parameter is None: | ||
self_parameter = current_parameter | ||
continue | ||
add(string) | ||
if (string == ','): | ||
add(' ') | ||
clean_signature = ''.join(text).strip().replace("\n", "") | ||
return clean_signature, self_parameter | ||
|
||
|
||
def _signature_fromstr(cls, obj, s, skip_bound_arg=True): | ||
"""Private helper to parse content of '__text_signature__' | ||
and return a Signature based on it. | ||
""" | ||
Parameter = cls._parameter_cls | ||
signatures = [_signature_fromstr1(cls, obj, | ||
clean_signature, self_parameter, | ||
skip_bound_arg) | ||
for clean_signature, self_parameter in _signature_split(s)] | ||
if len(signatures) == 1: | ||
return signatures[0] | ||
else: | ||
return MultiSignature(signatures) | ||
|
||
clean_signature, self_parameter = _signature_strip_non_python_syntax(s) | ||
def _signature_fromstr1(cls, obj, clean_signature, self_parameter, skip_bound_arg): | ||
Parameter = cls._parameter_cls | ||
|
||
program = "def foo" + clean_signature + ": pass" | ||
|
||
|
@@ -2830,6 +2842,8 @@ def replace(self, *, name=_void, kind=_void, | |
|
||
return type(self)(name, kind, default=default, annotation=annotation) | ||
|
||
__replace__ = replace | ||
|
||
def __str__(self): | ||
kind = self.kind | ||
formatted = self._name | ||
|
@@ -2852,8 +2866,6 @@ def __str__(self): | |
|
||
return formatted | ||
|
||
__replace__ = replace | ||
|
||
def __repr__(self): | ||
return '<{} "{}">'.format(self.__class__.__name__, self) | ||
|
||
|
@@ -3133,7 +3145,7 @@ def __hash__(self): | |
def __eq__(self, other): | ||
if self is other: | ||
return True | ||
if not isinstance(other, Signature): | ||
if not isinstance(other, Signature) or isinstance(other, MultiSignature): | ||
return NotImplemented | ||
return self._hash_basis() == other._hash_basis() | ||
|
||
|
@@ -3355,11 +3367,106 @@ def format(self, *, max_width=None): | |
return rendered | ||
|
||
|
||
class MultiSignature(Signature): | ||
__slots__ = ('_signatures',) | ||
|
||
def __init__(self, signatures): | ||
signatures = tuple(signatures) | ||
if not signatures: | ||
raise ValueError('No signatures') | ||
self._signatures = signatures | ||
|
||
@staticmethod | ||
def from_callable(obj, *, | ||
follow_wrapped=True, globals=None, locals=None, eval_str=False): | ||
"""Constructs MultiSignature for the given callable object.""" | ||
signature = Signature.from_callable(obj, follow_wrapped=follow_wrapped, | ||
globals=globals, locals=locals, | ||
eval_str=eval_str) | ||
if not isinstance(signature, MultiSignature): | ||
signature = MultiSignature((signature,)) | ||
return signature | ||
|
||
@property | ||
def parameters(self): | ||
try: | ||
return self._parameters | ||
except AttributeError: | ||
pass | ||
params = {} | ||
for s in self._signatures: | ||
params.update(s.parameters) | ||
self._parameters = types.MappingProxyType(params) | ||
return self._parameters | ||
|
||
@property | ||
def return_annotation(self): | ||
try: | ||
return self._return_annotation | ||
except AttributeError: | ||
pass | ||
self._return_annotation = types.UnionType(tuple(s.return_annotation | ||
for s in self._signatures | ||
if s.return_annotation != _empty)) | ||
return self._return_annotation | ||
|
||
def replace(self): | ||
raise NotImplementedError | ||
|
||
__replace__ = replace | ||
|
||
def __iter__(self): | ||
return iter(self._signatures) | ||
|
||
def __hash__(self): | ||
if len(self._signatures) == 1: | ||
return hash(self._signatures[0]) | ||
return hash(self._signatures) | ||
|
||
def __eq__(self, other): | ||
if self is other: | ||
return True | ||
if isinstance(other, MultiSignature): | ||
return self._signatures == other._signatures | ||
if len(self._signatures) == 1 and isinstance(other, Signature): | ||
return self._signatures[0] == other | ||
serhiy-storchaka marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
return NotImplemented | ||
|
||
def _bind(self, args, kwargs, *, partial=False): | ||
"""Private method. Don't use directly.""" | ||
for i, s in enumerate(self._signatures): | ||
try: | ||
return s._bind(args, kwargs, partial=partial) | ||
except TypeError: | ||
if i == len(self._signatures) - 1: | ||
raise | ||
|
||
def __reduce__(self): | ||
return type(self), (self._signatures,) | ||
|
||
def __repr__(self): | ||
return '<%s %s>' % (self.__class__.__name__, | ||
'|'.join(map(str, self._signatures))) | ||
|
||
def __str__(self): | ||
return '\n'.join(map(str, self._signatures)) | ||
|
||
def format(self, *, max_width=None): | ||
return '\n'.join(sig.format(max_width=max_width) | ||
for sig in self._signatures) | ||
|
||
|
||
def signature(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False): | ||
"""Get a signature object for the passed callable.""" | ||
return Signature.from_callable(obj, follow_wrapped=follow_wrapped, | ||
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. Given that the original Signature.from_callable is not changed and does the exact same call to Strictly speaking this is not a violation of backwards compatibility as MultiSignature is a subclass of Signature, but I find it weird and it may break some strict-type behaviors. Is there a way to have those only return pure Signature objects, without breaking everything this update brings to the table ? 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. Yes, it is. The idea was that |
||
globals=globals, locals=locals, eval_str=eval_str) | ||
|
||
def signatures(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False): | ||
"""Get a multi-signature object for the passed callable.""" | ||
return MultiSignature.from_callable(obj, follow_wrapped=follow_wrapped, | ||
globals=globals, locals=locals, | ||
eval_str=eval_str) | ||
|
||
|
||
class BufferFlags(enum.IntFlag): | ||
SIMPLE = 0x0 | ||
|
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.
I think this should be
and the addition to Signature's eq should be removed, above.
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.
No, it is not correct, it would make a 1-element MultiSignature equal to any Signature.
You perhaps mistyped, I wrote more correct fix.
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.
I did. That's good. (I would suggest removing the line about MultiSignature in Signature's eq as I believe it's dead code now, but up to you).
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.
It is not dead. It is called in
SignatureSubclass() == MultiSignature()
. Although it is not covered by tests yet.