From 3c23b2261bf3f53783d0ee5c8521614d6810cf49 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sun, 12 Feb 2023 07:37:18 +0300 Subject: [PATCH 01/13] gh-81677: basic support for annotations in signature strings --- Lib/inspect.py | 29 ++++++++++++++++--- Lib/test/test_inspect/test_inspect.py | 27 +++++++++++++++++ ...3-02-20-07-33-54.gh-issue-81677.bSmaNE.rst | 2 ++ Objects/typeobject.c | 11 +++---- 4 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-02-20-07-33-54.gh-issue-81677.bSmaNE.rst diff --git a/Lib/inspect.py b/Lib/inspect.py index aaa22bef896602..d476143b5a32cc 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2276,10 +2276,22 @@ def _signature_fromstr(cls, obj, s, skip_bound_arg=True): def parse_name(node): assert isinstance(node, ast.arg) - if node.annotation is not None: - raise ValueError("Annotations are not currently supported") return node.arg + def parse_annotation(node): + assert isinstance(node, (ast.arg, ast.FunctionDef)) + if isinstance(node, ast.arg): + annotation = node.annotation + else: + annotation = node.returns + if annotation: + expr = ast.unparse(annotation) + try: + return eval(expr, sys_module_dict) + except NameError: + raise ValueError + return empty + def wrap_value(s): try: value = eval(s, module_dict) @@ -2334,7 +2346,11 @@ def p(name_node, default_node, default=empty): default = ast.literal_eval(default_node) except ValueError: raise ValueError("{!r} builtin has invalid signature".format(obj)) from None - parameters.append(Parameter(name, kind, default=default, annotation=empty)) + try: + annotation = parse_annotation(name_node) + except ValueError: + raise ValueError("{!r} builtin has invalid signature".format(obj)) from None + parameters.append(Parameter(name, kind, default=default, annotation=annotation)) # non-keyword-only parameters total_non_kw_args = len(f.args.posonlyargs) + len(f.args.args) @@ -2381,7 +2397,12 @@ def p(name_node, default_node, default=empty): p = parameters[0].replace(kind=Parameter.POSITIONAL_ONLY) parameters[0] = p - return cls(parameters, return_annotation=cls.empty) + try: + return_annotation = parse_annotation(f) + except ValueError: + raise ValueError("{!r} builtin has invalid signature".format(obj)) from None + + return cls(parameters, return_annotation=return_annotation) def _signature_from_builtin(cls, func, skip_bound_arg=True): diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 31ac6b1070a0a2..adb8176541195a 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -4807,6 +4807,33 @@ class MyBufferedReader(BufferedReader): sig = inspect.signature(MyBufferedReader) self.assertEqual(str(sig), '(raw, buffer_size=8192)') + def test_annotations_in_text_signature(self): + def func(*args, **kwargs): + pass + + func.__text_signature__ = '($self, a: int) -> list' + sig = inspect.signature(func) + self.assertIsNotNone(sig) + self.assertEqual(str(sig), '(self, /, a: int) -> list') + + func.__text_signature__ = '($self, a: int | float)' + sig = inspect.signature(func) + self.assertIsNotNone(sig) + self.assertEqual(str(sig), '(self, /, a: int | float)') + + func.__text_signature__ = '($self, a: tuple[int, ...])' + sig = inspect.signature(func) + self.assertIsNotNone(sig) + self.assertEqual(str(sig), '(self, /, a: tuple[int, ...])') + + func.__text_signature__ = '(self, x: spam)' + with self.assertRaises(ValueError): + inspect.signature(func) + + func.__text_signature__ = '(self, x) -> spam' + with self.assertRaises(ValueError): + inspect.signature(func) + class NTimesUnwrappable: def __init__(self, n): diff --git a/Misc/NEWS.d/next/Library/2023-02-20-07-33-54.gh-issue-81677.bSmaNE.rst b/Misc/NEWS.d/next/Library/2023-02-20-07-33-54.gh-issue-81677.bSmaNE.rst new file mode 100644 index 00000000000000..0d1e2c91fedbf6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-02-20-07-33-54.gh-issue-81677.bSmaNE.rst @@ -0,0 +1,2 @@ +Basic support of annotations in :func:`inspect.signature` handling of +``__text_signature__``. Patch by Sergey B Kirpichev. diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 25085693070221..96551d94f965fc 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -505,8 +505,8 @@ find_signature(const char *name, const char *doc) return doc; } -#define SIGNATURE_END_MARKER ")\n--\n\n" -#define SIGNATURE_END_MARKER_LENGTH 6 +#define SIGNATURE_END_MARKER "\n--\n\n" +#define SIGNATURE_END_MARKER_LENGTH 5 /* * skips past the end of the docstring's introspection signature. * (assumes doc starts with a valid signature prefix.) @@ -623,11 +623,12 @@ _PyType_GetTextSignatureFromInternalDoc(const char *name, const char *internal_d Py_RETURN_NONE; } - /* back "end" up until it points just past the final ')' */ - end -= SIGNATURE_END_MARKER_LENGTH - 1; + /* back "end" up until it points just to the end marker */ + end -= SIGNATURE_END_MARKER_LENGTH; assert((end - start) >= 2); /* should be "()" at least */ - assert(end[-1] == ')'); assert(end[0] == '\n'); + assert(end[1] == '-'); + assert(end[2] == '-'); return PyUnicode_FromStringAndSize(start, end - start); } From 9b446825f2ebedd93e092637f27f05468903d167 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sat, 11 Nov 2023 06:14:17 +0300 Subject: [PATCH 02/13] Redo parse_annotation() to accept annotation directly --- Lib/inspect.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index d476143b5a32cc..d7fb84bfbaefc2 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2278,12 +2278,7 @@ def parse_name(node): assert isinstance(node, ast.arg) return node.arg - def parse_annotation(node): - assert isinstance(node, (ast.arg, ast.FunctionDef)) - if isinstance(node, ast.arg): - annotation = node.annotation - else: - annotation = node.returns + def parse_annotation(annotation): if annotation: expr = ast.unparse(annotation) try: @@ -2347,7 +2342,7 @@ def p(name_node, default_node, default=empty): except ValueError: raise ValueError("{!r} builtin has invalid signature".format(obj)) from None try: - annotation = parse_annotation(name_node) + annotation = parse_annotation(name_node.annotation) except ValueError: raise ValueError("{!r} builtin has invalid signature".format(obj)) from None parameters.append(Parameter(name, kind, default=default, annotation=annotation)) @@ -2398,7 +2393,7 @@ def p(name_node, default_node, default=empty): parameters[0] = p try: - return_annotation = parse_annotation(f) + return_annotation = parse_annotation(f.returns) except ValueError: raise ValueError("{!r} builtin has invalid signature".format(obj)) from None From d85dd6c46f4bcb8233ed2db63f425050b9adcf90 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sat, 11 Nov 2023 06:16:13 +0300 Subject: [PATCH 03/13] Move assert --- Lib/inspect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index d7fb84bfbaefc2..98fedff8f6fa4b 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2275,7 +2275,6 @@ def _signature_fromstr(cls, obj, s, skip_bound_arg=True): sys_module_dict = sys.modules.copy() def parse_name(node): - assert isinstance(node, ast.arg) return node.arg def parse_annotation(annotation): @@ -2334,6 +2333,7 @@ def visit_BinOp(self, node): raise ValueError def p(name_node, default_node, default=empty): + assert isinstance(name_node, ast.arg) name = parse_name(name_node) if default_node and default_node is not _empty: try: From c64280b540dfbe29f01f71b94ac9189df8701b9e Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sat, 11 Nov 2023 06:35:16 +0300 Subject: [PATCH 04/13] Match wrap_value() logic in parse_annotation() Also fix few typos --- Lib/inspect.py | 8 ++++++-- Lib/test/test_inspect/test_inspect.py | 11 +++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 98fedff8f6fa4b..8705b1dadcef85 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2281,9 +2281,13 @@ def parse_annotation(annotation): if annotation: expr = ast.unparse(annotation) try: - return eval(expr, sys_module_dict) + value = eval(expr, module_dict) except NameError: - raise ValueError + try: + value = eval(expr, sys_module_dict) + except NameError: + raise ValueError + return value return empty def wrap_value(s): diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 8d02fc0205bef5..d2f4702545f470 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -4810,6 +4810,8 @@ class MyBufferedReader(BufferedReader): self.assertEqual(str(sig), '(raw, buffer_size=8192)') def test_annotations_in_text_signature(self): + import fractions + def func(*args, **kwargs): pass @@ -4828,14 +4830,19 @@ def func(*args, **kwargs): self.assertIsNotNone(sig) self.assertEqual(str(sig), '(self, /, a: tuple[int, ...])') - func.__text_signature__ = '(self, x: spam)' + func.__text_signature__ = '($self, x: spam)' with self.assertRaises(ValueError): inspect.signature(func) - func.__text_signature__ = '(self, x) -> spam' + func.__text_signature__ = '($self, x) -> spam' with self.assertRaises(ValueError): inspect.signature(func) + func.__text_signature__ = '($self, x) -> fractions.Fraction' + sig = inspect.signature(func) + self.assertIsNotNone(sig) + self.assertEqual(str(sig), '(self, /, x) -> fractions.Fraction') + class NTimesUnwrappable: def __init__(self, n): From 3dc4125aae9dc3b20899a2a5c42af657adea92bc Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sat, 3 Feb 2024 08:43:38 +0300 Subject: [PATCH 05/13] gh-114949: fix signature in the type.__prepare__() doc string --- Objects/typeobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/typeobject.c b/Objects/typeobject.c index b0b93efc7868ec..f5c9a48a2d2004 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -5497,7 +5497,7 @@ static PyMethodDef type_methods[] = { TYPE___SUBCLASSES___METHODDEF {"__prepare__", _PyCFunction_CAST(type_prepare), METH_FASTCALL | METH_KEYWORDS | METH_CLASS, - PyDoc_STR("__prepare__($cls, name, bases, /, **kwds)\n" + PyDoc_STR("__prepare__($type, name, bases, /, **kwds)\n" "--\n" "\n" "Create the namespace for the class statement")}, From ff758ab8faaccbd9f5301d24965cac805019d7c0 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 26 Feb 2024 10:00:39 +0300 Subject: [PATCH 06/13] gh-115231: fill __module__ for built-in class/staticmethods --- Lib/test/test_funcattrs.py | 23 +++++++++++++++++++ ...-02-10-05-42-26.gh-issue-115231.6T7dzi.rst | 2 ++ Objects/descrobject.c | 6 ++++- Objects/typeobject.c | 5 +++- 4 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2024-02-10-05-42-26.gh-issue-115231.6T7dzi.rst diff --git a/Lib/test/test_funcattrs.py b/Lib/test/test_funcattrs.py index b3fc5ad42e7fde..8a4d4ae6663653 100644 --- a/Lib/test/test_funcattrs.py +++ b/Lib/test/test_funcattrs.py @@ -453,6 +453,29 @@ class BuiltinFunctionPropertiesTest(unittest.TestCase): # XXX Not sure where this should really go since I can't find a # test module specifically for builtin_function_or_method. + def test_builtin__module__(self): + import decimal + import math + + # builtin function: + self.assertEqual(len.__module__, 'builtins') + self.assertEqual(math.sin.__module__, 'math') + + # instance method: + self.assertRaises(AttributeError, getattr, int.to_bytes, '__module__') + self.assertRaises(AttributeError, getattr, decimal.Decimal.exp, '__module__') + self.assertEqual(int.to_bytes.__objclass__.__module__, 'builtins') + self.assertEqual(decimal.Decimal.exp.__objclass__.__module__, 'decimal') + + # builtin classmethod: + self.assertEqual(int.from_bytes.__module__, 'builtins') + self.assertEqual(int.from_bytes.__self__.__module__, 'builtins') + self.assertEqual(decimal.Decimal.from_float.__module__, 'decimal') + self.assertEqual(decimal.Decimal.from_float.__self__.__module__, 'decimal') + + # builtin staticmethod: + self.assertEqual(bytes.maketrans.__module__, 'builtins') + def test_builtin__qualname__(self): import time diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-02-10-05-42-26.gh-issue-115231.6T7dzi.rst b/Misc/NEWS.d/next/Core and Builtins/2024-02-10-05-42-26.gh-issue-115231.6T7dzi.rst new file mode 100644 index 00000000000000..12c622f6c086c4 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2024-02-10-05-42-26.gh-issue-115231.6T7dzi.rst @@ -0,0 +1,2 @@ +Setup ``__module__`` attribute for built-in class/static methods. Patch by +Sergey B Kirpichev. diff --git a/Objects/descrobject.c b/Objects/descrobject.c index df546a090c28e4..4589da63f9b515 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -130,7 +130,11 @@ classmethod_get(PyObject *self, PyObject *obj, PyObject *type) if (descr->d_method->ml_flags & METH_METHOD) { cls = descr->d_common.d_type; } - return PyCMethod_New(descr->d_method, type, NULL, cls); + PyObject *mod = PyObject_GetAttr((PyObject*)type, &_Py_ID(__module__)); + PyErr_Clear(); + PyObject *result = PyCMethod_New(descr->d_method, type, mod, cls); + Py_XDECREF(mod); + return result; } static PyObject * diff --git a/Objects/typeobject.c b/Objects/typeobject.c index f5c9a48a2d2004..8df70331343626 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6886,7 +6886,10 @@ type_add_method(PyTypeObject *type, PyMethodDef *meth) descr = PyDescr_NewClassMethod(type, meth); } else if (meth->ml_flags & METH_STATIC) { - PyObject *cfunc = PyCFunction_NewEx(meth, (PyObject*)type, NULL); + PyObject *mod = PyObject_GetAttr((PyObject*)type, &_Py_ID(__module__)); + PyErr_Clear(); + PyObject *cfunc = PyCFunction_NewEx(meth, (PyObject*)type, mod); + Py_XDECREF(mod); if (cfunc == NULL) { return -1; } From 83d21b268a40e403255823aab51665ef645ce0bc Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 26 Feb 2024 10:23:52 +0300 Subject: [PATCH 07/13] gh-82062: correctly set module for built-in instance methods in inspect.signature() --- Lib/inspect.py | 4 +++- Lib/test/test_capi/test_misc.py | 7 +++++++ .../2024-02-11-07-31-43.gh-issue-82062.eeS6w7.rst | 2 ++ Modules/_testcapi/docstring.c | 10 ++++++++++ 4 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2024-02-11-07-31-43.gh-issue-82062.eeS6w7.rst diff --git a/Lib/inspect.py b/Lib/inspect.py index 84b626ee87e95c..d37b9e85c9f763 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2268,7 +2268,9 @@ def _signature_fromstr(cls, obj, s, skip_bound_arg=True): module = None module_dict = {} - module_name = getattr(obj, '__module__', None) + module_name = (getattr(obj, '__module__', None) or + getattr(getattr(obj, '__objclass__', None), + '__module__', None)) if module_name: module = sys.modules.get(module_name, None) if module: diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py index 67fbef4f269814..738b5d6c991583 100644 --- a/Lib/test/test_capi/test_misc.py +++ b/Lib/test/test_capi/test_misc.py @@ -6,6 +6,7 @@ import contextlib import importlib.machinery import importlib.util +import inspect import json import os import pickle @@ -180,6 +181,12 @@ def test_docstring_signature_parsing(self): self.assertEqual(_testcapi.docstring_with_signature_and_extra_newlines.__text_signature__, "($module, /, parameter)") + @unittest.skipIf(MISSING_C_DOCSTRINGS, + "Signature information for builtins requires docstrings") + def test_signature_parsing_with_defaults(self): + meth = _testcapi.DocStringUnrepresentableSignatureTest.with_default + self.assertEqual(str(inspect.signature(meth)), '(self, /, x=1)') + def test_c_type_with_matrix_multiplication(self): M = _testcapi.matmulType m1 = M() diff --git a/Misc/NEWS.d/next/Library/2024-02-11-07-31-43.gh-issue-82062.eeS6w7.rst b/Misc/NEWS.d/next/Library/2024-02-11-07-31-43.gh-issue-82062.eeS6w7.rst new file mode 100644 index 00000000000000..b93de45b1d66a1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-02-11-07-31-43.gh-issue-82062.eeS6w7.rst @@ -0,0 +1,2 @@ +Fix :func:`inspect.signature()` to correctly set extension module for +built-in instance methods. diff --git a/Modules/_testcapi/docstring.c b/Modules/_testcapi/docstring.c index d99fbdd904b594..3f7acbae1b181b 100644 --- a/Modules/_testcapi/docstring.c +++ b/Modules/_testcapi/docstring.c @@ -169,6 +169,13 @@ static PyMethodDef DocStringUnrepresentableSignatureTest_methods[] = { "--\n\n" "This docstring has a signature with unrepresentable default." )}, + {"with_default", + (PyCFunction)test_with_docstring, METH_VARARGS, + PyDoc_STR( + "with_default($self, /, x=ONE)\n" + "--\n\n" + "This instance method has a default parameter value from the module scope." + )}, {NULL}, }; @@ -193,5 +200,8 @@ _PyTestCapi_Init_Docstring(PyObject *mod) if (PyModule_AddType(mod, &DocStringUnrepresentableSignatureTest) < 0) { return -1; } + if (PyModule_AddObject(mod, "ONE", PyLong_FromLong(1)) < 0) { + return -1; + } return 0; } From 1f3c7f30f6342cf6b72ca601d39e1d06f4137e40 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 27 Feb 2024 10:18:44 +0300 Subject: [PATCH 08/13] gh-116110: remove extra processing for the __signature__ attribute This is an alternative to #100168. --- Lib/enum.py | 22 +++++++++---- Lib/inspect.py | 10 +----- Lib/test/test_inspect/test_inspect.py | 32 ------------------- ...-02-27-10-22-15.gh-issue-115937.0cVNur.rst | 3 ++ 4 files changed, 19 insertions(+), 48 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-02-27-10-22-15.gh-issue-115937.0cVNur.rst diff --git a/Lib/enum.py b/Lib/enum.py index 22963cca4466f2..1fb236858a775e 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -1086,6 +1086,21 @@ def _add_member_(cls, name, member): # cls._member_map_[name] = member + @property + def __signature__(cls): + from inspect import Parameter, Signature + if cls._member_names_: + return Signature([Parameter('values', Parameter.VAR_POSITIONAL)]) + else: + return Signature([Parameter('new_class_name', Parameter.POSITIONAL_ONLY), + Parameter('names', Parameter.POSITIONAL_OR_KEYWORD), + Parameter('module', Parameter.KEYWORD_ONLY, default=None), + Parameter('qualname', Parameter.KEYWORD_ONLY, default=None), + Parameter('type', Parameter.KEYWORD_ONLY, default=None), + Parameter('start', Parameter.KEYWORD_ONLY, default=1), + Parameter('boundary', Parameter.KEYWORD_ONLY, default=None)]) + + EnumMeta = EnumType # keep EnumMeta name for backwards compatibility @@ -1129,13 +1144,6 @@ class Enum(metaclass=EnumType): attributes -- see the documentation for details. """ - @classmethod - def __signature__(cls): - if cls._member_names_: - return '(*values)' - else: - return '(new_class_name, /, names, *, module=None, qualname=None, type=None, start=1, boundary=None)' - def __new__(cls, value): # all enum instances are actually created during class construction # without calling this method; this method is called by the metaclass' diff --git a/Lib/inspect.py b/Lib/inspect.py index 9ec20a29f84fdf..39376ed25dd501 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2576,18 +2576,10 @@ def _signature_from_callable(obj, *, pass else: if sig is not None: - # since __text_signature__ is not writable on classes, __signature__ - # may contain text (or be a callable that returns text); - # if so, convert it - o_sig = sig - if not isinstance(sig, (Signature, str)) and callable(sig): - sig = sig() - if isinstance(sig, str): - sig = _signature_fromstr(sigcls, obj, sig) if not isinstance(sig, Signature): raise TypeError( 'unexpected object {!r} in __signature__ ' - 'attribute'.format(o_sig)) + 'attribute'.format(sig)) return sig try: diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 6655fff40db198..5523adc5868ef6 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -4572,38 +4572,6 @@ def foo(): pass self.assertEqual(signature_func(foo), inspect.Signature()) self.assertEqual(inspect.get_annotations(foo), {}) - def test_signature_as_str(self): - self.maxDiff = None - class S: - __signature__ = '(a, b=2)' - - self.assertEqual(self.signature(S), - ((('a', ..., ..., 'positional_or_keyword'), - ('b', 2, ..., 'positional_or_keyword')), - ...)) - - def test_signature_as_callable(self): - # __signature__ should be either a staticmethod or a bound classmethod - class S: - @classmethod - def __signature__(cls): - return '(a, b=2)' - - self.assertEqual(self.signature(S), - ((('a', ..., ..., 'positional_or_keyword'), - ('b', 2, ..., 'positional_or_keyword')), - ...)) - - class S: - @staticmethod - def __signature__(): - return '(a, b=2)' - - self.assertEqual(self.signature(S), - ((('a', ..., ..., 'positional_or_keyword'), - ('b', 2, ..., 'positional_or_keyword')), - ...)) - def test_signature_on_derived_classes(self): # gh-105080: Make sure that signatures are consistent on derived classes diff --git a/Misc/NEWS.d/next/Library/2024-02-27-10-22-15.gh-issue-115937.0cVNur.rst b/Misc/NEWS.d/next/Library/2024-02-27-10-22-15.gh-issue-115937.0cVNur.rst new file mode 100644 index 00000000000000..f9dae0c9b8e2b2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-02-27-10-22-15.gh-issue-115937.0cVNur.rst @@ -0,0 +1,3 @@ +Removed extra preprocessing for the ``__signature__`` attribute: the code +just check if it's a :class:`inspect.Signature` instance. Patch by Sergey B +Kirpichev. From 494d4e7996291f9f888d75a52f76ad6fc2a73d0e Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Fri, 19 Jul 2024 08:51:49 +0300 Subject: [PATCH 09/13] Revert "gh-114949: fix signature in the type.__prepare__() doc string" This reverts commit 3dc4125aae9dc3b20899a2a5c42af657adea92bc. --- Objects/typeobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/typeobject.c b/Objects/typeobject.c index ef5e4a11418ead..12125b77a0824d 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6051,7 +6051,7 @@ static PyMethodDef type_methods[] = { TYPE___SUBCLASSES___METHODDEF {"__prepare__", _PyCFunction_CAST(type_prepare), METH_FASTCALL | METH_KEYWORDS | METH_CLASS, - PyDoc_STR("__prepare__($type, name, bases, /, **kwds)\n" + PyDoc_STR("__prepare__($cls, name, bases, /, **kwds)\n" "--\n" "\n" "Create the namespace for the class statement")}, From 38b7d53e331ce4fc361f1fd363082e01b65f2792 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Fri, 19 Jul 2024 08:52:00 +0300 Subject: [PATCH 10/13] Revert "gh-115231: fill __module__ for built-in class/staticmethods" This reverts commit ff758ab8faaccbd9f5301d24965cac805019d7c0. --- Lib/test/test_funcattrs.py | 23 ------------------- ...-02-10-05-42-26.gh-issue-115231.6T7dzi.rst | 2 -- Objects/descrobject.c | 6 +---- Objects/typeobject.c | 5 +--- 4 files changed, 2 insertions(+), 34 deletions(-) delete mode 100644 Misc/NEWS.d/next/Core and Builtins/2024-02-10-05-42-26.gh-issue-115231.6T7dzi.rst diff --git a/Lib/test/test_funcattrs.py b/Lib/test/test_funcattrs.py index 8a4d4ae6663653..b3fc5ad42e7fde 100644 --- a/Lib/test/test_funcattrs.py +++ b/Lib/test/test_funcattrs.py @@ -453,29 +453,6 @@ class BuiltinFunctionPropertiesTest(unittest.TestCase): # XXX Not sure where this should really go since I can't find a # test module specifically for builtin_function_or_method. - def test_builtin__module__(self): - import decimal - import math - - # builtin function: - self.assertEqual(len.__module__, 'builtins') - self.assertEqual(math.sin.__module__, 'math') - - # instance method: - self.assertRaises(AttributeError, getattr, int.to_bytes, '__module__') - self.assertRaises(AttributeError, getattr, decimal.Decimal.exp, '__module__') - self.assertEqual(int.to_bytes.__objclass__.__module__, 'builtins') - self.assertEqual(decimal.Decimal.exp.__objclass__.__module__, 'decimal') - - # builtin classmethod: - self.assertEqual(int.from_bytes.__module__, 'builtins') - self.assertEqual(int.from_bytes.__self__.__module__, 'builtins') - self.assertEqual(decimal.Decimal.from_float.__module__, 'decimal') - self.assertEqual(decimal.Decimal.from_float.__self__.__module__, 'decimal') - - # builtin staticmethod: - self.assertEqual(bytes.maketrans.__module__, 'builtins') - def test_builtin__qualname__(self): import time diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-02-10-05-42-26.gh-issue-115231.6T7dzi.rst b/Misc/NEWS.d/next/Core and Builtins/2024-02-10-05-42-26.gh-issue-115231.6T7dzi.rst deleted file mode 100644 index 12c622f6c086c4..00000000000000 --- a/Misc/NEWS.d/next/Core and Builtins/2024-02-10-05-42-26.gh-issue-115231.6T7dzi.rst +++ /dev/null @@ -1,2 +0,0 @@ -Setup ``__module__`` attribute for built-in class/static methods. Patch by -Sergey B Kirpichev. diff --git a/Objects/descrobject.c b/Objects/descrobject.c index d280bf7adfc8e6..4eccd1704eb95a 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -130,11 +130,7 @@ classmethod_get(PyObject *self, PyObject *obj, PyObject *type) if (descr->d_method->ml_flags & METH_METHOD) { cls = descr->d_common.d_type; } - PyObject *mod = PyObject_GetAttr((PyObject*)type, &_Py_ID(__module__)); - PyErr_Clear(); - PyObject *result = PyCMethod_New(descr->d_method, type, mod, cls); - Py_XDECREF(mod); - return result; + return PyCMethod_New(descr->d_method, type, NULL, cls); } static PyObject * diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 12125b77a0824d..754f0cde24163c 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -7473,10 +7473,7 @@ type_add_method(PyTypeObject *type, PyMethodDef *meth) descr = PyDescr_NewClassMethod(type, meth); } else if (meth->ml_flags & METH_STATIC) { - PyObject *mod = PyObject_GetAttr((PyObject*)type, &_Py_ID(__module__)); - PyErr_Clear(); - PyObject *cfunc = PyCFunction_NewEx(meth, (PyObject*)type, mod); - Py_XDECREF(mod); + PyObject *cfunc = PyCFunction_NewEx(meth, (PyObject*)type, NULL); if (cfunc == NULL) { return -1; } From 1173cac9fce8f27224e3d551858fdb8e5e69c67b Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Fri, 19 Jul 2024 08:53:23 +0300 Subject: [PATCH 11/13] Revert "gh-116110: remove extra processing for the __signature__ attribute" This reverts commit 1f3c7f30f6342cf6b72ca601d39e1d06f4137e40. --- Lib/enum.py | 22 ++++--------- Lib/inspect.py | 10 +++++- Lib/test/test_inspect/test_inspect.py | 32 +++++++++++++++++++ ...-02-27-10-22-15.gh-issue-115937.0cVNur.rst | 3 -- 4 files changed, 48 insertions(+), 19 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2024-02-27-10-22-15.gh-issue-115937.0cVNur.rst diff --git a/Lib/enum.py b/Lib/enum.py index 4d297047b57678..c36fc75a24a239 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -1092,21 +1092,6 @@ def _add_member_(cls, name, member): # now add to _member_map_ (even aliases) cls._member_map_[name] = member - @property - def __signature__(cls): - from inspect import Parameter, Signature - if cls._member_names_: - return Signature([Parameter('values', Parameter.VAR_POSITIONAL)]) - else: - return Signature([Parameter('new_class_name', Parameter.POSITIONAL_ONLY), - Parameter('names', Parameter.POSITIONAL_OR_KEYWORD), - Parameter('module', Parameter.KEYWORD_ONLY, default=None), - Parameter('qualname', Parameter.KEYWORD_ONLY, default=None), - Parameter('type', Parameter.KEYWORD_ONLY, default=None), - Parameter('start', Parameter.KEYWORD_ONLY, default=1), - Parameter('boundary', Parameter.KEYWORD_ONLY, default=None)]) - - EnumMeta = EnumType # keep EnumMeta name for backwards compatibility @@ -1150,6 +1135,13 @@ class Enum(metaclass=EnumType): attributes -- see the documentation for details. """ + @classmethod + def __signature__(cls): + if cls._member_names_: + return '(*values)' + else: + return '(new_class_name, /, names, *, module=None, qualname=None, type=None, start=1, boundary=None)' + def __new__(cls, value): # all enum instances are actually created during class construction # without calling this method; this method is called by the metaclass' diff --git a/Lib/inspect.py b/Lib/inspect.py index a6f28df867faba..c01c5593029766 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2528,10 +2528,18 @@ def _signature_from_callable(obj, *, pass else: if sig is not None: + # since __text_signature__ is not writable on classes, __signature__ + # may contain text (or be a callable that returns text); + # if so, convert it + o_sig = sig + if not isinstance(sig, (Signature, str)) and callable(sig): + sig = sig() + if isinstance(sig, str): + sig = _signature_fromstr(sigcls, obj, sig) if not isinstance(sig, Signature): raise TypeError( 'unexpected object {!r} in __signature__ ' - 'attribute'.format(sig)) + 'attribute'.format(o_sig)) return sig try: diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 8171f26a58fd7a..f9629260c516bf 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -4894,6 +4894,38 @@ def foo(): pass self.assertEqual(signature_func(foo), inspect.Signature()) self.assertEqual(inspect.get_annotations(foo), {}) + def test_signature_as_str(self): + self.maxDiff = None + class S: + __signature__ = '(a, b=2)' + + self.assertEqual(self.signature(S), + ((('a', ..., ..., 'positional_or_keyword'), + ('b', 2, ..., 'positional_or_keyword')), + ...)) + + def test_signature_as_callable(self): + # __signature__ should be either a staticmethod or a bound classmethod + class S: + @classmethod + def __signature__(cls): + return '(a, b=2)' + + self.assertEqual(self.signature(S), + ((('a', ..., ..., 'positional_or_keyword'), + ('b', 2, ..., 'positional_or_keyword')), + ...)) + + class S: + @staticmethod + def __signature__(): + return '(a, b=2)' + + self.assertEqual(self.signature(S), + ((('a', ..., ..., 'positional_or_keyword'), + ('b', 2, ..., 'positional_or_keyword')), + ...)) + def test_signature_on_derived_classes(self): # gh-105080: Make sure that signatures are consistent on derived classes diff --git a/Misc/NEWS.d/next/Library/2024-02-27-10-22-15.gh-issue-115937.0cVNur.rst b/Misc/NEWS.d/next/Library/2024-02-27-10-22-15.gh-issue-115937.0cVNur.rst deleted file mode 100644 index f9dae0c9b8e2b2..00000000000000 --- a/Misc/NEWS.d/next/Library/2024-02-27-10-22-15.gh-issue-115937.0cVNur.rst +++ /dev/null @@ -1,3 +0,0 @@ -Removed extra preprocessing for the ``__signature__`` attribute: the code -just check if it's a :class:`inspect.Signature` instance. Patch by Sergey B -Kirpichev. From 76ab235e455ae1b45fcf53280673ca6b5f160f35 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Fri, 19 Jul 2024 08:55:46 +0300 Subject: [PATCH 12/13] - news --- .../next/Library/2024-02-11-07-31-43.gh-issue-82062.eeS6w7.rst | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2024-02-11-07-31-43.gh-issue-82062.eeS6w7.rst diff --git a/Misc/NEWS.d/next/Library/2024-02-11-07-31-43.gh-issue-82062.eeS6w7.rst b/Misc/NEWS.d/next/Library/2024-02-11-07-31-43.gh-issue-82062.eeS6w7.rst deleted file mode 100644 index b93de45b1d66a1..00000000000000 --- a/Misc/NEWS.d/next/Library/2024-02-11-07-31-43.gh-issue-82062.eeS6w7.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix :func:`inspect.signature()` to correctly set extension module for -built-in instance methods. From 9c7c0edcdbf478052eff5c94b0a683dbd45f92e1 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Fri, 19 Jul 2024 08:57:26 +0300 Subject: [PATCH 13/13] revert test_misc.py --- Lib/test/test_capi/test_misc.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py index 6e1023a6ff4ff5..9de97c0c2c776a 100644 --- a/Lib/test/test_capi/test_misc.py +++ b/Lib/test/test_capi/test_misc.py @@ -6,7 +6,6 @@ import contextlib import importlib.machinery import importlib.util -import inspect import json import os import pickle @@ -184,12 +183,6 @@ def test_docstring_signature_parsing(self): self.assertEqual(_testcapi.docstring_with_signature_and_extra_newlines.__text_signature__, "($module, /, parameter)") - @unittest.skipIf(MISSING_C_DOCSTRINGS, - "Signature information for builtins requires docstrings") - def test_signature_parsing_with_defaults(self): - meth = _testcapi.DocStringUnrepresentableSignatureTest.with_default - self.assertEqual(str(inspect.signature(meth)), '(self, /, x=1)') - def test_c_type_with_matrix_multiplication(self): M = _testcapi.matmulType m1 = M()