From 9247a46ea5ab7e4bb13d051369e19d43122f20fd Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 19 Aug 2023 16:13:37 +0300 Subject: [PATCH 1/9] gh-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 methods of builtin classes except type() and super(). --- Lib/dataclasses.py | 7 +- Lib/idlelib/calltip.py | 17 ++- Lib/idlelib/idle_test/test_calltip.py | 32 ++++++ Lib/inspect.py | 151 ++++++++++++++++++++++---- Lib/pydoc.py | 78 +++++++------ Lib/test/test_bytes.py | 8 +- Lib/test/test_dataclasses/__init__.py | 19 +++- Lib/test/test_functools.py | 2 +- Lib/test/test_inspect/test_inspect.py | 35 ++---- Lib/test/test_pydoc/test_pydoc.py | 13 +-- Modules/_testcapi/docstring.c | 15 +++ Objects/bytearrayobject.c | 34 +++--- Objects/bytesobject.c | 33 +++--- Objects/clinic/bytearrayobject.c.h | 21 +++- Objects/clinic/bytesobject.c.h | 21 +++- Objects/clinic/dictobject.c.h | 7 +- Objects/clinic/longobject.c.h | 5 +- Objects/clinic/memoryobject.c.h | 8 +- Objects/clinic/unicodeobject.c.h | 5 +- Objects/dictobject.c | 49 ++++++--- Objects/longobject.c | 9 +- Objects/memoryobject.c | 8 +- Objects/rangeobject.c | 7 +- Objects/sliceobject.c | 5 +- Objects/unicodeobject.c | 12 +- Python/bltinmodule.c | 47 +++++--- Python/clinic/bltinmodule.c.h | 11 +- Tools/clinic/libclinic/dsl_parser.py | 5 +- 28 files changed, 468 insertions(+), 196 deletions(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 3acd03cd865234..e29acafdbe0337 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1162,10 +1162,11 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, try: # In some cases fetching a signature is not possible. # But, we surely should not fail in this case. - text_sig = str(inspect.signature(cls)).replace(' -> None', '') + doc = '\n'.join(cls.__name__ + str(sig).replace(' -> None', '') + for sig in inspect.signatures(cls)) except (TypeError, ValueError): - text_sig = '' - cls.__doc__ = (cls.__name__ + text_sig) + doc = cls.__name__ + cls.__doc__ = doc if match_args: # I could probably compute this once. diff --git a/Lib/idlelib/calltip.py b/Lib/idlelib/calltip.py index 40bc5a0ad798fe..74abd96b2a5141 100644 --- a/Lib/idlelib/calltip.py +++ b/Lib/idlelib/calltip.py @@ -170,20 +170,25 @@ def get_argspec(ob): # Initialize argspec and wrap it to get lines. try: - argspec = str(inspect.signature(fob)) + signatures = inspect.signatures(fob) except Exception as err: msg = str(err) if msg.startswith(_invalid_method): return _invalid_method else: - argspec = '' + signatures = [] - if isinstance(fob, type) and argspec == '()': + lines = [] + for sig in signatures: + line = str(sig) + if len(line) > _MAX_COLS: + lines.extend(textwrap.wrap(line, _MAX_COLS, subsequent_indent=_INDENT)) + else: + lines.append(line) + if isinstance(fob, type) and lines == ['()']: # If fob has no argument, use default callable argspec. - argspec = _default_callable_argspec + lines = [_default_callable_argspec] - lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT) - if len(argspec) > _MAX_COLS else [argspec] if argspec else []) # Augment lines from docstring, if any, and join to get argspec. doc = inspect.getdoc(ob) diff --git a/Lib/idlelib/idle_test/test_calltip.py b/Lib/idlelib/idle_test/test_calltip.py index 28c196a42672fc..007800bca1cb9f 100644 --- a/Lib/idlelib/idle_test/test_calltip.py +++ b/Lib/idlelib/idle_test/test_calltip.py @@ -3,6 +3,7 @@ from idlelib import calltip import unittest from unittest.mock import Mock +import builtins import textwrap import types import re @@ -151,17 +152,48 @@ def f(): pass "Signature information for builtins requires docstrings") def test_multiline_docstring(self): # Test fewer lines than max. + def range(): + """range(stop) -> range object + range(start, stop[, step]) -> range object + + Return an object that produces a sequence of integers from start + (inclusive) to stop (exclusive) by step. + """ + range.__text_signature__ = '' self.assertEqual(get_spec(range), "range(stop) -> range object\n" "range(start, stop[, step]) -> range object") + self.assertEqual(get_spec(builtins.range), + "(stop, /)\n" + "(start, stop, step=1, /)\n" + "Create a range object.") # Test max lines + def bytes(): + """bytes(iterable_of_ints) -> bytes + bytes(string, encoding[, errors]) -> bytes + bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer + bytes(int) -> bytes object of size given by the parameter initialized with null bytes + bytes() -> empty bytes object + + Construct an immutable array of bytes from: + - an iterable yielding integers in range(256) + - a text string encoded using the specified encoding + - any object implementing the buffer API. + - an integer""" + bytes.__text_signature__ = '' self.assertEqual(get_spec(bytes), '''\ bytes(iterable_of_ints) -> bytes bytes(string, encoding[, errors]) -> bytes bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer bytes(int) -> bytes object of size given by the parameter initialized with null bytes bytes() -> empty bytes object''') + self.assertEqual(get_spec(builtins.bytes), '''\ +() +(source) +(source, encoding) +(source, encoding, errors) +Construct an immutable array of bytes.''') def test_multiline_docstring_2(self): # Test more than max lines diff --git a/Lib/inspect.py b/Lib/inspect.py index 422c09a92ad141..157bc6729e7466 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -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,7 +2217,8 @@ 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) @@ -2222,30 +2226,38 @@ def _signature_strip_non_python_syntax(signature): 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 + 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, 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 diff --git a/Lib/pydoc.py b/Lib/pydoc.py index d9cf03fb4ffd2a..d7b95c34cf6c65 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -196,25 +196,32 @@ def splitdoc(doc): return lines[0], '\n'.join(lines[2:]) return '', '\n'.join(lines) -def _getargspec(object): +def _getargspecs(object): try: - signature = inspect.signature(object) - if signature: + signatures = inspect.signatures(object) + if signatures: name = getattr(object, '__name__', '') # function are always single-line and should not be formatted max_width = (80 - len(name)) if name != '' else None - return signature.format(max_width=max_width) + return [sig.format(max_width=max_width) for sig in signatures] + return None except (ValueError, TypeError): - argspec = getattr(object, '__text_signature__', None) - if argspec: - if argspec[:2] == '($': - argspec = '(' + argspec[2:] + pass + text_signature = getattr(object, '__text_signature__', None) + if text_signature: + argspecs = [] + #for argspec in re.split(r'(?<=\))\n(?=\()', text_signature): + # if argspec[:2] == '($': + # argspec = '(' + argspec[2:] + for argspec, _ in inspect._signature_split(text_signature): if getattr(object, '__self__', None) is not None: # Strip the bound argument. m = re.match(r'\(\w+(?:(?=\))|,\s*(?:/(?:(?=\))|,\s*))?)', argspec) if m: argspec = '(' + argspec[m.end():] - return argspec + argspecs.append(argspec) + return argspecs + #print('>>>>>>>>', object, file=sys.stderr) return None def classname(object, modname): @@ -1060,9 +1067,10 @@ def spilldata(msg, attrs, predicate): title = title + '(%s)' % ', '.join(parents) decl = '' - argspec = _getargspec(object) - if argspec and argspec != '()': - decl = name + self.escape(argspec) + '\n\n' + argspecs = _getargspecs(object) + if argspecs and argspecs != ['()']: + decl = '\n'.join(name + self.escape(argspec) + for argspec in argspecs) + '\n\n' doc = getdoc(object) if decl: @@ -1136,29 +1144,32 @@ def docroutine(self, object, name=None, mod=None, reallink = realname title = '%s = %s' % ( anchor, name, reallink) - argspec = None + argspecs = None if inspect.isroutine(object): - argspec = _getargspec(object) - if argspec and realname == '': + argspecs = _getargspecs(object) + if argspecs and realname == '': title = '%s lambda ' % name # XXX lambda's won't usually have func_annotations['return'] # since the syntax doesn't support but it is possible. # So removing parentheses isn't truly safe. if not object.__annotations__: - argspec = argspec[1:-1] # remove parentheses - if not argspec: - argspec = '(...)' + argspecs = [argspec[1:-1] for argspec in argspecs] # remove parentheses + if not argspecs: + argspecs = ['(...)'] - decl = asyncqualifier + title + self.escape(argspec) + (note and - self.grey('%s' % note)) + decl = ''.join( + '
%s%s%s%s
' % ( + asyncqualifier, title, self.escape(argspec), + (note and self.grey('%s' % note))) + for argspec in argspecs) if skipdocs: - return '
%s
\n' % decl + return '
%s
\n' % decl else: doc = self.markup( getdoc(object), self.preformat, funcs, classes, methods) doc = doc and '
%s
' % doc - return '
%s
%s
\n' % (decl, doc) + return '
%s%s
\n' % (decl, doc) def docdata(self, object, name=None, mod=None, cl=None, *ignored): """Produce html documentation for a data descriptor.""" @@ -1392,9 +1403,11 @@ def makename(c, m=object.__module__): contents = [] push = contents.append - argspec = _getargspec(object) - if argspec and argspec != '()': - push(name + argspec + '\n') + argspecs = _getargspecs(object) + if argspecs and argspecs != ['()']: + for argspec in argspecs: + push(name + argspec) + push('') doc = getdoc(object) if doc: @@ -1578,20 +1591,21 @@ def docroutine(self, object, name=None, mod=None, cl=None, homecls=None): if note.startswith(' from '): note = '' title = self.bold(name) + ' = ' + realname - argspec = None + argspecs = None if inspect.isroutine(object): - argspec = _getargspec(object) - if argspec and realname == '': + argspecs = _getargspecs(object) + if argspecs and realname == '': title = self.bold(name) + ' lambda ' # XXX lambda's won't usually have func_annotations['return'] # since the syntax doesn't support but it is possible. # So removing parentheses isn't truly safe. if not object.__annotations__: - argspec = argspec[1:-1] - if not argspec: - argspec = '(...)' - decl = asyncqualifier + title + argspec + note + argspecs = [argspec[1:-1] for argspec in argspecs] # remove parentheses + if not argspecs: + argspecs = ['(...)'] + decl = '\n'.join(asyncqualifier + title + argspec + note + for argspec in argspecs) if skipdocs: return decl + '\n' diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index 9e1985bb3a7639..0804f1a62e9a7e 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -1870,9 +1870,13 @@ def test_compare_bytes_to_bytearray(self): @test.support.requires_docstrings def test_doc(self): self.assertIsNotNone(bytearray.__doc__) - self.assertTrue(bytearray.__doc__.startswith("bytearray("), bytearray.__doc__) + self.assertTrue(bytearray.__doc__.startswith( + "Construct a mutable bytearray object.\n\n"), + bytearray.__doc__) self.assertIsNotNone(bytes.__doc__) - self.assertTrue(bytes.__doc__.startswith("bytes("), bytes.__doc__) + self.assertTrue(bytes.__doc__.startswith( + "Construct an immutable array of bytes.\n\n"), + bytes.__doc__) def test_from_bytearray(self): sample = bytes(b"Hello world\n\x80\x81\xfe\xff") diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 832e5672c77d0d..2342a481d97c92 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -2300,7 +2300,7 @@ class C: self.assertDocStrEqual(C.__doc__, "C(x:collections.deque=)") - def test_docstring_with_no_signature(self): + def test_docstring_with_multi_signature(self): # See https://github.com/python/cpython/issues/103449 class Meta(type): __call__ = dict @@ -2311,6 +2311,23 @@ class Base(metaclass=Meta): class C(Base): pass + self.assertDocStrEqual(C.__doc__, + "C(**kwargs)\n" + "C(mapping_or_iterable, /, **kwargs)") + + def test_docstring_with_no_signature(self): + # See https://github.com/python/cpython/issues/103449 + class Meta(type): + def __call__(self, x): + pass + __call__.__text_signature__ = '' + class Base(metaclass=Meta): + pass + + @dataclass + class C(Base): + pass + self.assertDocStrEqual(C.__doc__, "C") diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index c48c399a10c853..9e8f5cdf196442 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -694,7 +694,7 @@ def wrapper(): pass functools.update_wrapper(wrapper, max) self.assertEqual(wrapper.__name__, 'max') - self.assertTrue(wrapper.__doc__.startswith('max(')) + self.assertEqual(wrapper.__doc__, max.__doc__) self.assertEqual(wrapper.__annotations__, {}) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 6494842c217662..8208040d1f228f 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -3000,10 +3000,6 @@ def test_signature_on_builtins_no_signature(self): 'no signature found for builtin'): inspect.signature(_testcapi.docstring_no_signature) - with self.assertRaisesRegex(ValueError, - 'no signature found for builtin'): - inspect.signature(str) - cls = _testcapi.DocStringNoSignatureTest obj = _testcapi.DocStringNoSignatureTest() tests = [ @@ -5171,13 +5167,9 @@ def foo(a): pass self.assertIs(type(ba.arguments), dict) class TestSignaturePrivateHelpers(unittest.TestCase): - def _strip_non_python_syntax(self, input, - clean_signature, self_parameter): - computed_clean_signature, \ - computed_self_parameter = \ - inspect._signature_strip_non_python_syntax(input) - self.assertEqual(computed_clean_signature, clean_signature) - self.assertEqual(computed_self_parameter, self_parameter) + def _strip_non_python_syntax(self, input, clean_signature, self_parameter): + self.assertEqual(list(inspect._signature_split(input)), + [(clean_signature, self_parameter)]) def test_signature_strip_non_python_syntax(self): self._strip_non_python_syntax( @@ -5234,21 +5226,12 @@ def test_builtins_have_signatures(self): # the test to fail in order to indicate when it needs to be # updated. no_signature = set() - # These need PEP 457 groups - needs_groups = {"range", "slice", "dir", "getattr", - "next", "iter", "vars"} - no_signature |= needs_groups - # These have unrepresentable parameter default values of NULL - needs_null = {"anext"} - no_signature |= needs_null - # These need *args support in Argument Clinic - needs_varargs = {"min", "max", "__build_class__"} - no_signature |= needs_varargs # These builtin types are expected to provide introspection info types_with_signatures = { - 'bool', 'classmethod', 'complex', 'enumerate', 'filter', 'float', - 'frozenset', 'list', 'map', 'memoryview', 'object', 'property', - 'reversed', 'set', 'staticmethod', 'tuple', 'zip' + '__loader__', 'bool', 'bytearray', 'bytes', 'classmethod', + 'complex', 'dict', 'enumerate', 'filter', 'float', 'frozenset', + 'int', 'list', 'map', 'memoryview', 'object', 'property', 'range', + 'reversed', 'set', 'slice', 'staticmethod', 'str', 'tuple', 'zip' } # Check the signatures we expect to be there ns = vars(builtins) @@ -5261,6 +5244,8 @@ def test_builtins_have_signatures(self): no_signature.add(name) if (name in no_signature): # Not yet converted + with self.subTest(builtin=name): + self.assertRaises(ValueError, inspect.signature, obj) continue if name in {'classmethod', 'staticmethod'}: # Bug gh-112006: inspect.unwrap() does not work with types @@ -5272,7 +5257,7 @@ def test_builtins_have_signatures(self): # This ensures this test will start failing as more signatures are # added, so the affected items can be moved into the scope of the # regression test above - for name in no_signature - needs_null: + for name in no_signature: with self.subTest(builtin=name): self.assertIsNone(ns[name].__text_signature__) diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index 436fdb38756ddd..381bb0599c170c 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -1376,12 +1376,15 @@ def test_module_level_callable_unrepresentable_default(self): builtin = _testcapi.func_with_unrepresentable_signature self.assertEqual(self._get_summary_line(builtin), "func_with_unrepresentable_signature(a, b=)") + builtin = _testcapi.func_with_unrepresentable_multisignature + self.assertEqual(self._get_summary_lines(builtin), + "func_with_unrepresentable_multisignature()\n" + "func_with_unrepresentable_multisignature(a, b=)\n" + " This docstring has a multisignature with unrepresentable default.\n") @support.cpython_only @requires_docstrings def test_builtin_staticmethod_unrepresentable_default(self): - self.assertEqual(self._get_summary_line(str.maketrans), - "maketrans(x, y=, z=, /)") _testcapi = import_helper.import_module("_testcapi") cls = _testcapi.DocStringUnrepresentableSignatureTest self.assertEqual(self._get_summary_line(cls.staticmeth), @@ -1390,9 +1393,6 @@ def test_builtin_staticmethod_unrepresentable_default(self): @support.cpython_only @requires_docstrings def test_unbound_builtin_method_unrepresentable_default(self): - self.assertEqual(self._get_summary_line(dict.pop), - "pop(self, key, default=, /) " - "unbound builtins.dict method") _testcapi = import_helper.import_module("_testcapi") cls = _testcapi.DocStringUnrepresentableSignatureTest self.assertEqual(self._get_summary_line(cls.meth), @@ -1402,9 +1402,6 @@ def test_unbound_builtin_method_unrepresentable_default(self): @support.cpython_only @requires_docstrings def test_bound_builtin_method_unrepresentable_default(self): - self.assertEqual(self._get_summary_line({}.pop), - "pop(key, default=, /) " - "method of builtins.dict instance") _testcapi = import_helper.import_module("_testcapi") obj = _testcapi.DocStringUnrepresentableSignatureTest() self.assertEqual(self._get_summary_line(obj.meth), diff --git a/Modules/_testcapi/docstring.c b/Modules/_testcapi/docstring.c index d99fbdd904b594..43edec6712c709 100644 --- a/Modules/_testcapi/docstring.c +++ b/Modules/_testcapi/docstring.c @@ -107,6 +107,14 @@ static PyMethodDef test_methods[] = { "--\n\n" "This docstring has a signature with unrepresentable default." )}, + {"func_with_unrepresentable_multisignature", + (PyCFunction)test_with_docstring, METH_VARARGS, + PyDoc_STR( + "func_with_unrepresentable_multisignature($module, /)\n" + "($module, /, a, b=)\n" + "--\n\n" + "This docstring has a multisignature with unrepresentable default." + )}, {NULL}, }; @@ -169,6 +177,13 @@ static PyMethodDef DocStringUnrepresentableSignatureTest_methods[] = { "--\n\n" "This docstring has a signature with unrepresentable default." )}, + {"meth_multi", + (PyCFunction)test_with_docstring, METH_VARARGS, + PyDoc_STR( + "meth_multi($self, /, a, b=)\n" + "--\n\n" + "This docstring has a multisignature with unrepresentable default." + )}, {NULL}, }; diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c index 8639496727536a..14754a4caa4d1d 100644 --- a/Objects/bytearrayobject.c +++ b/Objects/bytearrayobject.c @@ -738,18 +738,30 @@ bytearray_ass_subscript(PyByteArrayObject *self, PyObject *index, PyObject *valu } /*[clinic input] +@text_signature "()" +@text_signature "(source)" +@text_signature "(source, encoding)" +@text_signature "(source, encoding, errors)" bytearray.__init__ source as arg: object = NULL encoding: str = NULL errors: str = NULL +Construct a mutable bytearray object. + +If there are no arguments, create an empty bytearray object. If the first +argument is an integer, create a zero-filled bytearray object of the +specified size. If it is an iterable of integers, fill the new bytearray +object with them. If it is an object implementing the buffer protocol, +copy its content in the new bytearray object. If it is a text string, +encode it with the specified encoding and errors handler. [clinic start generated code]*/ static int bytearray___init___impl(PyByteArrayObject *self, PyObject *arg, const char *encoding, const char *errors) -/*[clinic end generated code: output=4ce1304649c2f8b3 input=1141a7122eefd7b9]*/ +/*[clinic end generated code: output=4ce1304649c2f8b3 input=d2ed9273070b926c]*/ { Py_ssize_t count; PyObject *it; @@ -2099,6 +2111,8 @@ bytearray_fromhex_impl(PyTypeObject *type, PyObject *string) } /*[clinic input] +@text_signature "($self, *, bytes_per_sep=1, /)" +@text_signature "($self, sep, bytes_per_sep=1, /)" bytearray.hex sep: object = NULL @@ -2123,7 +2137,7 @@ Create a string of hexadecimal numbers from a bytearray object. static PyObject * bytearray_hex_impl(PyByteArrayObject *self, PyObject *sep, int bytes_per_sep) -/*[clinic end generated code: output=29c4e5ef72c565a0 input=808667e49bcccb54]*/ +/*[clinic end generated code: output=29c4e5ef72c565a0 input=719afde525c65958]*/ { char* argbuf = PyByteArray_AS_STRING(self); Py_ssize_t arglen = PyByteArray_GET_SIZE(self); @@ -2310,20 +2324,6 @@ static PyNumberMethods bytearray_as_number = { bytearray_mod, /*nb_remainder*/ }; -PyDoc_STRVAR(bytearray_doc, -"bytearray(iterable_of_ints) -> bytearray\n\ -bytearray(string, encoding[, errors]) -> bytearray\n\ -bytearray(bytes_or_buffer) -> mutable copy of bytes_or_buffer\n\ -bytearray(int) -> bytes array of size given by the parameter initialized with null bytes\n\ -bytearray() -> empty bytes array\n\ -\n\ -Construct a mutable bytearray object from:\n\ - - an iterable yielding integers in range(256)\n\ - - a text string encoded using the specified encoding\n\ - - a bytes or a buffer object\n\ - - any object implementing the buffer API.\n\ - - an integer"); - static PyObject *bytearray_iter(PyObject *seq); @@ -2349,7 +2349,7 @@ PyTypeObject PyByteArray_Type = { &bytearray_as_buffer, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | _Py_TPFLAGS_MATCH_SELF, /* tp_flags */ - bytearray_doc, /* tp_doc */ + bytearray___init____doc__, /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ (richcmpfunc)bytearray_richcompare, /* tp_richcompare */ diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c index d576dd93f05e10..0120ca65629e7a 100644 --- a/Objects/bytesobject.c +++ b/Objects/bytesobject.c @@ -2479,6 +2479,8 @@ _PyBytes_FromHex(PyObject *string, int use_bytearray) } /*[clinic input] +@text_signature "($self, *, bytes_per_sep=1, /)" +@text_signature "($self, sep, bytes_per_sep=1, /)" bytes.hex sep: object = NULL @@ -2503,7 +2505,7 @@ Create a string of hexadecimal numbers from a bytes object. static PyObject * bytes_hex_impl(PyBytesObject *self, PyObject *sep, int bytes_per_sep) -/*[clinic end generated code: output=1f134da504064139 input=1a21282b1f1ae595]*/ +/*[clinic end generated code: output=1f134da504064139 input=9c31b616d1648899]*/ { const char *argbuf = PyBytes_AS_STRING(self); Py_ssize_t arglen = PyBytes_GET_SIZE(self); @@ -2599,6 +2601,10 @@ static PyObject * bytes_subtype_new(PyTypeObject *, PyObject *); /*[clinic input] +@text_signature "()" +@text_signature "(source)" +@text_signature "(source, encoding)" +@text_signature "(source, encoding, errors)" @classmethod bytes.__new__ as bytes_new @@ -2606,12 +2612,20 @@ bytes.__new__ as bytes_new encoding: str = NULL errors: str = NULL +Construct an immutable array of bytes. + +If there are no arguments, create an empty bytes object. If the first +argument is an integer, create a zero-filled bytes object of the +specified size. If it is an iterable of integers, fill the new bytes +object with them. If it is an object implementing the buffer protocol, +copy its content in the new bytes object. If it is a text string, +encode it with the specified encoding and errors handler. [clinic start generated code]*/ static PyObject * bytes_new_impl(PyTypeObject *type, PyObject *x, const char *encoding, const char *errors) -/*[clinic end generated code: output=1e0c471be311a425 input=f0a966d19b7262b4]*/ +/*[clinic end generated code: output=1e0c471be311a425 input=22b618010386f6c2]*/ { PyObject *bytes; PyObject *func; @@ -2942,19 +2956,6 @@ _Py_COMP_DIAG_POP return pnew; } -PyDoc_STRVAR(bytes_doc, -"bytes(iterable_of_ints) -> bytes\n\ -bytes(string, encoding[, errors]) -> bytes\n\ -bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer\n\ -bytes(int) -> bytes object of size given by the parameter initialized with null bytes\n\ -bytes() -> empty bytes object\n\ -\n\ -Construct an immutable array of bytes from:\n\ - - an iterable yielding integers in range(256)\n\ - - a text string encoded using the specified encoding\n\ - - any object implementing the buffer API.\n\ - - an integer"); - static PyObject *bytes_iter(PyObject *seq); PyTypeObject PyBytes_Type = { @@ -2980,7 +2981,7 @@ PyTypeObject PyBytes_Type = { Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_BYTES_SUBCLASS | _Py_TPFLAGS_MATCH_SELF, /* tp_flags */ - bytes_doc, /* tp_doc */ + bytes_new__doc__, /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ (richcmpfunc)bytes_richcompare, /* tp_richcompare */ diff --git a/Objects/clinic/bytearrayobject.c.h b/Objects/clinic/bytearrayobject.c.h index dabc2b16c94fce..f2e54120dbc005 100644 --- a/Objects/clinic/bytearrayobject.c.h +++ b/Objects/clinic/bytearrayobject.c.h @@ -9,6 +9,22 @@ preserve #include "pycore_abstract.h" // _PyNumber_Index() #include "pycore_modsupport.h" // _PyArg_UnpackKeywords() +PyDoc_STRVAR(bytearray___init____doc__, +"bytearray()\n" +"(source)\n" +"(source, encoding)\n" +"(source, encoding, errors)\n" +"--\n" +"\n" +"Construct a mutable bytearray object.\n" +"\n" +"If there are no arguments, create an empty bytearray object. If the first\n" +"argument is an integer, create a zero-filled bytearray object of the\n" +"specified size. If it is an iterable of integers, fill the new bytearray\n" +"object with them. If it is an object implementing the buffer protocol,\n" +"copy its content in the new bytearray object. If it is a text string,\n" +"encode it with the specified encoding and errors handler."); + static int bytearray___init___impl(PyByteArrayObject *self, PyObject *arg, const char *encoding, const char *errors); @@ -1207,7 +1223,8 @@ bytearray_fromhex(PyTypeObject *type, PyObject *arg) } PyDoc_STRVAR(bytearray_hex__doc__, -"hex($self, /, sep=, bytes_per_sep=1)\n" +"hex($self, *, bytes_per_sep=1, /)\n" +"($self, sep, bytes_per_sep=1, /)\n" "--\n" "\n" "Create a string of hexadecimal numbers from a bytearray object.\n" @@ -1363,4 +1380,4 @@ bytearray_sizeof(PyByteArrayObject *self, PyObject *Py_UNUSED(ignored)) { return bytearray_sizeof_impl(self); } -/*[clinic end generated code: output=0147908e97ebe882 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=d73d5d353f2847c8 input=a9049054013a1b77]*/ diff --git a/Objects/clinic/bytesobject.c.h b/Objects/clinic/bytesobject.c.h index 05e182778aece1..25d31f1f363bf5 100644 --- a/Objects/clinic/bytesobject.c.h +++ b/Objects/clinic/bytesobject.c.h @@ -954,7 +954,8 @@ bytes_fromhex(PyTypeObject *type, PyObject *arg) } PyDoc_STRVAR(bytes_hex__doc__, -"hex($self, /, sep=, bytes_per_sep=1)\n" +"hex($self, *, bytes_per_sep=1, /)\n" +"($self, sep, bytes_per_sep=1, /)\n" "--\n" "\n" "Create a string of hexadecimal numbers from a bytes object.\n" @@ -1040,6 +1041,22 @@ bytes_hex(PyBytesObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject return return_value; } +PyDoc_STRVAR(bytes_new__doc__, +"bytes()\n" +"(source)\n" +"(source, encoding)\n" +"(source, encoding, errors)\n" +"--\n" +"\n" +"Construct an immutable array of bytes.\n" +"\n" +"If there are no arguments, create an empty bytes object. If the first\n" +"argument is an integer, create a zero-filled bytes object of the\n" +"specified size. If it is an iterable of integers, fill the new bytes\n" +"object with them. If it is an object implementing the buffer protocol,\n" +"copy its content in the new bytes object. If it is a text string,\n" +"encode it with the specified encoding and errors handler."); + static PyObject * bytes_new_impl(PyTypeObject *type, PyObject *x, const char *encoding, const char *errors); @@ -1131,4 +1148,4 @@ bytes_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) exit: return return_value; } -/*[clinic end generated code: output=f2b10ccd2e3155c3 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=6807468568961267 input=a9049054013a1b77]*/ diff --git a/Objects/clinic/dictobject.c.h b/Objects/clinic/dictobject.c.h index fb46c4c64334f9..f6f977d40c3231 100644 --- a/Objects/clinic/dictobject.c.h +++ b/Objects/clinic/dictobject.c.h @@ -160,10 +160,11 @@ dict_clear(PyDictObject *self, PyObject *Py_UNUSED(ignored)) } PyDoc_STRVAR(dict_pop__doc__, -"pop($self, key, default=, /)\n" +"pop($self, key, /)\n" +"($self, key, default, /)\n" "--\n" "\n" -"D.pop(k[,d]) -> v, remove specified key and return the corresponding value.\n" +"Remove specified key and return the corresponding value.\n" "\n" "If the key is not found, return the default if given; otherwise,\n" "raise a KeyError."); @@ -312,4 +313,4 @@ dict_values(PyDictObject *self, PyObject *Py_UNUSED(ignored)) { return dict_values_impl(self); } -/*[clinic end generated code: output=f3dd5f3fb8122aef input=a9049054013a1b77]*/ +/*[clinic end generated code: output=4e34bd9506b20f50 input=a9049054013a1b77]*/ diff --git a/Objects/clinic/longobject.c.h b/Objects/clinic/longobject.c.h index 4a3d71c6111af5..d7383e9ef2f462 100644 --- a/Objects/clinic/longobject.c.h +++ b/Objects/clinic/longobject.c.h @@ -116,7 +116,8 @@ int___format__(PyObject *self, PyObject *arg) } PyDoc_STRVAR(int___round____doc__, -"__round__($self, ndigits=, /)\n" +"__round__($self, /)\n" +"($self, ndigits, /)\n" "--\n" "\n" "Rounding an Integral returns itself.\n" @@ -476,4 +477,4 @@ int_is_integer(PyObject *self, PyObject *Py_UNUSED(ignored)) { return int_is_integer_impl(self); } -/*[clinic end generated code: output=7e6e57246e55911f input=a9049054013a1b77]*/ +/*[clinic end generated code: output=70469c4972cd538e input=a9049054013a1b77]*/ diff --git a/Objects/clinic/memoryobject.c.h b/Objects/clinic/memoryobject.c.h index f199434dacb9e8..27add31ec650e2 100644 --- a/Objects/clinic/memoryobject.c.h +++ b/Objects/clinic/memoryobject.c.h @@ -141,7 +141,8 @@ memoryview_release(PyMemoryViewObject *self, PyObject *Py_UNUSED(ignored)) } PyDoc_STRVAR(memoryview_cast__doc__, -"cast($self, /, format, shape=)\n" +"cast($self, format, /)\n" +"($self, format, shape, /)\n" "--\n" "\n" "Cast a memoryview to a new format or shape."); @@ -327,7 +328,8 @@ memoryview_tobytes(PyMemoryViewObject *self, PyObject *const *args, Py_ssize_t n } PyDoc_STRVAR(memoryview_hex__doc__, -"hex($self, /, sep=, bytes_per_sep=1)\n" +"hex($self, *, bytes_per_sep=1, /)\n" +"($self, sep, bytes_per_sep=1, /)\n" "--\n" "\n" "Return the data in the buffer as a str of hexadecimal numbers.\n" @@ -413,4 +415,4 @@ memoryview_hex(PyMemoryViewObject *self, PyObject *const *args, Py_ssize_t nargs exit: return return_value; } -/*[clinic end generated code: output=7e76a09106921ba2 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=7b6a89244a5f026c input=a9049054013a1b77]*/ diff --git a/Objects/clinic/unicodeobject.c.h b/Objects/clinic/unicodeobject.c.h index 01c40b90d9b4b8..62d88bd4e7ee9d 100644 --- a/Objects/clinic/unicodeobject.c.h +++ b/Objects/clinic/unicodeobject.c.h @@ -1521,7 +1521,8 @@ unicode_swapcase(PyObject *self, PyObject *Py_UNUSED(ignored)) } PyDoc_STRVAR(unicode_maketrans__doc__, -"maketrans(x, y=, z=, /)\n" +"maketrans($self, x, /)\n" +"($self, x, y, z=\'\', /)\n" "--\n" "\n" "Return a translation table usable for str.translate().\n" @@ -1888,4 +1889,4 @@ unicode_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) exit: return return_value; } -/*[clinic end generated code: output=3aa49013ffa3fa93 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=d1a9ade94836dfaa input=a9049054013a1b77]*/ diff --git a/Objects/dictobject.c b/Objects/dictobject.c index e7993e4b051433..785b5307f22caa 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -4303,13 +4303,15 @@ dict_clear_impl(PyDictObject *self) } /*[clinic input] +@text_signature "($self, key, /)" +@text_signature "($self, key, default, /)" dict.pop key: object default: object = NULL / -D.pop(k[,d]) -> v, remove specified key and return the corresponding value. +Remove specified key and return the corresponding value. If the key is not found, return the default if given; otherwise, raise a KeyError. @@ -4317,7 +4319,7 @@ raise a KeyError. static PyObject * dict_pop_impl(PyDictObject *self, PyObject *key, PyObject *default_value) -/*[clinic end generated code: output=3abb47b89f24c21c input=e221baa01044c44c]*/ +/*[clinic end generated code: output=3abb47b89f24c21c input=30abdf0727d0f247]*/ { return _PyDict_Pop((PyObject*)self, key, default_value); } @@ -4543,10 +4545,19 @@ PyDoc_STRVAR(getitem__doc__, "__getitem__($self, key, /)\n--\n\nReturn self[key]."); PyDoc_STRVAR(update__doc__, -"D.update([E, ]**F) -> None. Update D from dict/iterable E and F.\n\ -If E is present and has a .keys() method, then does: for k in E: D[k] = E[k]\n\ -If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v\n\ -In either case, this is followed by: for k in F: D[k] = F[k]"); +"update($self, /, **kwargs)\n\ +($self, mapping_or_iterable, /, **kwargs)\n\ +--\n\ +\n\ +Update a dict from a mapping or iterable and keyword arguments.\n\ +\n\ +If the positional argument is present and has a .keys() method,\n\ +then does:\n\ + for k in mapping_or_iterable: self[k] = mapping_or_iterable[k]\n\ +If it is present and lacks a .keys() method, then does:\n\ + for k, v in mapping_or_iterable: self[k] = v\n\ +In either case, this is followed by:\n\ + for k in kwargs: self[k] = kwargs[k]"); /* Forward */ @@ -4732,15 +4743,23 @@ dict_iter(PyObject *self) } PyDoc_STRVAR(dictionary_doc, -"dict() -> new empty dictionary\n" -"dict(mapping) -> new dictionary initialized from a mapping object's\n" -" (key, value) pairs\n" -"dict(iterable) -> new dictionary initialized as if via:\n" -" d = {}\n" -" for k, v in iterable:\n" -" d[k] = v\n" -"dict(**kwargs) -> new dictionary initialized with the name=value pairs\n" -" in the keyword argument list. For example: dict(one=1, two=2)"); +"dict(**kwargs)\n" +"(mapping_or_iterable, /, **kwargs)\n" +"--\n" +"\n" +"Create a new dictionary.\n" +"\n" +"If no arguments are supplied, create an empty dictionary:\n" +" dict() -> {}\n" +"If the positional argument is a mapping object, a new dictionary will\n" +"be initialized from its (key, value) pairs.\n" +"If it is an iterable of pairs, a new dictionary will\n" +"be initialized from these pairs.\n" +" dict([('one', 1), ('two', 2)]) -> {'one': 1, 'two': 2}\n" +"If keyword arguments are supplied, new items will be added using the\n" +"keyword name as a key and the argument value as a value:\n" +" dict(one=1, two=2) -> {'one': 1, 'two': 2}\n" +"Positional and keyword arguments can be combined."); PyTypeObject PyDict_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) diff --git a/Objects/longobject.c b/Objects/longobject.c index c4ab064d688d67..f40098bf809375 100644 --- a/Objects/longobject.c +++ b/Objects/longobject.c @@ -6032,6 +6032,8 @@ _PyLong_DivmodNear(PyObject *a, PyObject *b) } /*[clinic input] +@text_signature "($self, /)" +@text_signature "($self, ndigits, /)" int.__round__ ndigits as o_ndigits: object = NULL @@ -6044,7 +6046,7 @@ Rounding with an ndigits argument also returns an integer. static PyObject * int___round___impl(PyObject *self, PyObject *o_ndigits) -/*[clinic end generated code: output=954fda6b18875998 input=1614cf23ec9e18c3]*/ +/*[clinic end generated code: output=954fda6b18875998 input=c39d8036d189bdea]*/ { PyObject *temp, *result, *ndigits; @@ -6484,8 +6486,9 @@ static PyGetSetDef long_getset[] = { }; PyDoc_STRVAR(long_doc, -"int([x]) -> integer\n\ -int(x, base=10) -> integer\n\ +"int(x=0, /)\n\ +(x, /, base=10)\n\ +--\n\ \n\ Convert a number or string to an integer, or return 0 if no arguments\n\ are given. If x is a number, return x.__int__(). For floating point\n\ diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 6a38952fdc1f3b..b6f0df95e6a92f 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -1426,6 +1426,8 @@ zero_in_shape(PyMemoryViewObject *mv) size of the original input. Otherwise, an error is raised. */ /*[clinic input] +@text_signature "($self, format, /)" +@text_signature "($self, format, shape, /)" memoryview.cast format: unicode @@ -1437,7 +1439,7 @@ Cast a memoryview to a new format or shape. static PyObject * memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format, PyObject *shape) -/*[clinic end generated code: output=bae520b3a389cbab input=138936cc9041b1a3]*/ +/*[clinic end generated code: output=bae520b3a389cbab input=7a8da62fc30e35e4]*/ { PyMemoryViewObject *mv = NULL; Py_ssize_t ndim = 1; @@ -2297,6 +2299,8 @@ memoryview_tobytes_impl(PyMemoryViewObject *self, const char *order) } /*[clinic input] +@text_signature "($self, *, bytes_per_sep=1, /)" +@text_signature "($self, sep, bytes_per_sep=1, /)" memoryview.hex sep: object = NULL @@ -2322,7 +2326,7 @@ Return the data in the buffer as a str of hexadecimal numbers. static PyObject * memoryview_hex_impl(PyMemoryViewObject *self, PyObject *sep, int bytes_per_sep) -/*[clinic end generated code: output=430ca760f94f3ca7 input=539f6a3a5fb56946]*/ +/*[clinic end generated code: output=430ca760f94f3ca7 input=10129bce3c852fc3]*/ { Py_buffer *src = VIEW_ADDR(self); PyObject *bytes; diff --git a/Objects/rangeobject.c b/Objects/rangeobject.c index ce9eef69ad75a8..817481a26473ef 100644 --- a/Objects/rangeobject.c +++ b/Objects/rangeobject.c @@ -154,8 +154,11 @@ range_vectorcall(PyTypeObject *type, PyObject *const *args, } PyDoc_STRVAR(range_doc, -"range(stop) -> range object\n\ -range(start, stop[, step]) -> range object\n\ +"range(stop, /)\n\ +(start, stop, step=1, /)\n\ +--\n\ +\n\ +Create a range object.\n\ \n\ Return an object that produces a sequence of integers from start (inclusive)\n\ to stop (exclusive) by step. range(i, j) produces i, i+1, i+2, ..., j-1.\n\ diff --git a/Objects/sliceobject.c b/Objects/sliceobject.c index 7333aea91e5648..a99b2392313948 100644 --- a/Objects/sliceobject.c +++ b/Objects/sliceobject.c @@ -352,8 +352,9 @@ slice_new(PyTypeObject *type, PyObject *args, PyObject *kw) } PyDoc_STRVAR(slice_doc, -"slice(stop)\n\ -slice(start, stop[, step])\n\ +"slice(stop, /)\n\ +(start, stop, step=1, /)\n\ +--\n\ \n\ Create a slice object. This is used for extended slicing (e.g. a[0:10:2])."); diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c index 5f15071d7d54ef..c849ade14b219c 100644 --- a/Objects/unicodeobject.c +++ b/Objects/unicodeobject.c @@ -12745,6 +12745,8 @@ unicode_swapcase_impl(PyObject *self) /*[clinic input] +@text_signature "($self, x, /)" +@text_signature "($self, x, y, z='', /)" @staticmethod str.maketrans as unicode_maketrans @@ -12769,7 +12771,7 @@ must be a string, whose characters will be mapped to None in the result. static PyObject * unicode_maketrans_impl(PyObject *x, PyObject *y, PyObject *z) -/*[clinic end generated code: output=a925c89452bd5881 input=7bfbf529a293c6c5]*/ +/*[clinic end generated code: output=a925c89452bd5881 input=39cc0e989ac451fd]*/ { PyObject *new = NULL, *key, *value; Py_ssize_t i = 0; @@ -14702,8 +14704,12 @@ _PyUnicode_ExactDealloc(PyObject *op) } PyDoc_STRVAR(unicode_doc, -"str(object='') -> str\n\ -str(bytes_or_buffer[, encoding[, errors]]) -> str\n\ +"str()\n\ +(object)\n\ +(object, encoding)\n\ +(object, encoding, errors)\n\ +(object, *, errors)\n\ +--\n\ \n\ Create a new string object from the given object. If encoding or\n\ errors is specified, then the object must expose a data buffer\n\ diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index f66a8c07c6f872..3983762c11dda4 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -239,7 +239,9 @@ builtin___build_class__(PyObject *self, PyObject *const *args, Py_ssize_t nargs, } PyDoc_STRVAR(build_class_doc, -"__build_class__(func, name, /, *bases, [metaclass], **kwds) -> class\n\ +"__build_class__(func, name, /, *bases, **kwds)\n\ +(func, name, /, *bases, metaclass, **kwds)\n\ +--\n\ \n\ Internal helper function used by the class statement."); @@ -890,7 +892,9 @@ builtin_dir(PyObject *self, PyObject *args) } PyDoc_STRVAR(dir_doc, -"dir([object]) -> list of strings\n" +"dir()\n" +"(object, /)\n" +"--\n" "\n" "If called without an argument, return the names in the current scope.\n" "Else, return an alphabetized list of names comprising (some of) the attributes\n" @@ -1196,7 +1200,9 @@ builtin_getattr(PyObject *self, PyObject *const *args, Py_ssize_t nargs) } PyDoc_STRVAR(getattr_doc, -"getattr(object, name[, default]) -> value\n\ +"getattr($module, object, name, /)\n\ +($module, object, name, default, /)\n\ +--\n\ \n\ Get a named attribute from an object; getattr(x, 'y') is equivalent to x.y.\n\ When a default argument is given, it is returned when the attribute doesn't\n\ @@ -1551,7 +1557,9 @@ builtin_next(PyObject *self, PyObject *const *args, Py_ssize_t nargs) } PyDoc_STRVAR(next_doc, -"next(iterator[, default])\n\ +"next(iterator, /)\n\ +next(iterator, default, /)\n\ +--\n\ \n\ Return the next item from the iterator. If default is given and the iterator\n\ is exhausted, it is returned instead of raising StopIteration."); @@ -1670,8 +1678,9 @@ builtin_iter(PyObject *self, PyObject *const *args, Py_ssize_t nargs) } PyDoc_STRVAR(iter_doc, -"iter(iterable) -> iterator\n\ -iter(callable, sentinel) -> iterator\n\ +"iter(iterable, /)\n\ +(callable, sentinel)\n\ +--\n\ \n\ Get an iterator from an object. In the first form, the argument must\n\ supply its own iterator, or be a sequence.\n\ @@ -1697,22 +1706,24 @@ builtin_aiter(PyObject *module, PyObject *async_iterable) PyObject *PyAnextAwaitable_New(PyObject *, PyObject *); /*[clinic input] +@text_signature "(aiterator, /)" +@text_signature "(aiterator, default, /)" anext as builtin_anext aiterator: object default: object = NULL / -async anext(aiterator[, default]) +Return the next item from the async iterator. -Return the next item from the async iterator. If default is given and the async -iterator is exhausted, it is returned instead of raising StopAsyncIteration. +If default is given and the async iterator is exhausted, +it is returned instead of raising StopAsyncIteration. [clinic start generated code]*/ static PyObject * builtin_anext_impl(PyObject *module, PyObject *aiterator, PyObject *default_value) -/*[clinic end generated code: output=f02c060c163a81fa input=8f63f4f78590bb4c]*/ +/*[clinic end generated code: output=f02c060c163a81fa input=2215abe23cb47877]*/ { PyTypeObject *t; PyObject *awaitable; @@ -1905,8 +1916,10 @@ builtin_min(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *k } PyDoc_STRVAR(min_doc, -"min(iterable, *[, default=obj, key=func]) -> value\n\ -min(arg1, arg2, *args, *[, key=func]) -> value\n\ +"min(iterable, /, *, key=None)\n\ +(iterable, /, *, default, key=None)\n\ +(arg1, arg2, /, *args, key=None)\n\ +--\n\ \n\ With a single iterable argument, return its smallest item. The\n\ default keyword-only argument specifies an object to return if\n\ @@ -1922,8 +1935,10 @@ builtin_max(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *k } PyDoc_STRVAR(max_doc, -"max(iterable, *[, default=obj, key=func]) -> value\n\ -max(arg1, arg2, *args, *[, key=func]) -> value\n\ +"max(iterable, /, *, key=None)\n\ +(iterable, /, *, default, key=None)\n\ +(arg1, arg2, /, *args, key=None)\n\ +--\n\ \n\ With a single iterable argument, return its biggest item. The\n\ default keyword-only argument specifies an object to return if\n\ @@ -2497,7 +2512,9 @@ builtin_vars(PyObject *self, PyObject *args) } PyDoc_STRVAR(vars_doc, -"vars([object]) -> dictionary\n\ +"vars()\n\ +vars(object, /)\n\ +--\n\ \n\ Without arguments, equivalent to locals().\n\ With an argument, equivalent to object.__dict__."); diff --git a/Python/clinic/bltinmodule.c.h b/Python/clinic/bltinmodule.c.h index 3898f987cd61ea..00c7b69c204c18 100644 --- a/Python/clinic/bltinmodule.c.h +++ b/Python/clinic/bltinmodule.c.h @@ -690,13 +690,14 @@ PyDoc_STRVAR(builtin_aiter__doc__, {"aiter", (PyCFunction)builtin_aiter, METH_O, builtin_aiter__doc__}, PyDoc_STRVAR(builtin_anext__doc__, -"anext($module, aiterator, default=, /)\n" +"anext(aiterator, /)\n" +"(aiterator, default, /)\n" "--\n" "\n" -"async anext(aiterator[, default])\n" +"Return the next item from the async iterator.\n" "\n" -"Return the next item from the async iterator. If default is given and the async\n" -"iterator is exhausted, it is returned instead of raising StopAsyncIteration."); +"If default is given and the async iterator is exhausted,\n" +"it is returned instead of raising StopAsyncIteration."); #define BUILTIN_ANEXT_METHODDEF \ {"anext", _PyCFunction_CAST(builtin_anext), METH_FASTCALL, builtin_anext__doc__}, @@ -1193,4 +1194,4 @@ builtin_issubclass(PyObject *module, PyObject *const *args, Py_ssize_t nargs) exit: return return_value; } -/*[clinic end generated code: output=643a8d5f900e0c36 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=d50c558d44af3683 input=a9049054013a1b77]*/ diff --git a/Tools/clinic/libclinic/dsl_parser.py b/Tools/clinic/libclinic/dsl_parser.py index 4c739efe1066e4..37fa591ad183cd 100644 --- a/Tools/clinic/libclinic/dsl_parser.py +++ b/Tools/clinic/libclinic/dsl_parser.py @@ -453,8 +453,9 @@ def at_coexist(self) -> None: def at_text_signature(self, text_signature: str) -> None: if self.forced_text_signature: - fail("Called @text_signature twice!") - self.forced_text_signature = text_signature + self.forced_text_signature += '\n' + text_signature + else: + self.forced_text_signature = text_signature def parse(self, block: Block) -> None: self.reset() From 54db6d3d75844fe66b9d12e21a8d165ee243494d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 10 Apr 2024 16:27:58 +0300 Subject: [PATCH 2/9] Add tests and fix errors. --- Lib/inspect.py | 81 +++++- Lib/test/test_inspect/test_inspect.py | 365 +++++++++++++++++++++++++- 2 files changed, 427 insertions(+), 19 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 157bc6729e7466..58c4560a1ebcd1 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -160,7 +160,7 @@ import functools import builtins from keyword import iskeyword -from operator import attrgetter +from operator import attrgetter, or_ from collections import namedtuple, OrderedDict # Create constants for the compiler flags in Include/code.h @@ -2058,6 +2058,9 @@ def _signature_get_partial(wrapped_sig, partial, extra_args=()): on it. """ + if isinstance(wrapped_sig, MultiSignature): + return _multisignature_get_partial(wrapped_sig, partial, extra_args) + old_params = wrapped_sig.parameters new_params = OrderedDict(old_params.items()) @@ -2128,11 +2131,29 @@ def _signature_get_partial(wrapped_sig, partial, extra_args=()): return wrapped_sig.replace(parameters=new_params.values()) +def _multisignature_get_partial(wrapped_sig, partial, extra_args=()): + last_exc = None + signatures = [] + for sig in wrapped_sig: + try: + signatures.append(_signature_get_partial(sig, partial, extra_args)) + except (TypeError, ValueError) as e: + last_exc = e + if not signatures: + raise last_exc + if len(signatures) == 1: + return signatures[0] + return MultiSignature(signatures) + + def _signature_bound_method(sig): """Private helper to transform signatures for unbound functions to bound methods. """ + if isinstance(sig, MultiSignature): + return MultiSignature([_signature_bound_method(s) for s in sig]) + params = tuple(sig.parameters.values()) if not params or params[0].kind in (_VAR_KEYWORD, _KEYWORD_ONLY): @@ -2599,6 +2620,15 @@ def _signature_from_callable(obj, *, # First argument of the wrapped callable is `*args`, as in # `partialmethod(lambda *args)`. return sig + elif isinstance(sig, MultiSignature): + new_sigs = [] + for s in sig: + sig_params = tuple(s.parameters.values()) + assert (not sig_params or + first_wrapped_param is not sig_params[0]) + new_params = (first_wrapped_param,) + sig_params + new_sigs.append(s.replace(parameters=new_params)) + return MultiSignature(new_sigs) else: sig_params = tuple(sig.parameters.values()) assert (not sig_params or @@ -3405,9 +3435,18 @@ def return_annotation(self): 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_annotations = [] + for s in self._signatures: + ann = s.return_annotation + if ann != _empty: + if ann is None: + ann = type(ann) + elif isinstance(ann, str): + from typing import ForwardRef + ann = ForwardRef(ann) + return_annotations.append(ann) + self._return_annotation = (functools.reduce(or_, return_annotations) + if return_annotations else _empty) return self._return_annotation def replace(self): @@ -3432,14 +3471,34 @@ def __eq__(self, other): return self._signatures[0] == other return NotImplemented - def _bind(self, args, kwargs, *, partial=False): - """Private method. Don't use directly.""" - for i, s in enumerate(self._signatures): + def bind(self, /, *args, **kwargs): + last_exc = None + for s in self._signatures: + try: + return s.bind(*args, **kwargs) + except TypeError as e: + last_exc = e + raise last_exc + + def bind_partial(self, /, *args, **kwargs): + last_exc = None + bas = [] + for s in self._signatures: try: - return s._bind(args, kwargs, partial=partial) - except TypeError: - if i == len(self._signatures) - 1: - raise + bas.append(s.bind_partial(*args, **kwargs)) + except TypeError as e: + last_exc = e + if not bas: + raise last_exc + if len(bas) == 1: + return bas[0] + arguments = bas[0].arguments + for ba in bas: + if ba.arguments != arguments: + raise TypeError('ambiguous binding') + sig = (self if len(bas) == len(self._signatures) else + self.__class__([ba.signature for ba in bas])) + return BoundArguments(sig, arguments) def __reduce__(self): return type(self), (self._signatures,) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 8208040d1f228f..1cd3d4278c1331 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -19,6 +19,7 @@ import subprocess import time import types +import typing import tempfile import textwrap import unicodedata @@ -2692,19 +2693,27 @@ class MyParameter(inspect.Parameter): pass +def _conv_signature(sig): + return (tuple((param.name, + (... if param.default is param.empty else param.default), + (... if param.annotation is param.empty + else param.annotation), + str(param.kind).lower()) + for param in sig.parameters.values()), + (... if sig.return_annotation is sig.empty + else sig.return_annotation)) + class TestSignatureObject(unittest.TestCase): @staticmethod def signature(func, **kw): sig = inspect.signature(func, **kw) - return (tuple((param.name, - (... if param.default is param.empty else param.default), - (... if param.annotation is param.empty - else param.annotation), - str(param.kind).lower()) - for param in sig.parameters.values()), - (... if sig.return_annotation is sig.empty - else sig.return_annotation)) + return _conv_signature(sig) + + @staticmethod + def signatures(func, **kw): + sigs = inspect.signatures(func, **kw) + return [_conv_signature(sig) for sig in sigs] def test_signature_object(self): S = inspect.Signature @@ -2782,6 +2791,69 @@ def test2(pod=42, /): with self.assertRaisesRegex(ValueError, 'follows default argument'): S((pkd, pk)) + def test_multisignature_object(self): + def test(stop): + pass + def test2(start, stop, step=1): + pass + sig = inspect.signature(test) + sig2 = inspect.signature(test2) + + self.assertRaises(ValueError, inspect.MultiSignature, []) + + msig = inspect.MultiSignature([sig2]) + self.assertEqual(str(msig), '(start, stop, step=1)') + self.assertEqual(repr(msig), '') + self.assertEqual(list(msig), [sig2]) + self.assertEqual(msig, sig2) + self.assertEqual(sig2, msig) + self.assertEqual(hash(msig), hash(sig2)) + self.assertEqual(msig.parameters, sig2.parameters) + + msig = inspect.MultiSignature([sig, sig2]) + self.assertEqual(str(msig), '(stop)\n(start, stop, step=1)') + self.assertEqual(repr(msig), '') + self.assertEqual(list(msig), [sig, sig2]) + self.assertNotEqual(msig, sig) + self.assertNotEqual(sig, msig) + self.assertNotEqual(msig, sig2) + self.assertNotEqual(sig2, msig) + self.assertEqual(list(msig.parameters), ['stop', 'start', 'step']) + self.assertEqual(msig.parameters['stop'], sig.parameters['stop']) + self.assertEqual(msig.parameters['start'], sig2.parameters['start']) + self.assertEqual(msig.parameters['step'], sig2.parameters['step']) + self.assertEqual(msig.return_annotation, inspect.Signature.empty) + + msig2 = inspect.MultiSignature([inspect.signature(test), + inspect.signature(test2)]) + self.assertEqual(msig, msig2) + self.assertEqual(hash(msig), hash(msig2)) + + def test_multisignature_return_annotation(self): + def test(stop) -> 'x': + pass + def test2(start, stop) -> list: + pass + def test3(start, stop, step) -> list: + pass + def test4(*args): + pass + sig = inspect.signature(test) + sig2 = inspect.signature(test2) + sig3 = inspect.signature(test3) + sig4 = inspect.signature(test4) + + msig = inspect.MultiSignature([sig2, sig3]) + self.assertEqual(msig.return_annotation, list) + + msig = inspect.MultiSignature([sig, sig2, sig3]) + self.assertEqual(msig.return_annotation, + typing.Union[typing.ForwardRef('x'), list]) + + msig = inspect.MultiSignature([sig, sig2, sig3, sig4]) + self.assertEqual(msig.return_annotation, + typing.Union[typing.ForwardRef('x'), list]) + def test_signature_object_pickle(self): def foo(a, b, *, c:1={}, **kw) -> {42:'ham'}: pass foo_partial = functools.partial(foo, a=1) @@ -2810,6 +2882,19 @@ def foo(a, b, *, c:1={}, **kw) -> {42:'ham'}: pass self.assertTrue(isinstance(sig_pickled.parameters['z'], MyParameter)) + def test_multisignature_object_pickle(self): + def test(stop): + pass + def test2(start, stop, step=1): + pass + sig = inspect.signature(test) + sig2 = inspect.signature(test2) + msig = inspect.MultiSignature([sig, sig2]) + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(proto=proto): + msig2 = pickle.loads(pickle.dumps(msig, proto)) + self.assertEqual(msig2, msig) + def test_signature_immutability(self): def test(a): pass @@ -3408,6 +3493,77 @@ def foo(a, b, /, c, d, **kwargs): ('kwargs', ..., ..., 'var_keyword')), ...)) + def test_multisignature_on_partial(self): + from functools import partial + + def test(stop): + pass + def test2(start, stop, step=1): + pass + sig = inspect.signature(test) + sig2 = inspect.signature(test2) + test.__signature__ = inspect.MultiSignature([sig, sig2]) + + self.assertEqual(str(inspect.signature(partial(test))), + '(stop)\n(start, stop, step=1)') + self.assertEqual(self.signature(partial(test)), + ((('stop', ..., ..., 'positional_or_keyword'), + ('start', ..., ..., 'positional_or_keyword'), + ('step', 1, ..., 'positional_or_keyword')), + ...)) + self.assertEqual(self.signatures(partial(test)), [ + ((('stop', ..., ..., 'positional_or_keyword'),), ...), + ((('start', ..., ..., 'positional_or_keyword'), + ('stop', ..., ..., 'positional_or_keyword'), + ('step', 1, ..., 'positional_or_keyword')), + ...)]) + + self.assertEqual(str(inspect.signature(partial(test, 5))), + '()\n(stop, step=1)') + self.assertEqual(self.signature(partial(test, 5)), + ((('stop', ..., ..., 'positional_or_keyword'), + ('step', 1, ..., 'positional_or_keyword')), + ...)) + self.assertEqual(self.signatures(partial(test, 5)), [ + ((), ...), + ((('stop', ..., ..., 'positional_or_keyword'), + ('step', 1, ..., 'positional_or_keyword')), + ...)]) + + self.assertEqual(str(inspect.signature(partial(test, 5, 10))), + '(step=1)') + self.assertEqual(self.signature(partial(test, 5, 10)), + ((('step', 1, ..., 'positional_or_keyword'),), + ...)) + self.assertEqual(self.signatures(partial(test, 5, 10)), [ + ((('step', 1, ..., 'positional_or_keyword'),), + ...)]) + + self.assertEqual(str(inspect.signature(partial(test, stop=5))), + '(*, stop=5)\n(start, *, stop=5, step=1)') + self.assertEqual(self.signature(partial(test, stop=5)), + ((('stop', 5, ..., 'keyword_only'), + ('start', ..., ..., 'positional_or_keyword'), + ('step', 1, ..., 'keyword_only')), + ...)) + self.assertEqual(self.signatures(partial(test, stop=5)), [ + ((('stop', 5, ..., 'keyword_only'),), ...), + ((('start', ..., ..., 'positional_or_keyword'), + ('stop', 5, ..., 'keyword_only'), + ('step', 1, ..., 'keyword_only')), + ...)]) + + self.assertEqual(str(inspect.signature(partial(partial(test, stop=5), 2))), + '(*, stop=5, step=1)') + self.assertEqual(self.signature(partial(partial(test, stop=5), 2)), + ((('stop', 5, ..., 'keyword_only'), + ('step', 1, ..., 'keyword_only')), + ...)) + self.assertEqual(self.signatures(partial(partial(test, stop=5), 2)), [ + ((('stop', 5, ..., 'keyword_only'), + ('step', 1, ..., 'keyword_only')), + ...)]) + def test_signature_on_partialmethod(self): from functools import partialmethod @@ -3445,6 +3601,77 @@ def test(self: 'anno', x): ((('self', ..., 'anno', 'positional_or_keyword'),), ...)) + def test_multisignature_on_partialmethod(self): + from functools import partialmethod + + class A: + def test(self, stop): + pass + def test2(self, start, stop, step=1): + pass + sig = inspect.signature(test) + sig2 = inspect.signature(test2) + test.__signature__ = inspect.MultiSignature([sig, sig2]) + + part1 = partialmethod(test, 5) + part2 = partialmethod(test, stop=5) + + self.assertEqual(str(inspect.signature(A.part1)), + '(self)\n(self, stop, step=1)') + self.assertEqual(str(inspect.signature(A().part1)), + '()\n(stop, step=1)') + self.assertEqual(self.signature(A.part1), + ((('self', ..., ..., 'positional_or_keyword'), + ('stop', ..., ..., 'positional_or_keyword'), + ('step', 1, ..., 'positional_or_keyword')), + ...)) + self.assertEqual(self.signature(A().part1), + ((('stop', ..., ..., 'positional_or_keyword'), + ('step', 1, ..., 'positional_or_keyword')), + ...)) + self.assertEqual(self.signatures(A.part1), [ + ((('self', ..., ..., 'positional_or_keyword'),), ...), + ((('self', ..., ..., 'positional_or_keyword'), + ('stop', ..., ..., 'positional_or_keyword'), + ('step', 1, ..., 'positional_or_keyword')), + ...)]) + self.assertEqual(self.signatures(A().part1), [ + ((), ...), + ((('stop', ..., ..., 'positional_or_keyword'), + ('step', 1, ..., 'positional_or_keyword')), + ...)]) + + self.assertEqual(str(inspect.signature(A.part2)), + '(self, *, stop=5)\n(self, start, *, stop=5, step=1)') + self.assertEqual(str(inspect.signature(A().part2)), + '(*, stop=5)\n(start, *, stop=5, step=1)') + self.assertEqual(self.signature(A.part2), + ((('self', ..., ..., 'positional_or_keyword'), + ('stop', 5, ..., 'keyword_only'), + ('start', ..., ..., 'positional_or_keyword'), + ('step', 1, ..., 'keyword_only')), + ...)) + self.assertEqual(self.signature(A().part2), + ((('stop', 5, ..., 'keyword_only'), + ('start', ..., ..., 'positional_or_keyword'), + ('step', 1, ..., 'keyword_only')), + ...)) + self.assertEqual(self.signatures(A.part2), [ + ((('self', ..., ..., 'positional_or_keyword'), + ('stop', 5, ..., 'keyword_only'),), + ...), + ((('self', ..., ..., 'positional_or_keyword'), + ('start', ..., ..., 'positional_or_keyword'), + ('stop', 5, ..., 'keyword_only'), + ('step', 1, ..., 'keyword_only')), + ...)]) + self.assertEqual(self.signatures(A().part2), [ + ((('stop', 5, ..., 'keyword_only'),), ...), + ((('start', ..., ..., 'positional_or_keyword'), + ('stop', 5, ..., 'keyword_only'), + ('step', 1, ..., 'keyword_only')), + ...)]) + def test_signature_on_fake_partialmethod(self): def foo(a): pass foo.__partialmethod__ = 'spam' @@ -5166,6 +5393,128 @@ def foo(a): pass ba = inspect.signature(foo).bind(1) self.assertIs(type(ba.arguments), dict) + def test_multisignature_bind(self): + def test(stop): + pass + def test2(start, stop, step=1): + pass + sig = inspect.signature(test) + sig2 = inspect.signature(test2) + msig = inspect.MultiSignature([sig, sig2]) + + ba = msig.bind(5) + self.assertEqual(ba.args, (5,)) + self.assertEqual(ba.kwargs, {}) + self.assertEqual(ba.arguments, {'stop': 5}) + self.assertEqual(ba.signature, sig) + + ba = msig.bind(5, 10) + self.assertEqual(ba.args, (5, 10)) + self.assertEqual(ba.kwargs, {}) + self.assertEqual(ba.arguments, {'start': 5, 'stop': 10}) + self.assertEqual(ba.signature, sig2) + + ba = msig.bind(5, 10, 2) + self.assertEqual(ba.args, (5, 10, 2)) + self.assertEqual(ba.kwargs, {}) + self.assertEqual(ba.arguments, {'start': 5, 'stop': 10, 'step': 2}) + self.assertEqual(ba.signature, sig2) + + ba = msig.bind(stop=5) + self.assertEqual(ba.args, (5,)) + self.assertEqual(ba.kwargs, {}) + self.assertEqual(ba.arguments, {'stop': 5}) + self.assertEqual(ba.signature, sig) + + ba = msig.bind(start=5, stop=10) + self.assertEqual(ba.args, (5, 10)) + self.assertEqual(ba.kwargs, {}) + self.assertEqual(ba.arguments, {'start': 5, 'stop': 10}) + self.assertEqual(ba.signature, sig2) + + ba = msig.bind(start=5, stop=10, step=2) + self.assertEqual(ba.args, (5, 10, 2)) + self.assertEqual(ba.kwargs, {}) + self.assertEqual(ba.arguments, {'start': 5, 'stop': 10, 'step': 2}) + self.assertEqual(ba.signature, sig2) + + self.assertRaises(TypeError, msig.bind) + self.assertRaises(TypeError, msig.bind, start=5) + self.assertRaises(TypeError, msig.bind, step=2) + self.assertRaises(TypeError, msig.bind, 5, 10, 2, 3) + + def test_multisignature_bind_partial(self): + def test(stop): + pass + def test2(start, stop, step=1): + pass + sig = inspect.signature(test) + sig2 = inspect.signature(test2) + msig = inspect.MultiSignature([sig, sig2]) + + ba = msig.bind_partial(5, 10) + self.assertEqual(ba.args, (5, 10)) + self.assertEqual(ba.kwargs, {}) + self.assertEqual(ba.arguments, {'start': 5, 'stop': 10}) + self.assertEqual(ba.signature, sig2) + + ba = msig.bind_partial(5, 10, 2) + self.assertEqual(ba.args, (5, 10, 2)) + self.assertEqual(ba.kwargs, {}) + self.assertEqual(ba.arguments, {'start': 5, 'stop': 10, 'step': 2}) + self.assertEqual(ba.signature, sig2) + + ba = msig.bind_partial(stop=5) + self.assertEqual(ba.args, (5,)) + self.assertEqual(ba.kwargs, {}) + self.assertEqual(ba.arguments, {'stop': 5}) + self.assertEqual(ba.signature, msig) + + ba = msig.bind_partial(start=5, stop=10) + self.assertEqual(ba.args, (5, 10)) + self.assertEqual(ba.kwargs, {}) + self.assertEqual(ba.arguments, {'start': 5, 'stop': 10}) + self.assertEqual(ba.signature, sig2) + + ba = msig.bind_partial(start=5, stop=10, step=2) + self.assertEqual(ba.args, (5, 10, 2)) + self.assertEqual(ba.kwargs, {}) + self.assertEqual(ba.arguments, {'start': 5, 'stop': 10, 'step': 2}) + self.assertEqual(ba.signature, sig2) + + ba = msig.bind_partial() + self.assertEqual(ba.args, ()) + self.assertEqual(ba.kwargs, {}) + self.assertEqual(ba.arguments, {}) + self.assertEqual(ba.signature, msig) + ba.apply_defaults() + self.assertEqual(ba.args, ()) + self.assertEqual(ba.kwargs, {'step': 1}) + self.assertEqual(ba.arguments, {'step': 1}) + + ba = msig.bind_partial(start=5) + self.assertEqual(ba.args, (5,)) + self.assertEqual(ba.kwargs, {}) + self.assertEqual(ba.arguments, {'start': 5}) + self.assertEqual(ba.signature, sig2) + ba.apply_defaults() + self.assertEqual(ba.args, (5,)) + self.assertEqual(ba.kwargs, {'step': 1}) + self.assertEqual(ba.arguments, {'start': 5, 'step': 1}) + + ba = msig.bind_partial(step=2) + self.assertEqual(ba.args, ()) + self.assertEqual(ba.kwargs, {'step': 2}) + self.assertEqual(ba.arguments, {'step': 2}) + ba.apply_defaults() + self.assertEqual(ba.args, ()) + self.assertEqual(ba.kwargs, {'step': 2}) + self.assertEqual(ba.arguments, {'step': 2}) + + self.assertRaises(TypeError, msig.bind_partial, 5) + self.assertRaises(TypeError, msig.bind_partial, 5, 10, 2, 3) + + class TestSignaturePrivateHelpers(unittest.TestCase): def _strip_non_python_syntax(self, input, clean_signature, self_parameter): self.assertEqual(list(inspect._signature_split(input)), From 9bbde5f372ecdd347fcfa3178d1ff4f01e948177 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 10 Apr 2024 16:41:36 +0300 Subject: [PATCH 3/9] Fix signatures of hex() methods. --- Objects/bytearrayobject.c | 6 +++--- Objects/bytesobject.c | 6 +++--- Objects/clinic/bytearrayobject.c.h | 6 +++--- Objects/clinic/bytesobject.c.h | 6 +++--- Objects/clinic/memoryobject.c.h | 6 +++--- Objects/memoryobject.c | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c index 14754a4caa4d1d..ddd1be3a7fe12f 100644 --- a/Objects/bytearrayobject.c +++ b/Objects/bytearrayobject.c @@ -2111,8 +2111,8 @@ bytearray_fromhex_impl(PyTypeObject *type, PyObject *string) } /*[clinic input] -@text_signature "($self, *, bytes_per_sep=1, /)" -@text_signature "($self, sep, bytes_per_sep=1, /)" +@text_signature "($self, /, *, bytes_per_sep=1)" +@text_signature "($self, /, sep, bytes_per_sep=1)" bytearray.hex sep: object = NULL @@ -2137,7 +2137,7 @@ Create a string of hexadecimal numbers from a bytearray object. static PyObject * bytearray_hex_impl(PyByteArrayObject *self, PyObject *sep, int bytes_per_sep) -/*[clinic end generated code: output=29c4e5ef72c565a0 input=719afde525c65958]*/ +/*[clinic end generated code: output=29c4e5ef72c565a0 input=4d67f552fc1c4eee]*/ { char* argbuf = PyByteArray_AS_STRING(self); Py_ssize_t arglen = PyByteArray_GET_SIZE(self); diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c index 0120ca65629e7a..7a61be2d9737f8 100644 --- a/Objects/bytesobject.c +++ b/Objects/bytesobject.c @@ -2479,8 +2479,8 @@ _PyBytes_FromHex(PyObject *string, int use_bytearray) } /*[clinic input] -@text_signature "($self, *, bytes_per_sep=1, /)" -@text_signature "($self, sep, bytes_per_sep=1, /)" +@text_signature "($self, /, *, bytes_per_sep=1)" +@text_signature "($self, /, sep, bytes_per_sep=1)" bytes.hex sep: object = NULL @@ -2505,7 +2505,7 @@ Create a string of hexadecimal numbers from a bytes object. static PyObject * bytes_hex_impl(PyBytesObject *self, PyObject *sep, int bytes_per_sep) -/*[clinic end generated code: output=1f134da504064139 input=9c31b616d1648899]*/ +/*[clinic end generated code: output=1f134da504064139 input=a896de1af0991eda]*/ { const char *argbuf = PyBytes_AS_STRING(self); Py_ssize_t arglen = PyBytes_GET_SIZE(self); diff --git a/Objects/clinic/bytearrayobject.c.h b/Objects/clinic/bytearrayobject.c.h index f2e54120dbc005..1d04f190f0dd54 100644 --- a/Objects/clinic/bytearrayobject.c.h +++ b/Objects/clinic/bytearrayobject.c.h @@ -1223,8 +1223,8 @@ bytearray_fromhex(PyTypeObject *type, PyObject *arg) } PyDoc_STRVAR(bytearray_hex__doc__, -"hex($self, *, bytes_per_sep=1, /)\n" -"($self, sep, bytes_per_sep=1, /)\n" +"hex($self, /, *, bytes_per_sep=1)\n" +"($self, /, sep, bytes_per_sep=1)\n" "--\n" "\n" "Create a string of hexadecimal numbers from a bytearray object.\n" @@ -1380,4 +1380,4 @@ bytearray_sizeof(PyByteArrayObject *self, PyObject *Py_UNUSED(ignored)) { return bytearray_sizeof_impl(self); } -/*[clinic end generated code: output=d73d5d353f2847c8 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=5113e17d22c7d19d input=a9049054013a1b77]*/ diff --git a/Objects/clinic/bytesobject.c.h b/Objects/clinic/bytesobject.c.h index 25d31f1f363bf5..df33017a04be86 100644 --- a/Objects/clinic/bytesobject.c.h +++ b/Objects/clinic/bytesobject.c.h @@ -954,8 +954,8 @@ bytes_fromhex(PyTypeObject *type, PyObject *arg) } PyDoc_STRVAR(bytes_hex__doc__, -"hex($self, *, bytes_per_sep=1, /)\n" -"($self, sep, bytes_per_sep=1, /)\n" +"hex($self, /, *, bytes_per_sep=1)\n" +"($self, /, sep, bytes_per_sep=1)\n" "--\n" "\n" "Create a string of hexadecimal numbers from a bytes object.\n" @@ -1148,4 +1148,4 @@ bytes_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) exit: return return_value; } -/*[clinic end generated code: output=6807468568961267 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=aa9a6f967010d6ea input=a9049054013a1b77]*/ diff --git a/Objects/clinic/memoryobject.c.h b/Objects/clinic/memoryobject.c.h index 27add31ec650e2..d560d920c74317 100644 --- a/Objects/clinic/memoryobject.c.h +++ b/Objects/clinic/memoryobject.c.h @@ -328,8 +328,8 @@ memoryview_tobytes(PyMemoryViewObject *self, PyObject *const *args, Py_ssize_t n } PyDoc_STRVAR(memoryview_hex__doc__, -"hex($self, *, bytes_per_sep=1, /)\n" -"($self, sep, bytes_per_sep=1, /)\n" +"hex($self, /, *, bytes_per_sep=1)\n" +"($self, /, sep, bytes_per_sep=1)\n" "--\n" "\n" "Return the data in the buffer as a str of hexadecimal numbers.\n" @@ -415,4 +415,4 @@ memoryview_hex(PyMemoryViewObject *self, PyObject *const *args, Py_ssize_t nargs exit: return return_value; } -/*[clinic end generated code: output=7b6a89244a5f026c input=a9049054013a1b77]*/ +/*[clinic end generated code: output=2a03415900028ba0 input=a9049054013a1b77]*/ diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index b6f0df95e6a92f..73f0cc7fb1c731 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -2299,8 +2299,8 @@ memoryview_tobytes_impl(PyMemoryViewObject *self, const char *order) } /*[clinic input] -@text_signature "($self, *, bytes_per_sep=1, /)" -@text_signature "($self, sep, bytes_per_sep=1, /)" +@text_signature "($self, /, *, bytes_per_sep=1)" +@text_signature "($self, /, sep, bytes_per_sep=1)" memoryview.hex sep: object = NULL @@ -2326,7 +2326,7 @@ Return the data in the buffer as a str of hexadecimal numbers. static PyObject * memoryview_hex_impl(PyMemoryViewObject *self, PyObject *sep, int bytes_per_sep) -/*[clinic end generated code: output=430ca760f94f3ca7 input=10129bce3c852fc3]*/ +/*[clinic end generated code: output=430ca760f94f3ca7 input=fdf8251033d71260]*/ { Py_buffer *src = VIEW_ADDR(self); PyObject *bytes; From 5c9ddb946e34e0dfd764c2f1129884a837cb06f7 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 11 Apr 2024 09:06:20 +0300 Subject: [PATCH 4/9] Refactor support of partial and partialmethod. --- Lib/inspect.py | 117 +++++--------- Lib/test/test_inspect/test_inspect.py | 211 ++++++++++++++++++++++---- 2 files changed, 224 insertions(+), 104 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 58c4560a1ebcd1..b82f39dd5d66aa 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2052,31 +2052,13 @@ def _signature_get_user_defined_method(cls, method_name): return meth -def _signature_get_partial(wrapped_sig, partial, extra_args=()): - """Private helper to calculate how 'wrapped_sig' signature will - look like after applying a 'functools.partial' object (or alike) - on it. - """ - - if isinstance(wrapped_sig, MultiSignature): - return _multisignature_get_partial(wrapped_sig, partial, extra_args) - - old_params = wrapped_sig.parameters - new_params = OrderedDict(old_params.items()) - - partial_args = partial.args or () - partial_keywords = partial.keywords or {} - - if extra_args: - partial_args = extra_args + partial_args - - try: - ba = wrapped_sig.bind_partial(*partial_args, **partial_keywords) - except TypeError as ex: - msg = 'partial object {!r} has incorrect arguments'.format(partial) - raise ValueError(msg) from ex - +def _signature_partial(sig, args, kwargs): + if isinstance(sig, MultiSignature): + return _multisignature_partial(sig, args, kwargs) + ba = sig.bind_partial(*args, **kwargs) + old_params = sig.parameters + new_params = OrderedDict(old_params) transform_to_kwonly = False for param_name, param in old_params.items(): try: @@ -2091,7 +2073,7 @@ def _signature_get_partial(wrapped_sig, partial, extra_args=()): continue if param.kind is _POSITIONAL_OR_KEYWORD: - if param_name in partial_keywords: + if param_name in kwargs: # This means that this parameter, and all parameters # after it should be keyword-only (and var-positional # should be removed). Here's why. Consider the following @@ -2128,22 +2110,38 @@ def _signature_get_partial(wrapped_sig, partial, extra_args=()): elif param.kind is _VAR_POSITIONAL: new_params.pop(param.name) - return wrapped_sig.replace(parameters=new_params.values()) + return sig.replace(parameters=new_params.values()) -def _multisignature_get_partial(wrapped_sig, partial, extra_args=()): +def _multisignature_partial(sig, args, kwargs, partial=_signature_partial): last_exc = None signatures = [] - for sig in wrapped_sig: + for s in sig: try: - signatures.append(_signature_get_partial(sig, partial, extra_args)) - except (TypeError, ValueError) as e: + signatures.append(partial(s, args, kwargs)) + except TypeError as e: last_exc = e if not signatures: raise last_exc if len(signatures) == 1: return signatures[0] - return MultiSignature(signatures) + return sig.__class__(signatures) + + +def _signature_partialmethod(sig, args, kwargs): + if isinstance(sig, MultiSignature): + return _multisignature_partial(sig, args, kwargs, _signature_partialmethod) + + new_sig = _signature_partial(sig, (None, *args), kwargs) + first_wrapped_param = tuple(sig.parameters.values())[0] + if first_wrapped_param.kind is Parameter.VAR_POSITIONAL: + # First argument of the wrapped callable is `*args`, as in + # `partialmethod(lambda *args)`. + return new_sig + sig_params = tuple(new_sig.parameters.values()) + assert not sig_params or first_wrapped_param is not sig_params[0] + new_params = (first_wrapped_param, *sig_params) + return new_sig.replace(parameters=new_params) def _signature_bound_method(sig): @@ -2151,29 +2149,11 @@ def _signature_bound_method(sig): functions to bound methods. """ - if isinstance(sig, MultiSignature): - return MultiSignature([_signature_bound_method(s) for s in sig]) - - params = tuple(sig.parameters.values()) - - if not params or params[0].kind in (_VAR_KEYWORD, _KEYWORD_ONLY): + try: + return _signature_partial(sig, (None,), {}) + except TypeError: raise ValueError('invalid method signature') - kind = params[0].kind - if kind in (_POSITIONAL_OR_KEYWORD, _POSITIONAL_ONLY): - # Drop first parameter: - # '(p1, p2[, ...])' -> '(p2[, ...])' - params = params[1:] - else: - if kind is not _VAR_POSITIONAL: - # Unless we add a new parameter type we never - # get here - raise ValueError('invalid argument type') - # It's a var-positional parameter. - # Do nothing. '(*args[, ...])' -> '(*args[, ...])' - - return sig.replace(parameters=params) - def _signature_is_builtin(obj): """Private helper to test if `obj` is a callable that might @@ -2613,28 +2593,11 @@ def _signature_from_callable(obj, *, # automatically (as for boundmethods) wrapped_sig = _get_signature_of(partialmethod.func) - - sig = _signature_get_partial(wrapped_sig, partialmethod, (None,)) - first_wrapped_param = tuple(wrapped_sig.parameters.values())[0] - if first_wrapped_param.kind is Parameter.VAR_POSITIONAL: - # First argument of the wrapped callable is `*args`, as in - # `partialmethod(lambda *args)`. - return sig - elif isinstance(sig, MultiSignature): - new_sigs = [] - for s in sig: - sig_params = tuple(s.parameters.values()) - assert (not sig_params or - first_wrapped_param is not sig_params[0]) - new_params = (first_wrapped_param,) + sig_params - new_sigs.append(s.replace(parameters=new_params)) - return MultiSignature(new_sigs) - else: - sig_params = tuple(sig.parameters.values()) - assert (not sig_params or - first_wrapped_param is not sig_params[0]) - new_params = (first_wrapped_param,) + sig_params - return sig.replace(parameters=new_params) + try: + return _signature_partialmethod(wrapped_sig, partialmethod.args, partialmethod.keywords) + except TypeError as ex: + msg = f'partial object {partialmethod!r} has incorrect arguments' + raise ValueError(msg) from ex if isfunction(obj) or _signature_is_functionlike(obj): # If it's a pure Python function, or an object that is duck type @@ -2649,7 +2612,11 @@ def _signature_from_callable(obj, *, if isinstance(obj, functools.partial): wrapped_sig = _get_signature_of(obj.func) - return _signature_get_partial(wrapped_sig, obj) + try: + return _signature_partial(wrapped_sig, obj.args, obj.keywords) + except TypeError as ex: + msg = f'partial object {obj!r} has incorrect arguments' + raise ValueError(msg) from ex if isinstance(obj, type): # obj is a class or a metaclass diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 1cd3d4278c1331..621d8d970235a2 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -2830,18 +2830,18 @@ def test2(start, stop, step=1): self.assertEqual(hash(msig), hash(msig2)) def test_multisignature_return_annotation(self): - def test(stop) -> 'x': + @inspect.signature + def sig(stop) -> 'x': pass - def test2(start, stop) -> list: + @inspect.signature + def sig2(start, stop) -> list: pass - def test3(start, stop, step) -> list: + @inspect.signature + def sig3(start, stop, step) -> list: pass - def test4(*args): + @inspect.signature + def sig4(*args): pass - sig = inspect.signature(test) - sig2 = inspect.signature(test2) - sig3 = inspect.signature(test3) - sig4 = inspect.signature(test4) msig = inspect.MultiSignature([sig2, sig3]) self.assertEqual(msig.return_annotation, list) @@ -2883,12 +2883,12 @@ def foo(a, b, *, c:1={}, **kw) -> {42:'ham'}: pass MyParameter)) def test_multisignature_object_pickle(self): - def test(stop): + @inspect.signature + def sig(stop): pass - def test2(start, stop, step=1): + @inspect.signature + def sig2(start, stop, step=1): pass - sig = inspect.signature(test) - sig2 = inspect.signature(test2) msig = inspect.MultiSignature([sig, sig2]) for proto in range(pickle.HIGHEST_PROTOCOL + 1): with self.subTest(proto=proto): @@ -3496,12 +3496,14 @@ def foo(a, b, /, c, d, **kwargs): def test_multisignature_on_partial(self): from functools import partial - def test(stop): + @inspect.signature + def sig(stop): pass - def test2(start, stop, step=1): + @inspect.signature + def sig2(start, stop, step=1): + pass + def test(*args, **kwargs): pass - sig = inspect.signature(test) - sig2 = inspect.signature(test2) test.__signature__ = inspect.MultiSignature([sig, sig2]) self.assertEqual(str(inspect.signature(partial(test))), @@ -3605,13 +3607,16 @@ def test_multisignature_on_partialmethod(self): from functools import partialmethod class A: - def test(self, stop): + @inspect.signature + def sig1(self, stop): pass - def test2(self, start, stop, step=1): + @inspect.signature + def sig2(self, start, stop, step=1): pass - sig = inspect.signature(test) - sig2 = inspect.signature(test2) - test.__signature__ = inspect.MultiSignature([sig, sig2]) + + def test(*args, **kwargs): + pass + test.__signature__ = inspect.MultiSignature([sig1, sig2]) part1 = partialmethod(test, 5) part2 = partialmethod(test, stop=5) @@ -3672,6 +3677,154 @@ def test2(self, start, stop, step=1): ('step', 1, ..., 'keyword_only')), ...)]) + def test_multisignature_on_partialmethod_different_self(self): + from functools import partialmethod + + class A: + @inspect.signature + def sig1(self, stop): + pass + @inspect.signature + def sig2(self2, start, stop, step=1): + pass + + def test(*args, **kwargs): + pass + test.__signature__ = inspect.MultiSignature([sig1, sig2]) + + part = partialmethod(test, 5) + + self.assertEqual(str(inspect.signature(A.part)), + '(self)\n(self2, stop, step=1)') + self.assertEqual(str(inspect.signature(A().part)), + '()\n(stop, step=1)') + self.assertEqual(self.signature(A.part), + ((('self', ..., ..., 'positional_or_keyword'), + ('self2', ..., ..., 'positional_or_keyword'), + ('stop', ..., ..., 'positional_or_keyword'), + ('step', 1, ..., 'positional_or_keyword')), + ...)) + self.assertEqual(self.signature(A().part), + ((('stop', ..., ..., 'positional_or_keyword'), + ('step', 1, ..., 'positional_or_keyword')), + ...)) + self.assertEqual(self.signatures(A.part), [ + ((('self', ..., ..., 'positional_or_keyword'),), ...), + ((('self2', ..., ..., 'positional_or_keyword'), + ('stop', ..., ..., 'positional_or_keyword'), + ('step', 1, ..., 'positional_or_keyword')), + ...)]) + self.assertEqual(self.signatures(A().part), [ + ((), ...), + ((('stop', ..., ..., 'positional_or_keyword'), + ('step', 1, ..., 'positional_or_keyword')), + ...)]) + + def test_multisignature_on_partialmethod_no_self(self): + from functools import partialmethod + + class A: + @inspect.signature + def sig1(self, stop): + pass + @inspect.signature + def sig2(*, step=1): + pass + + def test(*args, **kwargs): + pass + test.__signature__ = inspect.MultiSignature([sig1, sig2]) + + part = partialmethod(test) + + self.assertEqual(str(inspect.signature(A.part)), + '(self, stop)') + self.assertEqual(str(inspect.signature(A().part)), + '(stop)') + self.assertEqual(self.signature(A.part), + ((('self', ..., ..., 'positional_or_keyword'), + ('stop', ..., ..., 'positional_or_keyword')), + ...)) + self.assertEqual(self.signature(A().part), + ((('stop', ..., ..., 'positional_or_keyword'),), ...)) + self.assertEqual(self.signatures(A.part), [ + ((('self', ..., ..., 'positional_or_keyword'), + ('stop', ..., ..., 'positional_or_keyword')), + ...)]) + self.assertEqual(self.signatures(A().part), [ + ((('stop', ..., ..., 'positional_or_keyword'),), ...)]) + + def test_multisignature_on_partialmethod_varpositional_self(self): + from functools import partialmethod + + class A: + @inspect.signature + def sig1(self, stop): + pass + @inspect.signature + def sig2(*args, step=1): + pass + + def test1(*args, **kwargs): + pass + test1.__signature__ = inspect.MultiSignature([sig1, sig2]) + + part1 = partialmethod(test1, 5) + + def test2(*args, **kwargs): + pass + test2.__signature__ = inspect.MultiSignature([sig2, sig1]) + + part2 = partialmethod(test2, 5) + + self.assertEqual(str(inspect.signature(A.part1)), + '(self)\n(*args, step=1)') + self.assertEqual(str(inspect.signature(A().part1)), + '()\n(*args, step=1)') + self.assertEqual(self.signature(A.part1), + ((('self', ..., ..., 'positional_or_keyword'), + ('args', ..., ..., 'var_positional'), + ('step', 1, ..., 'keyword_only')), + ...)) + self.assertEqual(self.signature(A().part1), + ((('args', ..., ..., 'var_positional'), + ('step', 1, ..., 'keyword_only')), + ...)) + self.assertEqual(self.signatures(A.part1), [ + ((('self', ..., ..., 'positional_or_keyword'),), ...), + ((('args', ..., ..., 'var_positional'), + ('step', 1, ..., 'keyword_only')), + ...)]) + self.assertEqual(self.signatures(A().part1), [ + ((), ...), + ((('args', ..., ..., 'var_positional'), + ('step', 1, ..., 'keyword_only')), + ...)]) + + self.assertEqual(str(inspect.signature(A.part2)), + '(*args, step=1)\n(self)') + self.assertEqual(str(inspect.signature(A().part2)), + '(*args, step=1)\n()') + self.assertEqual(self.signature(A.part2), + ((('args', ..., ..., 'var_positional'), + ('step', 1, ..., 'keyword_only'), + ('self', ..., ..., 'positional_or_keyword')), + ...)) + self.assertEqual(self.signature(A().part2), + ((('args', ..., ..., 'var_positional'), + ('step', 1, ..., 'keyword_only')), + ...)) + self.assertEqual(self.signatures(A.part2), [ + ((('args', ..., ..., 'var_positional'), + ('step', 1, ..., 'keyword_only')), + ...), + ((('self', ..., ..., 'positional_or_keyword'),), ...)]) + self.assertEqual(self.signatures(A().part2), [ + ((('args', ..., ..., 'var_positional'), + ('step', 1, ..., 'keyword_only')), + ...), + ((), ...)]) + def test_signature_on_fake_partialmethod(self): def foo(a): pass foo.__partialmethod__ = 'spam' @@ -5394,12 +5547,12 @@ def foo(a): pass self.assertIs(type(ba.arguments), dict) def test_multisignature_bind(self): - def test(stop): + @inspect.signature + def sig(stop): pass - def test2(start, stop, step=1): + @inspect.signature + def sig2(start, stop, step=1): pass - sig = inspect.signature(test) - sig2 = inspect.signature(test2) msig = inspect.MultiSignature([sig, sig2]) ba = msig.bind(5) @@ -5444,12 +5597,12 @@ def test2(start, stop, step=1): self.assertRaises(TypeError, msig.bind, 5, 10, 2, 3) def test_multisignature_bind_partial(self): - def test(stop): + @inspect.signature + def sig(stop): pass - def test2(start, stop, step=1): + @inspect.signature + def sig2(start, stop, step=1): pass - sig = inspect.signature(test) - sig2 = inspect.signature(test2) msig = inspect.MultiSignature([sig, sig2]) ba = msig.bind_partial(5, 10) From 5e7ca6d741dd0e4afb354298eb3f5c4743965f95 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 11 Apr 2024 09:47:52 +0300 Subject: [PATCH 5/9] Update Lib/inspect.py --- Lib/inspect.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index b82f39dd5d66aa..588f2932b84d71 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -3434,8 +3434,8 @@ def __eq__(self, 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 + if isinstance(other, Signature): + return len(self._signatures) == 1 and self._signatures[0] == other return NotImplemented def bind(self, /, *args, **kwargs): From 53bc04eabcadaa1497f6fd98faa693550b48e59c Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 11 Apr 2024 10:34:34 +0300 Subject: [PATCH 6/9] Remove old commented out code. --- Lib/pydoc.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Lib/pydoc.py b/Lib/pydoc.py index d7b95c34cf6c65..09ef1240b14c4d 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -210,9 +210,6 @@ def _getargspecs(object): text_signature = getattr(object, '__text_signature__', None) if text_signature: argspecs = [] - #for argspec in re.split(r'(?<=\))\n(?=\()', text_signature): - # if argspec[:2] == '($': - # argspec = '(' + argspec[2:] for argspec, _ in inspect._signature_split(text_signature): if getattr(object, '__self__', None) is not None: # Strip the bound argument. @@ -221,7 +218,6 @@ def _getargspecs(object): argspec = '(' + argspec[m.end():] argspecs.append(argspec) return argspecs - #print('>>>>>>>>', object, file=sys.stderr) return None def classname(object, modname): From 1a3536fddc30e9dbacd8b8e7afe99036be61ecf1 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 11 Apr 2024 10:43:59 +0300 Subject: [PATCH 7/9] Add $module for builtin functions. --- Python/bltinmodule.c | 40 +++++++++++++++++------------------ Python/clinic/bltinmodule.c.h | 6 +++--- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index 3983762c11dda4..763a7abaef16ea 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -239,8 +239,8 @@ builtin___build_class__(PyObject *self, PyObject *const *args, Py_ssize_t nargs, } PyDoc_STRVAR(build_class_doc, -"__build_class__(func, name, /, *bases, **kwds)\n\ -(func, name, /, *bases, metaclass, **kwds)\n\ +"__build_class__($module, func, name, /, *bases, **kwds)\n\ +($module, func, name, /, *bases, metaclass, **kwds)\n\ --\n\ \n\ Internal helper function used by the class statement."); @@ -477,7 +477,7 @@ builtin_breakpoint(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyOb } PyDoc_STRVAR(breakpoint_doc, -"breakpoint(*args, **kws)\n\ +"breakpoint($module, /, *args, **kws)\n\ --\n\ \n\ Call sys.breakpointhook(*args, **kws). sys.breakpointhook() must accept\n\ @@ -892,8 +892,8 @@ builtin_dir(PyObject *self, PyObject *args) } PyDoc_STRVAR(dir_doc, -"dir()\n" -"(object, /)\n" +"dir($module, /)\n" +"($module, object, /)\n" "--\n" "\n" "If called without an argument, return the names in the current scope.\n" @@ -1557,8 +1557,8 @@ builtin_next(PyObject *self, PyObject *const *args, Py_ssize_t nargs) } PyDoc_STRVAR(next_doc, -"next(iterator, /)\n\ -next(iterator, default, /)\n\ +"next($module, iterator, /)\n\ +next($module, iterator, default, /)\n\ --\n\ \n\ Return the next item from the iterator. If default is given and the iterator\n\ @@ -1678,8 +1678,8 @@ builtin_iter(PyObject *self, PyObject *const *args, Py_ssize_t nargs) } PyDoc_STRVAR(iter_doc, -"iter(iterable, /)\n\ -(callable, sentinel)\n\ +"iter($module, iterable, /)\n\ +($module, callable, sentinel)\n\ --\n\ \n\ Get an iterator from an object. In the first form, the argument must\n\ @@ -1706,8 +1706,8 @@ builtin_aiter(PyObject *module, PyObject *async_iterable) PyObject *PyAnextAwaitable_New(PyObject *, PyObject *); /*[clinic input] -@text_signature "(aiterator, /)" -@text_signature "(aiterator, default, /)" +@text_signature "($module, aiterator, /)" +@text_signature "($module, aiterator, default, /)" anext as builtin_anext aiterator: object @@ -1723,7 +1723,7 @@ it is returned instead of raising StopAsyncIteration. static PyObject * builtin_anext_impl(PyObject *module, PyObject *aiterator, PyObject *default_value) -/*[clinic end generated code: output=f02c060c163a81fa input=2215abe23cb47877]*/ +/*[clinic end generated code: output=f02c060c163a81fa input=0a1ca4f26c511583]*/ { PyTypeObject *t; PyObject *awaitable; @@ -1916,9 +1916,9 @@ builtin_min(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *k } PyDoc_STRVAR(min_doc, -"min(iterable, /, *, key=None)\n\ -(iterable, /, *, default, key=None)\n\ -(arg1, arg2, /, *args, key=None)\n\ +"min($module, iterable, /, *, key=None)\n\ +($module, iterable, /, *, default, key=None)\n\ +($module, arg1, arg2, /, *args, key=None)\n\ --\n\ \n\ With a single iterable argument, return its smallest item. The\n\ @@ -1935,9 +1935,9 @@ builtin_max(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *k } PyDoc_STRVAR(max_doc, -"max(iterable, /, *, key=None)\n\ -(iterable, /, *, default, key=None)\n\ -(arg1, arg2, /, *args, key=None)\n\ +"max($module, iterable, /, *, key=None)\n\ +($module, iterable, /, *, default, key=None)\n\ +($module, arg1, arg2, /, *args, key=None)\n\ --\n\ \n\ With a single iterable argument, return its biggest item. The\n\ @@ -2512,8 +2512,8 @@ builtin_vars(PyObject *self, PyObject *args) } PyDoc_STRVAR(vars_doc, -"vars()\n\ -vars(object, /)\n\ +"vars($module, /)\n\ +vars($module, object, /)\n\ --\n\ \n\ Without arguments, equivalent to locals().\n\ diff --git a/Python/clinic/bltinmodule.c.h b/Python/clinic/bltinmodule.c.h index 00c7b69c204c18..72529af9bf18a6 100644 --- a/Python/clinic/bltinmodule.c.h +++ b/Python/clinic/bltinmodule.c.h @@ -690,8 +690,8 @@ PyDoc_STRVAR(builtin_aiter__doc__, {"aiter", (PyCFunction)builtin_aiter, METH_O, builtin_aiter__doc__}, PyDoc_STRVAR(builtin_anext__doc__, -"anext(aiterator, /)\n" -"(aiterator, default, /)\n" +"anext($module, aiterator, /)\n" +"($module, aiterator, default, /)\n" "--\n" "\n" "Return the next item from the async iterator.\n" @@ -1194,4 +1194,4 @@ builtin_issubclass(PyObject *module, PyObject *const *args, Py_ssize_t nargs) exit: return return_value; } -/*[clinic end generated code: output=d50c558d44af3683 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=23e67d22a2f8345d input=a9049054013a1b77]*/ From 6ff7151cb7fb8e6a67207b00f4879d10b99e1950 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 11 Apr 2024 13:50:02 +0300 Subject: [PATCH 8/9] Tests signatures of methods. Fix more signatures in builtins, types and sys modules. --- Lib/test/test_inspect/test_inspect.py | 67 ++++++++++++++++++--------- Modules/_datetimemodule.c | 9 ++-- Objects/cellobject.c | 3 +- Objects/clinic/descrobject.c.h | 8 +++- Objects/codeobject.c | 3 +- Objects/descrobject.c | 9 ++-- Objects/genericaliasobject.c | 2 + Objects/genobject.c | 21 ++++++--- Objects/iterobject.c | 7 ++- Objects/memoryobject.c | 4 +- Objects/namespaceobject.c | 9 ++-- Objects/object.c | 4 +- Objects/rangeobject.c | 2 +- Objects/sliceobject.c | 2 +- Objects/structseq.c | 3 +- Objects/typeobject.c | 5 +- Python/clinic/traceback.c.h | 8 ++-- Python/sysmodule.c | 22 ++++++--- Python/traceback.c | 8 ++-- 19 files changed, 129 insertions(+), 67 deletions(-) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 621d8d970235a2..27438bfcff0706 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -5718,7 +5718,7 @@ class TestSignatureDefinitions(unittest.TestCase): @cpython_only @unittest.skipIf(MISSING_C_DOCSTRINGS, "Signature information for builtins requires docstrings") - def test_builtins_have_signatures(self): + def _test_builtins_have_signatures(self, module, no_signature=(), methods_no_signature={}): # This checks all builtin callables in CPython have signatures # A few have signatures Signature can't yet handle, so we skip those # since they will have to wait until PEP 457 adds the required @@ -5727,34 +5727,25 @@ def test_builtins_have_signatures(self): # reasons, so we also skip those for the time being, but design # the test to fail in order to indicate when it needs to be # updated. - no_signature = set() - # These builtin types are expected to provide introspection info - types_with_signatures = { - '__loader__', 'bool', 'bytearray', 'bytes', 'classmethod', - 'complex', 'dict', 'enumerate', 'filter', 'float', 'frozenset', - 'int', 'list', 'map', 'memoryview', 'object', 'property', 'range', - 'reversed', 'set', 'slice', 'staticmethod', 'str', 'tuple', 'zip' - } + no_signature = no_signature or set() # Check the signatures we expect to be there - ns = vars(builtins) + ns = vars(module) for name, obj in sorted(ns.items()): if not callable(obj): continue - # The builtin types haven't been converted to AC yet - if isinstance(obj, type) and (name not in types_with_signatures): - # Note that this also skips all the exception types + if isinstance(obj, type) and issubclass(obj, BaseException): no_signature.add(name) - if (name in no_signature): - # Not yet converted - with self.subTest(builtin=name): - self.assertRaises(ValueError, inspect.signature, obj) - continue - if name in {'classmethod', 'staticmethod'}: - # Bug gh-112006: inspect.unwrap() does not work with types - # with the __wrapped__ data descriptor. - continue with self.subTest(builtin=name): - self.assertIsNotNone(inspect.signature(obj)) + if name in no_signature: + self.assertRaises(ValueError, inspect.signature, obj) + self.assertRaises(ValueError, inspect.signatures, obj) + else: + self.assertIsNotNone(inspect.signature(obj)) + self.assertIsNotNone(inspect.signatures(obj)) + if isinstance(obj, type): + with self.subTest(type=name): + self._test_builtin_methods_have_signatures(obj, + methods_no_signature.get(name, ())) # Check callables that haven't been converted don't claim a signature # This ensures this test will start failing as more signatures are # added, so the affected items can be moved into the scope of the @@ -5763,6 +5754,36 @@ def test_builtins_have_signatures(self): with self.subTest(builtin=name): self.assertIsNone(ns[name].__text_signature__) + def _test_builtin_methods_have_signatures(self, cls, no_signature): + ns = vars(cls) + for name in ns: + obj = getattr(cls, name, None) + if not callable(obj): + continue + with self.subTest(method=name): + if name in no_signature: + self.assertRaises(ValueError, inspect.signature, obj) + self.assertRaises(ValueError, inspect.signatures, obj) + else: + self.assertIsNotNone(inspect.signature(obj)) + self.assertIsNotNone(inspect.signatures(obj)) + + def test_builtins_have_signatures(self): + no_signature = {'type', 'super'} + methods_no_signature = { + 'bytearray': {'count', 'endswith', 'find', 'index', 'rfind', 'rindex', 'startswith'}, + 'bytes': {'count', 'endswith', 'find', 'index', 'rfind', 'rindex', 'startswith'}, + 'str': {'count', 'endswith', 'startswith'}, + 'object': {'__class__'}, + } + self._test_builtins_have_signatures(builtins, no_signature, methods_no_signature) + + def test_types_builtins_have_signatures(self): + self._test_builtins_have_signatures(types) + + def test_sys_builtins_have_signatures(self): + self._test_builtins_have_signatures(sys) + def test_python_function_override_signature(self): def func(*args, **kwargs): pass diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index a626bda2ea9be9..227b3e40a69e22 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -3643,7 +3643,8 @@ static PyMethodDef date_methods[] = { DATETIME_DATE_REPLACE_METHODDEF - {"__replace__", _PyCFunction_CAST(datetime_date_replace), METH_FASTCALL | METH_KEYWORDS}, + {"__replace__", _PyCFunction_CAST(datetime_date_replace), METH_FASTCALL | METH_KEYWORDS, + PyDoc_STR("__replace__($self, **changes)\n--\n\n")}, {"__reduce__", (PyCFunction)date_reduce, METH_NOARGS, PyDoc_STR("__reduce__() -> (cls, state)")}, @@ -4770,7 +4771,8 @@ static PyMethodDef time_methods[] = { DATETIME_TIME_REPLACE_METHODDEF - {"__replace__", _PyCFunction_CAST(datetime_time_replace), METH_FASTCALL | METH_KEYWORDS}, + {"__replace__", _PyCFunction_CAST(datetime_time_replace), METH_FASTCALL | METH_KEYWORDS, + PyDoc_STR("__replace__($self, **changes)\n--\n\n")}, {"fromisoformat", (PyCFunction)time_fromisoformat, METH_O | METH_CLASS, PyDoc_STR("string -> time from a string in ISO 8601 format")}, @@ -6617,7 +6619,8 @@ static PyMethodDef datetime_methods[] = { DATETIME_DATETIME_REPLACE_METHODDEF - {"__replace__", _PyCFunction_CAST(datetime_datetime_replace), METH_FASTCALL | METH_KEYWORDS}, + {"__replace__", _PyCFunction_CAST(datetime_datetime_replace), METH_FASTCALL | METH_KEYWORDS, + PyDoc_STR("__replace__($self, **changes)\n--\n\n")}, {"astimezone", _PyCFunction_CAST(datetime_astimezone), METH_VARARGS | METH_KEYWORDS, PyDoc_STR("tz -> convert to local time in new timezone tz\n")}, diff --git a/Objects/cellobject.c b/Objects/cellobject.c index b1154e4ca4ace6..a2530bbe2cdf75 100644 --- a/Objects/cellobject.c +++ b/Objects/cellobject.c @@ -20,7 +20,8 @@ PyCell_New(PyObject *obj) } PyDoc_STRVAR(cell_new_doc, -"cell([contents])\n" +"cell()\n" +"cell(contents)\n" "--\n" "\n" "Create a new cell object.\n" diff --git a/Objects/clinic/descrobject.c.h b/Objects/clinic/descrobject.c.h index 02fb440d9c83af..d79be80d3ec165 100644 --- a/Objects/clinic/descrobject.c.h +++ b/Objects/clinic/descrobject.c.h @@ -8,6 +8,12 @@ preserve #endif #include "pycore_modsupport.h" // _PyArg_UnpackKeywords() +PyDoc_STRVAR(mappingproxy_new__doc__, +"mappingproxy(mapping)\n" +"--\n" +"\n" +"Read-only proxy of a mapping."); + static PyObject * mappingproxy_new_impl(PyTypeObject *type, PyObject *mapping); @@ -167,4 +173,4 @@ property_init(PyObject *self, PyObject *args, PyObject *kwargs) exit: return return_value; } -/*[clinic end generated code: output=a4664ccf3da10f5a input=a9049054013a1b77]*/ +/*[clinic end generated code: output=050e331316a04207 input=a9049054013a1b77]*/ diff --git a/Objects/codeobject.c b/Objects/codeobject.c index f14ff73394b168..4142afdeac8fee 100644 --- a/Objects/codeobject.c +++ b/Objects/codeobject.c @@ -2170,7 +2170,8 @@ static struct PyMethodDef code_methods[] = { {"co_positions", (PyCFunction)code_positionsiterator, METH_NOARGS}, CODE_REPLACE_METHODDEF CODE__VARNAME_FROM_OPARG_METHODDEF - {"__replace__", _PyCFunction_CAST(code_replace), METH_FASTCALL|METH_KEYWORDS}, + {"__replace__", _PyCFunction_CAST(code_replace), METH_FASTCALL|METH_KEYWORDS, + PyDoc_STR("__replace__($self, **changes)\n--\n\n")}, {NULL, NULL} /* sentinel */ }; diff --git a/Objects/descrobject.c b/Objects/descrobject.c index 3423f152ce862d..0c61eb1d7c12d3 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -1165,8 +1165,8 @@ mappingproxy_reversed(PyObject *self, PyObject *Py_UNUSED(ignored)) static PyMethodDef mappingproxy_methods[] = { {"get", _PyCFunction_CAST(mappingproxy_get), METH_FASTCALL, - PyDoc_STR("D.get(k[,d]) -> D[k] if k in D, else d." - " d defaults to None.")}, + PyDoc_STR("get(self, key, default=None, /)\n--\n\n" + "Return the value for key if key is in the mapping, else default.")}, {"keys", mappingproxy_keys, METH_NOARGS, PyDoc_STR("D.keys() -> a set-like object providing a view on D's keys")}, {"values", mappingproxy_values, METH_NOARGS, @@ -1254,11 +1254,12 @@ mappingproxy.__new__ as mappingproxy_new mapping: object +Read-only proxy of a mapping. [clinic start generated code]*/ static PyObject * mappingproxy_new_impl(PyTypeObject *type, PyObject *mapping) -/*[clinic end generated code: output=65f27f02d5b68fa7 input=d2d620d4f598d4f8]*/ +/*[clinic end generated code: output=65f27f02d5b68fa7 input=c156df096ef7590c]*/ { mappingproxyobject *mappingproxy; @@ -2024,7 +2025,7 @@ PyTypeObject PyDictProxy_Type = { 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_MAPPING, /* tp_flags */ - 0, /* tp_doc */ + mappingproxy_new__doc__, /* tp_doc */ mappingproxy_traverse, /* tp_traverse */ 0, /* tp_clear */ mappingproxy_richcompare, /* tp_richcompare */ diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index c045d495e85526..2779baf0bd1c61 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -537,6 +537,8 @@ _Py_subs_parameters(PyObject *self, PyObject *args, PyObject *parameters, PyObje } PyDoc_STRVAR(genericalias__doc__, +"GenericAlias(origin, args, /)\n" +"--\n\n" "Represent a PEP 585 generic type\n" "\n" "E.g. for t = list[int], t.__origin__ is list and t.__args__ is (int,)."); diff --git a/Objects/genobject.c b/Objects/genobject.c index 8d1dbb72ba9ec2..7e0133a6a1bc59 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -413,8 +413,11 @@ gen_close(PyGenObject *gen, PyObject *args) PyDoc_STRVAR(throw_doc, -"throw(value)\n\ -throw(type[,value[,tb]])\n\ +"throw(value, /)\n\ +(type, /)\n\ +(type, value, /)\n\ +(type, value, traceback, /)\n\ +--\n\ \n\ Raise exception in generator, return next yielded value or raise\n\ StopIteration.\n\ @@ -1131,8 +1134,11 @@ PyDoc_STRVAR(coro_send_doc, return next iterated value or raise StopIteration."); PyDoc_STRVAR(coro_throw_doc, -"throw(value)\n\ -throw(type[,value[,traceback]])\n\ +"throw(value, /)\n\ +(type, /)\n\ +(type, value, /)\n\ +(type, value, traceback, /)\n\ +--\n\ \n\ Raise exception in coroutine, return next iterated value or raise\n\ StopIteration.\n\ @@ -1548,8 +1554,11 @@ PyDoc_STRVAR(async_asend_doc, "asend(v) -> send 'v' in generator."); PyDoc_STRVAR(async_athrow_doc, -"athrow(value)\n\ -athrow(type[,value[,tb]])\n\ +"athrow(value, /)\n\ +(type, /)\n\ +(type, value, /)\n\ +(type, value, traceback, /)\n\ +--\n\ \n\ raise exception in generator.\n\ the (type, val, tb) signature is deprecated, \n\ diff --git a/Objects/iterobject.c b/Objects/iterobject.c index 135ced9ea1f268..c5aa7d5b8c19cb 100644 --- a/Objects/iterobject.c +++ b/Objects/iterobject.c @@ -437,8 +437,11 @@ return next yielded value or raise StopIteration."); PyDoc_STRVAR(throw_doc, -"throw(value)\n\ -throw(typ[,val[,tb]])\n\ +"throw(value, /)\n\ +(type, /)\n\ +(type, value, /)\n\ +(type, value, traceback, /)\n\ +--\n\ \n\ raise exception in the wrapped iterator, return next yielded value\n\ or raise StopIteration.\n\ diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 73f0cc7fb1c731..30c55d13f0522c 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -3259,6 +3259,8 @@ PyDoc_STRVAR(memory_f_contiguous_doc, "A bool indicating whether the memory is Fortran contiguous."); PyDoc_STRVAR(memory_contiguous_doc, "A bool indicating whether the memory is contiguous."); +PyDoc_STRVAR(memory_exit_doc, + "__exit__($self, *exc_info)\n--\n\n"); static PyGetSetDef memory_getsetlist[] = { @@ -3287,7 +3289,7 @@ static PyMethodDef memory_methods[] = { MEMORYVIEW_TOREADONLY_METHODDEF MEMORYVIEW__FROM_FLAGS_METHODDEF {"__enter__", memory_enter, METH_NOARGS, NULL}, - {"__exit__", memory_exit, METH_VARARGS, NULL}, + {"__exit__", memory_exit, METH_VARARGS, memory_exit_doc}, {NULL, NULL} }; diff --git a/Objects/namespaceobject.c b/Objects/namespaceobject.c index b975bcfeea2cdf..178042a24f6f3d 100644 --- a/Objects/namespaceobject.c +++ b/Objects/namespaceobject.c @@ -219,15 +219,16 @@ namespace_replace(PyObject *self, PyObject *args, PyObject *kwargs) static PyMethodDef namespace_methods[] = { {"__reduce__", (PyCFunction)namespace_reduce, METH_NOARGS, namespace_reduce__doc__}, - {"__replace__", _PyCFunction_CAST(namespace_replace), METH_VARARGS|METH_KEYWORDS, NULL}, + {"__replace__", _PyCFunction_CAST(namespace_replace), METH_VARARGS|METH_KEYWORDS, + PyDoc_STR("__replace__($self, **changes)\n--\n\n")}, {NULL, NULL} // sentinel }; PyDoc_STRVAR(namespace_doc, -"A simple attribute-based namespace.\n\ -\n\ -SimpleNamespace(**kwargs)"); +"SimpleNamespace(**kwargs)\n\ +--\n\n\ +A simple attribute-based namespace."); PyTypeObject _PyNamespace_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) diff --git a/Objects/object.c b/Objects/object.c index c8e6f8fc1a2b40..c2b2a330f12863 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -2028,7 +2028,7 @@ PyTypeObject _PyNone_Type = { 0, /*tp_setattro */ 0, /*tp_as_buffer */ Py_TPFLAGS_DEFAULT, /*tp_flags */ - 0, /*tp_doc */ + PyDoc_STR("NoneType()\n--\n\n"), /*tp_doc */ 0, /*tp_traverse */ 0, /*tp_clear */ _Py_BaseObject_RichCompare, /*tp_richcompare */ @@ -2127,7 +2127,7 @@ PyTypeObject _PyNotImplemented_Type = { 0, /*tp_setattro */ 0, /*tp_as_buffer */ Py_TPFLAGS_DEFAULT, /*tp_flags */ - 0, /*tp_doc */ + PyDoc_STR("NotImplementedType()\n--\n\n"), /*tp_doc */ 0, /*tp_traverse */ 0, /*tp_clear */ 0, /*tp_richcompare */ diff --git a/Objects/rangeobject.c b/Objects/rangeobject.c index 817481a26473ef..5a447263d88224 100644 --- a/Objects/rangeobject.c +++ b/Objects/rangeobject.c @@ -754,7 +754,7 @@ PyDoc_STRVAR(index_doc, static PyMethodDef range_methods[] = { {"__reversed__", range_reverse, METH_NOARGS, reverse_doc}, - {"__reduce__", (PyCFunction)range_reduce, METH_VARARGS}, + {"__reduce__", (PyCFunction)range_reduce, METH_NOARGS}, {"count", (PyCFunction)range_count, METH_O, count_doc}, {"index", (PyCFunction)range_index, METH_O, index_doc}, {NULL, NULL} /* sentinel */ diff --git a/Objects/sliceobject.c b/Objects/sliceobject.c index a99b2392313948..2695cb5f47c250 100644 --- a/Objects/sliceobject.c +++ b/Objects/sliceobject.c @@ -78,7 +78,7 @@ PyTypeObject PyEllipsis_Type = { 0, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT, /* tp_flags */ - 0, /* tp_doc */ + PyDoc_STR("ellipsis()\n--\n\n"), /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ 0, /* tp_richcompare */ diff --git a/Objects/structseq.c b/Objects/structseq.c index 661d96a968fb80..fb8d3bbc02abe7 100644 --- a/Objects/structseq.c +++ b/Objects/structseq.c @@ -453,7 +453,8 @@ structseq_replace(PyStructSequence *self, PyObject *args, PyObject *kwargs) static PyMethodDef structseq_methods[] = { {"__reduce__", (PyCFunction)structseq_reduce, METH_NOARGS, NULL}, - {"__replace__", _PyCFunction_CAST(structseq_replace), METH_VARARGS | METH_KEYWORDS, NULL}, + {"__replace__", _PyCFunction_CAST(structseq_replace), METH_VARARGS | METH_KEYWORDS, + PyDoc_STR("__replace__($self, **changes)\n--\n\n")}, {NULL, NULL} // sentinel }; diff --git a/Objects/typeobject.c b/Objects/typeobject.c index e9f2d2577e9fab..9ba73feca7e194 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6871,7 +6871,7 @@ static PyMethodDef object_methods[] = { OBJECT___REDUCE_EX___METHODDEF OBJECT___REDUCE___METHODDEF OBJECT___GETSTATE___METHODDEF - {"__subclasshook__", object_subclasshook, METH_CLASS | METH_VARARGS, + {"__subclasshook__", object_subclasshook, METH_CLASS | METH_O, object_subclasshook_doc}, {"__init_subclass__", object_init_subclass, METH_CLASS | METH_NOARGS, object_init_subclass_doc}, @@ -9882,7 +9882,8 @@ static pytype_slotdef slotdefs[] = { TPSLOT(__new__, tp_new, slot_tp_new, NULL, "__new__(type, /, *args, **kwargs)\n--\n\n" "Create and return new object. See help(type) for accurate signature."), - TPSLOT(__del__, tp_finalize, slot_tp_finalize, (wrapperfunc)wrap_del, ""), + TPSLOT(__del__, tp_finalize, slot_tp_finalize, (wrapperfunc)wrap_del, + "__del__($self)\n--\n\n"), BUFSLOT(__buffer__, bf_getbuffer, slot_bf_getbuffer, wrap_buffer, "__buffer__($self, flags, /)\n--\n\n" diff --git a/Python/clinic/traceback.c.h b/Python/clinic/traceback.c.h index aee08d6ad97047..fe53a2786d1ad6 100644 --- a/Python/clinic/traceback.c.h +++ b/Python/clinic/traceback.c.h @@ -9,7 +9,7 @@ preserve #include "pycore_modsupport.h" // _PyArg_UnpackKeywords() PyDoc_STRVAR(tb_new__doc__, -"TracebackType(tb_next, tb_frame, tb_lasti, tb_lineno)\n" +"traceback(tb_next, tb_frame, tb_lasti, tb_lineno)\n" "--\n" "\n" "Create a new traceback object."); @@ -43,7 +43,7 @@ tb_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) static const char * const _keywords[] = {"tb_next", "tb_frame", "tb_lasti", "tb_lineno", NULL}; static _PyArg_Parser _parser = { .keywords = _keywords, - .fname = "TracebackType", + .fname = "traceback", .kwtuple = KWTUPLE, }; #undef KWTUPLE @@ -61,7 +61,7 @@ tb_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) } tb_next = fastargs[0]; if (!PyObject_TypeCheck(fastargs[1], &PyFrame_Type)) { - _PyArg_BadArgument("TracebackType", "argument 'tb_frame'", (&PyFrame_Type)->tp_name, fastargs[1]); + _PyArg_BadArgument("traceback", "argument 'tb_frame'", (&PyFrame_Type)->tp_name, fastargs[1]); goto exit; } tb_frame = (PyFrameObject *)fastargs[1]; @@ -78,4 +78,4 @@ tb_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) exit: return return_value; } -/*[clinic end generated code: output=4e2f6b935841b09c input=a9049054013a1b77]*/ +/*[clinic end generated code: output=916a759875507c5a input=a9049054013a1b77]*/ diff --git a/Python/sysmodule.c b/Python/sysmodule.c index cd193c1581c679..ee6eff738cb48e 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -500,7 +500,8 @@ sys_addaudithook_impl(PyObject *module, PyObject *hook) } PyDoc_STRVAR(audit_doc, -"audit(event, *args)\n\ +"audit($module, event, /, *args)\n\ +--\n\ \n\ Passes the event to any audit hooks that are attached."); @@ -644,7 +645,8 @@ sys_breakpointhook(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyOb } PyDoc_STRVAR(breakpointhook_doc, -"breakpointhook(*args, **kws)\n" +"breakpointhook($module, /, *args, **kwargs)\n" +"--\n" "\n" "This hook function is called by built-in breakpoint().\n" ); @@ -1103,7 +1105,8 @@ sys_settrace(PyObject *self, PyObject *args) } PyDoc_STRVAR(settrace_doc, -"settrace(function)\n\ +"settrace($module, function, /)\n\ +--\n\ \n\ Set the global debug tracing function. It will be called on each\n\ function call. See the debugger chapter in the library manual." @@ -1177,7 +1180,8 @@ sys_setprofile(PyObject *self, PyObject *args) } PyDoc_STRVAR(setprofile_doc, -"setprofile(function)\n\ +"setprofile($module, function, /)\n\ +--\n\ \n\ Set the profiling function. It will be called on each function call\n\ and return. See the profiler chapter in the library manual." @@ -1420,7 +1424,11 @@ sys_set_asyncgen_hooks(PyObject *self, PyObject *args, PyObject *kw) } PyDoc_STRVAR(set_asyncgen_hooks_doc, -"set_asyncgen_hooks([firstiter] [, finalizer])\n\ +"set_asyncgen_hooks($module, /)\n\ +($module, /, *, finalizer)\n\ +($module, /, firstiter)\n\ +($module, /, firstiter, finalizer)\n\ +--\n\ \n\ Set a finalizer for async generators objects." ); @@ -1932,7 +1940,9 @@ sys_getsizeof(PyObject *self, PyObject *args, PyObject *kwds) } PyDoc_STRVAR(getsizeof_doc, -"getsizeof(object [, default]) -> int\n\ +"getsizeof($module, object)\n\ +($module, object, default)\n\ +--\n\ \n\ Return the size of object in bytes."); diff --git a/Python/traceback.c b/Python/traceback.c index 2564a7db5dcfec..47b77c9108dd9a 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -34,9 +34,9 @@ extern char* _PyTokenizer_FindEncodingFilename(int, PyObject *); /*[clinic input] -class TracebackType "PyTracebackObject *" "&PyTraceback_Type" +class traceback "PyTracebackObject *" "&PyTraceback_Type" [clinic start generated code]*/ -/*[clinic end generated code: output=da39a3ee5e6b4b0d input=928fa06c10151120]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=cf96294b2bebc811]*/ #include "clinic/traceback.c.h" @@ -63,7 +63,7 @@ tb_create_raw(PyTracebackObject *next, PyFrameObject *frame, int lasti, /*[clinic input] @classmethod -TracebackType.__new__ as tb_new +traceback.__new__ as tb_new tb_next: object tb_frame: object(type='PyFrameObject *', subclass_of='&PyFrame_Type') @@ -76,7 +76,7 @@ Create a new traceback object. static PyObject * tb_new_impl(PyTypeObject *type, PyObject *tb_next, PyFrameObject *tb_frame, int tb_lasti, int tb_lineno) -/*[clinic end generated code: output=fa077debd72d861a input=01cbe8ec8783fca7]*/ +/*[clinic end generated code: output=fa077debd72d861a input=b88143145454cb59]*/ { if (tb_next == Py_None) { tb_next = NULL; From 452bb6f86526f2f3e705dd06c693a4765aaec1a7 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 9 Apr 2025 13:13:46 +0300 Subject: [PATCH 9/9] Add more tests. --- Lib/test/test_pydoc/test_pydoc.py | 27 ++++++++++++++++++++++++++- Modules/_testcapi/docstring.c | 3 ++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index 840dbb6542f69d..2d31d19873ad0d 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -1505,7 +1505,10 @@ def _get_summary_line(o): text = pydoc.plain(pydoc.render_doc(o)) lines = text.split('\n') assert len(lines) >= 2 - return lines[2] + for i in range(3, len(lines)): + if lines[i].startswith(' '): + break + return '\n'.join(lines[2:i]) @staticmethod def _get_summary_lines(o): @@ -1650,6 +1653,28 @@ def test_bound_builtin_method_unrepresentable_default(self): "meth(a, b=) " "method of _testcapi.DocStringUnrepresentableSignatureTest instance") + @support.cpython_only + @requires_docstrings + def test_unbound_builtin_method_multisig_unrepresentable_default(self): + _testcapi = import_helper.import_module("_testcapi") + cls = _testcapi.DocStringUnrepresentableSignatureTest + self.assertEqual(self._get_summary_line(cls.meth_multi), + "meth_multi(self, /) unbound " + "_testcapi.DocStringUnrepresentableSignatureTest method\n" + "meth_multi(self, /, a, b=) unbound " + "_testcapi.DocStringUnrepresentableSignatureTest method") + + @support.cpython_only + @requires_docstrings + def test_bound_builtin_method_multisig_unrepresentable_default(self): + _testcapi = import_helper.import_module("_testcapi") + obj = _testcapi.DocStringUnrepresentableSignatureTest() + self.assertEqual(self._get_summary_line(obj.meth_multi), + "meth_multi() " + "method of _testcapi.DocStringUnrepresentableSignatureTest instance\n" + "meth_multi(a, b=) " + "method of _testcapi.DocStringUnrepresentableSignatureTest instance") + @support.cpython_only @requires_docstrings def test_unbound_builtin_classmethod_unrepresentable_default(self): diff --git a/Modules/_testcapi/docstring.c b/Modules/_testcapi/docstring.c index e9b31453a76f33..75e14865b34793 100644 --- a/Modules/_testcapi/docstring.c +++ b/Modules/_testcapi/docstring.c @@ -187,7 +187,8 @@ static PyMethodDef DocStringUnrepresentableSignatureTest_methods[] = { {"meth_multi", (PyCFunction)test_with_docstring, METH_VARARGS, PyDoc_STR( - "meth_multi($self, /, a, b=)\n" + "meth_multi($self, /)\n" + "($self, /, a, b=)\n" "--\n\n" "This docstring has a multisignature with unrepresentable default." )},