From 507a574de31a1bd7fed8ba4f04afa285d985109b Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 9 Apr 2021 17:51:22 +0200 Subject: bpo-43682: @staticmethod inherits attributes (GH-25268) Static methods (@staticmethod) and class methods (@classmethod) now inherit the method attributes (__module__, __name__, __qualname__, __doc__, __annotations__) and have a new __wrapped__ attribute. Changes: * Add a repr() method to staticmethod and classmethod types. * Add tests on the @classmethod decorator. --- Doc/library/functions.rst | 10 +++ Doc/whatsnew/3.10.rst | 6 ++ Lib/test/test_decorators.py | 26 +++++-- Lib/test/test_descr.py | 18 +++-- Lib/test/test_pydoc.py | 6 +- Lib/test/test_reprlib.py | 4 +- .../2021-04-08-01-06-22.bpo-43682.eUn4p5.rst | 5 ++ Objects/funcobject.c | 80 +++++++++++++++++++--- 8 files changed, 133 insertions(+), 22 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2021-04-08-01-06-22.bpo-43682.eUn4p5.rst diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index 5cb1df9..dca8b93 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -269,6 +269,11 @@ are always available. They are listed here in alphabetical order. Class methods can now wrap other :term:`descriptors ` such as :func:`property`. + .. versionchanged:: 3.10 + Class methods now inherit the method attributes (``__module__``, + ``__name__``, ``__qualname__``, ``__doc__`` and ``__annotations__``) and + have a new ``__wrapped__`` attribute. + .. function:: compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1) Compile the *source* into a code or AST object. Code objects can be executed @@ -1632,6 +1637,11 @@ are always available. They are listed here in alphabetical order. For more information on static methods, see :ref:`types`. + .. versionchanged:: 3.10 + Static methods now inherit the method attributes (``__module__``, + ``__name__``, ``__qualname__``, ``__doc__`` and ``__annotations__``) and + have a new ``__wrapped__`` attribute. + .. index:: single: string; str() (built-in function) diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index d690463..7cf5576 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -617,6 +617,12 @@ Other Language Changes respectively. (Contributed by Joshua Bronson, Daniel Pope, and Justin Wang in :issue:`31861`.) +* Static methods (:func:`@staticmethod `) and class methods + (:func:`@classmethod `) now inherit the method attributes + (``__module__``, ``__name__``, ``__qualname__``, ``__doc__``, + ``__annotations__``) and have a new ``__wrapped__`` attribute. + (Contributed by Victor Stinner in :issue:`43682`.) + New Modules =========== diff --git a/Lib/test/test_decorators.py b/Lib/test/test_decorators.py index 298979e..7d0243a 100644 --- a/Lib/test/test_decorators.py +++ b/Lib/test/test_decorators.py @@ -1,3 +1,4 @@ +from test import support import unittest def funcattrs(**kwds): @@ -76,11 +77,28 @@ class TestDecorators(unittest.TestCase): self.assertEqual(C.foo(), 42) self.assertEqual(C().foo(), 42) - def test_staticmethod_function(self): - @staticmethod - def notamethod(x): + def check_wrapper_attrs(self, method_wrapper, format_str): + def func(x): return x - self.assertRaises(TypeError, notamethod, 1) + wrapper = method_wrapper(func) + + self.assertIs(wrapper.__func__, func) + self.assertIs(wrapper.__wrapped__, func) + + for attr in ('__module__', '__qualname__', '__name__', + '__doc__', '__annotations__'): + self.assertIs(getattr(wrapper, attr), + getattr(func, attr)) + + self.assertEqual(repr(wrapper), format_str.format(func)) + + self.assertRaises(TypeError, wrapper, 1) + + def test_staticmethod(self): + self.check_wrapper_attrs(staticmethod, '') + + def test_classmethod(self): + self.check_wrapper_attrs(classmethod, '') def test_dotted(self): decorators = MiscDecorators() diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 8c75ec3..79d6c4b 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -1545,7 +1545,9 @@ order (MRO) for bases """ self.assertEqual(d.foo(1), (d, 1)) self.assertEqual(D.foo(d, 1), (d, 1)) # Test for a specific crash (SF bug 528132) - def f(cls, arg): return (cls, arg) + def f(cls, arg): + "f docstring" + return (cls, arg) ff = classmethod(f) self.assertEqual(ff.__get__(0, int)(42), (int, 42)) self.assertEqual(ff.__get__(0)(42), (int, 42)) @@ -1571,10 +1573,16 @@ order (MRO) for bases """ self.fail("classmethod shouldn't accept keyword args") cm = classmethod(f) - self.assertEqual(cm.__dict__, {}) + cm_dict = {'__annotations__': {}, + '__doc__': "f docstring", + '__module__': __name__, + '__name__': 'f', + '__qualname__': f.__qualname__} + self.assertEqual(cm.__dict__, cm_dict) + cm.x = 42 self.assertEqual(cm.x, 42) - self.assertEqual(cm.__dict__, {"x" : 42}) + self.assertEqual(cm.__dict__, {"x" : 42, **cm_dict}) del cm.x self.assertNotHasAttr(cm, "x") @@ -1654,10 +1662,10 @@ order (MRO) for bases """ self.assertEqual(d.foo(1), (d, 1)) self.assertEqual(D.foo(d, 1), (d, 1)) sm = staticmethod(None) - self.assertEqual(sm.__dict__, {}) + self.assertEqual(sm.__dict__, {'__doc__': None}) sm.x = 42 self.assertEqual(sm.x, 42) - self.assertEqual(sm.__dict__, {"x" : 42}) + self.assertEqual(sm.__dict__, {"x" : 42, '__doc__': None}) del sm.x self.assertNotHasAttr(sm, "x") diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py index 61575b5..e94ebd3 100644 --- a/Lib/test/test_pydoc.py +++ b/Lib/test/test_pydoc.py @@ -1142,7 +1142,8 @@ class TestDescriptions(unittest.TestCase): '''A static method''' ... self.assertEqual(self._get_summary_lines(X.__dict__['sm']), - "") + 'sm(...)\n' + ' A static method\n') self.assertEqual(self._get_summary_lines(X.sm), """\ sm(x, y) A static method @@ -1162,7 +1163,8 @@ sm(x, y) '''A class method''' ... self.assertEqual(self._get_summary_lines(X.__dict__['cm']), - "") + 'cm(...)\n' + ' A class method\n') self.assertEqual(self._get_summary_lines(X.cm), """\ cm(x) method of builtins.type instance A class method diff --git a/Lib/test/test_reprlib.py b/Lib/test/test_reprlib.py index a328810..0555b71 100644 --- a/Lib/test/test_reprlib.py +++ b/Lib/test/test_reprlib.py @@ -203,9 +203,9 @@ class ReprTests(unittest.TestCase): class C: def foo(cls): pass x = staticmethod(C.foo) - self.assertTrue(repr(x).startswith('') x = classmethod(C.foo) - self.assertTrue(repr(x).startswith('') def test_unsortable(self): # Repr.repr() used to call sorted() on sets, frozensets and dicts diff --git a/Misc/NEWS.d/next/Core and Builtins/2021-04-08-01-06-22.bpo-43682.eUn4p5.rst b/Misc/NEWS.d/next/Core and Builtins/2021-04-08-01-06-22.bpo-43682.eUn4p5.rst new file mode 100644 index 0000000..ab5873e --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2021-04-08-01-06-22.bpo-43682.eUn4p5.rst @@ -0,0 +1,5 @@ +Static methods (:func:`@staticmethod `) and class methods +(:func:`@classmethod `) now inherit the method attributes +(``__module__``, ``__name__``, ``__qualname__``, ``__doc__``, +``__annotations__``) and have a new ``__wrapped__`` attribute. +Patch by Victor Stinner. diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 45135a8..df59131 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -639,7 +639,7 @@ static PyObject* func_repr(PyFunctionObject *op) { return PyUnicode_FromFormat("", - op->func_qualname, op); + op->func_qualname, op); } static int @@ -715,6 +715,50 @@ PyTypeObject PyFunction_Type = { }; +static int +functools_copy_attr(PyObject *wrapper, PyObject *wrapped, PyObject *name) +{ + PyObject *value = PyObject_GetAttr(wrapped, name); + if (value == NULL) { + if (PyErr_ExceptionMatches(PyExc_AttributeError)) { + PyErr_Clear(); + return 0; + } + return -1; + } + + int res = PyObject_SetAttr(wrapper, name, value); + Py_DECREF(value); + return res; +} + +// Similar to functools.wraps(wrapper, wrapped) +static int +functools_wraps(PyObject *wrapper, PyObject *wrapped) +{ +#define COPY_ATTR(ATTR) \ + do { \ + _Py_IDENTIFIER(ATTR); \ + PyObject *attr = _PyUnicode_FromId(&PyId_ ## ATTR); \ + if (attr == NULL) { \ + return -1; \ + } \ + if (functools_copy_attr(wrapper, wrapped, attr) < 0) { \ + return -1; \ + } \ + } while (0) \ + + COPY_ATTR(__module__); + COPY_ATTR(__name__); + COPY_ATTR(__qualname__); + COPY_ATTR(__doc__); + COPY_ATTR(__annotations__); + return 0; + +#undef COPY_ATTR +} + + /* Class method object */ /* A class method receives the class as implicit first argument, @@ -798,11 +842,16 @@ cm_init(PyObject *self, PyObject *args, PyObject *kwds) return -1; Py_INCREF(callable); Py_XSETREF(cm->cm_callable, callable); + + if (functools_wraps((PyObject *)cm, cm->cm_callable) < 0) { + return -1; + } return 0; } static PyMemberDef cm_memberlist[] = { {"__func__", T_OBJECT, offsetof(classmethod, cm_callable), READONLY}, + {"__wrapped__", T_OBJECT, offsetof(classmethod, cm_callable), READONLY}, {NULL} /* Sentinel */ }; @@ -821,13 +870,17 @@ cm_get___isabstractmethod__(classmethod *cm, void *closure) static PyGetSetDef cm_getsetlist[] = { {"__isabstractmethod__", - (getter)cm_get___isabstractmethod__, NULL, - NULL, - NULL}, + (getter)cm_get___isabstractmethod__, NULL, NULL, NULL}, {"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict, NULL, NULL}, {NULL} /* Sentinel */ }; +static PyObject* +cm_repr(classmethod *cm) +{ + return PyUnicode_FromFormat("", cm->cm_callable); +} + PyDoc_STRVAR(classmethod_doc, "classmethod(function) -> method\n\ \n\ @@ -860,7 +913,7 @@ PyTypeObject PyClassMethod_Type = { 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_as_async */ - 0, /* tp_repr */ + (reprfunc)cm_repr, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ @@ -980,11 +1033,16 @@ sm_init(PyObject *self, PyObject *args, PyObject *kwds) return -1; Py_INCREF(callable); Py_XSETREF(sm->sm_callable, callable); + + if (functools_wraps((PyObject *)sm, sm->sm_callable) < 0) { + return -1; + } return 0; } static PyMemberDef sm_memberlist[] = { {"__func__", T_OBJECT, offsetof(staticmethod, sm_callable), READONLY}, + {"__wrapped__", T_OBJECT, offsetof(staticmethod, sm_callable), READONLY}, {NULL} /* Sentinel */ }; @@ -1003,13 +1061,17 @@ sm_get___isabstractmethod__(staticmethod *sm, void *closure) static PyGetSetDef sm_getsetlist[] = { {"__isabstractmethod__", - (getter)sm_get___isabstractmethod__, NULL, - NULL, - NULL}, + (getter)sm_get___isabstractmethod__, NULL, NULL, NULL}, {"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict, NULL, NULL}, {NULL} /* Sentinel */ }; +static PyObject* +sm_repr(staticmethod *sm) +{ + return PyUnicode_FromFormat("", sm->sm_callable); +} + PyDoc_STRVAR(staticmethod_doc, "staticmethod(function) -> method\n\ \n\ @@ -1040,7 +1102,7 @@ PyTypeObject PyStaticMethod_Type = { 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_as_async */ - 0, /* tp_repr */ + (reprfunc)sm_repr, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ -- cgit v0.12