Skip to content

Commit bd24653

Browse files
pythongh-73536: Add support of multi-signatures
* Add inspect.MultiSignature which is a subclass of inspect.Signature. * Add inspect.signatures(). * inspect.signature() can now return a multi-signature. * Support multi-signatures in pydoc. * Support multi-signatures in IDLE calltips. * Support multi-signatures in dataclasses docstrings. * Allow multiple @text_signature in Argument Clinic. * Add representable signatures for all builtin functions and method of builtin classes except type() and super().
1 parent 5718324 commit bd24653

28 files changed

Lines changed: 468 additions & 196 deletions

‎Lib/dataclasses.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,10 +1162,11 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
11621162
try:
11631163
# In some cases fetching a signature is not possible.
11641164
# But, we surely should not fail in this case.
1165-
text_sig = str(inspect.signature(cls)).replace(' -> None', '')
1165+
doc = '\n'.join(cls.__name__ + str(sig).replace(' -> None', '')
1166+
for sig in inspect.signatures(cls))
11661167
except (TypeError, ValueError):
1167-
text_sig = ''
1168-
cls.__doc__ = (cls.__name__ + text_sig)
1168+
doc = cls.__name__
1169+
cls.__doc__ = doc
11691170

11701171
if match_args:
11711172
# I could probably compute this once.

‎Lib/idlelib/calltip.py‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,20 +170,25 @@ def get_argspec(ob):
170170

171171
# Initialize argspec and wrap it to get lines.
172172
try:
173-
argspec = str(inspect.signature(fob))
173+
signatures = inspect.signatures(fob)
174174
except Exception as err:
175175
msg = str(err)
176176
if msg.startswith(_invalid_method):
177177
return _invalid_method
178178
else:
179-
argspec = ''
179+
signatures = []
180180

181-
if isinstance(fob, type) and argspec == '()':
181+
lines = []
182+
for sig in signatures:
183+
line = str(sig)
184+
if len(line) > _MAX_COLS:
185+
lines.extend(textwrap.wrap(line, _MAX_COLS, subsequent_indent=_INDENT))
186+
else:
187+
lines.append(line)
188+
if isinstance(fob, type) and lines == ['()']:
182189
# If fob has no argument, use default callable argspec.
183-
argspec = _default_callable_argspec
190+
lines = [_default_callable_argspec]
184191

185-
lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT)
186-
if len(argspec) > _MAX_COLS else [argspec] if argspec else [])
187192

188193
# Augment lines from docstring, if any, and join to get argspec.
189194
doc = inspect.getdoc(ob)

‎Lib/idlelib/idle_test/test_calltip.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from idlelib import calltip
44
import unittest
55
from unittest.mock import Mock
6+
import builtins
67
import textwrap
78
import types
89
import re
@@ -151,17 +152,48 @@ def f(): pass
151152
"Signature information for builtins requires docstrings")
152153
def test_multiline_docstring(self):
153154
# Test fewer lines than max.
155+
def range():
156+
"""range(stop) -> range object
157+
range(start, stop[, step]) -> range object
158+
159+
Return an object that produces a sequence of integers from start
160+
(inclusive) to stop (exclusive) by step.
161+
"""
162+
range.__text_signature__ = '<no signature>'
154163
self.assertEqual(get_spec(range),
155164
"range(stop) -> range object\n"
156165
"range(start, stop[, step]) -> range object")
166+
self.assertEqual(get_spec(builtins.range),
167+
"(stop, /)\n"
168+
"(start, stop, step=1, /)\n"
169+
"Create a range object.")
157170

158171
# Test max lines
172+
def bytes():
173+
"""bytes(iterable_of_ints) -> bytes
174+
bytes(string, encoding[, errors]) -> bytes
175+
bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer
176+
bytes(int) -> bytes object of size given by the parameter initialized with null bytes
177+
bytes() -> empty bytes object
178+
179+
Construct an immutable array of bytes from:
180+
- an iterable yielding integers in range(256)
181+
- a text string encoded using the specified encoding
182+
- any object implementing the buffer API.
183+
- an integer"""
184+
bytes.__text_signature__ = '<no signature>'
159185
self.assertEqual(get_spec(bytes), '''\
160186
bytes(iterable_of_ints) -> bytes
161187
bytes(string, encoding[, errors]) -> bytes
162188
bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer
163189
bytes(int) -> bytes object of size given by the parameter initialized with null bytes
164190
bytes() -> empty bytes object''')
191+
self.assertEqual(get_spec(builtins.bytes), '''\
192+
()
193+
(source)
194+
(source, encoding)
195+
(source, encoding, errors)
196+
Construct an immutable array of bytes.''')
165197

