[Python-checkins] bpo-43682: @staticmethod inherits attributes (GH-25268)

vstinner webhook-mailer at python.org
Fri Apr 9 11:51:30 EDT 2021


https://github.com/python/cpython/commit/507a574de31a1bd7fed8ba4f04afa285d985109b
commit: 507a574de31a1bd7fed8ba4f04afa285d985109b
branch: master
author: Victor Stinner <vstinner at python.org>
committer: vstinner <vstinner at python.org>
date: 2021-04-09T17:51:22+02:00
summary:

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.

files:
A Misc/NEWS.d/next/Core and Builtins/2021-04-08-01-06-22.bpo-43682.eUn4p5.rst
M Doc/library/functions.rst
M Doc/whatsnew/3.10.rst
M Lib/test/test_decorators.py
M Lib/test/test_descr.py
M Lib/test/test_pydoc.py
M Lib/test/test_reprlib.py
M Objects/funcobject.c

diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst
index 5cb1df93702d6..dca8b9334877d 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 <descriptor>` 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 d690463fe2440..7cf5576765748 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 <staticmethod>`) and class methods
+  (:func:`@classmethod <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 298979e509f8d..7d0243ab19939 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 @@ def foo(): return 42
         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, '<staticmethod({!r})>')
+
+    def test_classmethod(self):
+        self.check_wrapper_attrs(classmethod, '<classmethod({!r})>')
 
     def test_dotted(self):
         decorators = MiscDecorators()
diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py
index 8c75ec304f780..79d6c4b5e7232 100644
--- a/Lib/test/test_descr.py
+++ b/Lib/test/test_descr.py
@@ -1545,7 +1545,9 @@ class D(C):
         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 @@ def f(cls, arg): return (cls, arg)
             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 @@ class D(C):
         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 61575b522a66b..e94ebd30160e0 100644
--- a/Lib/test/test_pydoc.py
+++ b/Lib/test/test_pydoc.py
@@ -1142,7 +1142,8 @@ def sm(x, y):
                 '''A static method'''
                 ...
         self.assertEqual(self._get_summary_lines(X.__dict__['sm']),
-                         "<staticmethod object>")
+                         'sm(...)\n'
+                         '    A static method\n')
         self.assertEqual(self._get_summary_lines(X.sm), """\
 sm(x, y)
     A static method
@@ -1162,7 +1163,8 @@ def cm(cls, x):
                 '''A class method'''
                 ...
         self.assertEqual(self._get_summary_lines(X.__dict__['cm']),
-                         "<classmethod object>")
+                         '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 a328810c21ec6..0555b71bbf21a 100644
--- a/Lib/test/test_reprlib.py
+++ b/Lib/test/test_reprlib.py
@@ -203,9 +203,9 @@ def test_descriptors(self):
         class C:
             def foo(cls): pass
         x = staticmethod(C.foo)
-        self.assertTrue(repr(x).startswith('<staticmethod object at 0x'))
+        self.assertEqual(repr(x), f'<staticmethod({C.foo!r})>')
         x = classmethod(C.foo)
-        self.assertTrue(repr(x).startswith('<classmethod object at 0x'))
+        self.assertEqual(repr(x), f'<classmethod({C.foo!r})>')
 
     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 0000000000000..ab5873edbd70f
--- /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 <staticmethod>`) and class methods
+(:func:`@classmethod <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 45135a8c98a70..df59131912190 100644
--- a/Objects/funcobject.c
+++ b/Objects/funcobject.c
@@ -639,7 +639,7 @@ static PyObject*
 func_repr(PyFunctionObject *op)
 {
     return PyUnicode_FromFormat("<function %U at %p>",
-                               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("<classmethod(%R)>", 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("<staticmethod(%R)>", 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 */



More information about the Python-checkins mailing list