166198
def test_multiline_docstring_2(self):
167199
# Test more than max lines

‎Lib/inspect.py‎

Lines changed: 129 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"GEN_CREATED",
6868
"GEN_RUNNING",
6969
"GEN_SUSPENDED",
70+
"MultiSignature",
7071
"Parameter",
7172
"Signature",
7273
"TPFLAGS_IS_ABSTRACT",
@@ -134,6 +135,7 @@
134135
"istraceback",
135136
"markcoroutinefunction",
136137
"signature",
138+
"signatures",
137139
"stack",
138140
"trace",
139141
"unwrap",
@@ -2189,19 +2191,20 @@ def _signature_is_functionlike(obj):
21892191
(isinstance(annotations, (dict)) or annotations is None) )
21902192

21912193

2192-
def _signature_strip_non_python_syntax(signature):
2194+
def _signature_split(signature):
21932195
"""
21942196
Private helper function. Takes a signature in Argument Clinic's
21952197
extended signature format.
21962198
2197-
Returns a tuple of two things:
2199+
Yields pairs:
21982200
* that signature re-rendered in standard Python syntax, and
21992201
* the index of the "self" parameter (generally 0), or None if
22002202
the function does not have a "self" parameter.
22012203
"""
22022204

22032205
if not signature:
2204-
return signature, None
2206+
yield (signature, None)
2207+
return
22052208

22062209
self_parameter = None
22072210

@@ -2214,38 +2217,47 @@ def _signature_strip_non_python_syntax(signature):
22142217

22152218
current_parameter = 0
22162219
OP = token.OP
2217-
ERRORTOKEN = token.ERRORTOKEN
2220+
NEWLINE = token.NEWLINE
2221+
NL = token.NL
22182222

22192223
# token stream always starts with ENCODING token, skip it
22202224
t = next(token_stream)
22212225
assert t.type == tokenize.ENCODING
22222226

22232227
for t in token_stream:
22242228
type, string = t.type, t.string
2225-
2226-
if type == OP:
2229+
if type == NEWLINE:
2230+
yield (''.join(text), self_parameter)
2231+
text.clear()
2232+
self_parameter = None
2233+
current_parameter = 0
2234+
continue
2235+
elif type == NL:
2236+
continue
2237+
elif type == OP:
22272238
if string == ',':
22282239
current_parameter += 1
2229-
2230-
if (type == OP) and (string == '$'):
2231-
assert self_parameter is None
2232-
self_parameter = current_parameter
2233-
continue
2234-
2240+
string = ', '
2241+
elif string == '$' and self_parameter is None:
2242+
self_parameter = current_parameter
2243+
continue
22352244
add(string)
2236-
if (string == ','):
2237-
add(' ')
2238-
clean_signature = ''.join(text).strip().replace("\n", "")
2239-
return clean_signature, self_parameter
2240-
22412245

22422246
def _signature_fromstr(cls, obj, s, skip_bound_arg=True):
22432247
"""Private helper to parse content of '__text_signature__'
22442248
and return a Signature based on it.
22452249
"""
2246-
Parameter = cls._parameter_cls
2250+
signatures = [_signature_fromstr1(cls, obj,
2251+
clean_signature, self_parameter,
2252+
skip_bound_arg)
2253+
for clean_signature, self_parameter in _signature_split(s)]
2254+
if len(signatures) == 1:
2255+
return signatures[0]
2256+
else:
2257+
return MultiSignature(signatures)
22472258

2248-
clean_signature, self_parameter = _signature_strip_non_python_syntax(s)
2259+
def _signature_fromstr1(cls, obj, clean_signature, self_parameter, skip_bound_arg):
2260+
Parameter = cls._parameter_cls
22492261

22502262
program = "def foo" + clean_signature + ": pass"
22512263

@@ -2830,6 +2842,8 @@ def replace(self, *, name=_void, kind=_void,
28302842

28312843
return type(self)(name, kind, default=default, annotation=annotation)
28322844

2845+
__replace__ = replace
2846+
28332847
def __str__(self):
28342848
kind = self.kind
28352849
formatted = self._name
@@ -2852,8 +2866,6 @@ def __str__(self):
28522866

28532867
return formatted
28542868

2855-
__replace__ = replace
2856-
28572869
def __repr__(self):
28582870
return '<{} "{}">'.format(self.__class__.__name__, self)
28592871

@@ -3133,7 +3145,7 @@ def __hash__(self):
31333145
def __eq__(self, other):
31343146
if self is other:
31353147
return True
3136-
if not isinstance(other, Signature):
3148+
if not isinstance(other, Signature) or isinstance(other, MultiSignature):
31373149
return NotImplemented
31383150
return self._hash_basis() == other._hash_basis()
31393151

@@ -3355,11 +3367,106 @@ def format(self, *, max_width=None):
33553367
return rendered
33563368

33573369

3370+
class MultiSignature(Signature):
3371+
__slots__ = ('_signatures',)
3372+
3373+
def __init__(self, signatures):
3374+
signatures = tuple(signatures)
3375+
if not signatures:
3376+
raise ValueError('No signatures')
3377+
self._signatures = signatures
3378+
3379+
@staticmethod
3380+
def from_callable(obj, *,
3381+
follow_wrapped=True, globals=None, locals=None, eval_str=False):
3382+
"""Constructs MultiSignature for the given callable object."""
3383+
signature = Signature.from_callable(obj, follow_wrapped=follow_wrapped,
3384+
globals=globals, locals=locals,
3385+
eval_str=eval_str)
3386+
if not isinstance(signature, MultiSignature):
3387+
signature = MultiSignature((signature,))
3388+
return signature
3389+
3390+
@property
3391+
def parameters(self):
3392+
try:
3393+
return self._parameters
3394+
except AttributeError:
3395+
pass
3396+
params = {}
3397+
for s in self._signatures:
3398+
params.update(s.parameters)
3399+
self._parameters = types.MappingProxyType(params)
3400+
return self._parameters
3401+
3402+
@property
3403+
def return_annotation(self):
3404+
try:
3405+
return self._return_annotation
3406+
except AttributeError:
3407+
pass
3408+
self._return_annotation = types.UnionType(tuple(s.return_annotation
3409+
for s in self._signatures
3410+
if s.return_annotation != _empty))
3411+
return self._return_annotation
3412+
3413+
def replace(self):
3414+
raise NotImplementedError
3415+
3416+
__replace__ = replace
3417+
3418+
def __iter__(self):
3419+
return iter(self._signatures)
3420+
3421+
def __hash__(self):
3422+
if len(self._signatures) == 1:
3423+
return hash(self._signatures[0])
3424+
return hash(self._signatures)
3425+
3426+
def __eq__(self, other):
3427+
if self is other:
3428+
return True
3429+
if isinstance(other, MultiSignature):
3430+
return self._signatures == other._signatures
3431+
if len(self._signatures) == 1 and isinstance(other, Signature):
3432+
return self._signatures[0] == other
3433+
return NotImplemented
3434+
3435+
def _bind(self, args, kwargs, *, partial=False):
3436+
"""Private method. Don't use directly."""
3437+
for i, s in enumerate(self._signatures):
3438+
try:
3439+
return s._bind(args, kwargs, partial=partial)
3440+
except TypeError:
3441+
if i == len(self._signatures) - 1:
3442+
raise
3443+
3444+
def __reduce__(self):
3445+
return type(self), (self._signatures,)
3446+
3447+
def __repr__(self):
3448+
return '<%s %s>' % (self.__class__.__name__,
3449+
'|'.join(map(str, self._signatures)))
3450+
3451+
def __str__(self):
3452+
return '\n'.join(map(str, self._signatures))
3453+
3454+
def format(self, *, max_width=None):
3455+
return '\n'.join(sig.format(max_width=max_width)
3456+
for sig in self._signatures)
3457+
3458+
33583459
def signature(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False):
33593460
"""Get a signature object for the passed callable."""
33603461
return Signature.from_callable(obj, follow_wrapped=follow_wrapped,
33613462
globals=globals, locals=locals, eval_str=eval_str)
33623463

3464+
def signatures(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False):
3465+
"""Get a multi-signature object for the passed callable."""
3466+
return MultiSignature.from_callable(obj, follow_wrapped=follow_wrapped,
3467+
globals=globals, locals=locals,
3468+
eval_str=eval_str)
3469+
33633470

33643471
class BufferFlags(enum.IntFlag):
33653472
SIMPLE = 0x0

0 commit comments

Comments
 (0)