Skip to content

Commit 507a574

Browse files
authored
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.
1 parent 150af75 commit 507a574

File tree

8 files changed

+133
-22
lines changed

8 files changed

+133
-22
lines changed

Doc/library/functions.rst

+10
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,11 @@ are always available. They are listed here in alphabetical order.
269269
Class methods can now wrap other :term:`descriptors <descriptor>` such as
270270
:func:`property`.
271271

272+
.. versionchanged:: 3.10
273+
Class methods now inherit the method attributes (``__module__``,
274+
``__name__``, ``__qualname__``, ``__doc__`` and ``__annotations__``) and
275+
have a new ``__wrapped__`` attribute.
276+
272277
.. function:: compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1)
273278

274279
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.
16321637

16331638
For more information on static methods, see :ref:`types`.
16341639

1640+
.. versionchanged:: 3.10
1641+
Static methods now inherit the method attributes (``__module__``,
1642+
``__name__``, ``__qualname__``, ``__doc__`` and ``__annotations__``) and
1643+
have a new ``__wrapped__`` attribute.
1644+
16351645

16361646
.. index::
16371647
single: string; str() (built-in function)

Doc/whatsnew/3.10.rst

+6
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,12 @@ Other Language Changes
617617
respectively.
618618
(Contributed by Joshua Bronson, Daniel Pope, and Justin Wang in :issue:`31861`.)
619619
620+
* Static methods (:func:`@staticmethod <staticmethod>`) and class methods
621+
(:func:`@classmethod <classmethod>`) now inherit the method attributes
622+
(``__module__``, ``__name__``, ``__qualname__``, ``__doc__``,
623+
``__annotations__``) and have a new ``__wrapped__`` attribute.
624+
(Contributed by Victor Stinner in :issue:`43682`.)
625+
620626
621627
New Modules
622628
===========

Lib/test/test_decorators.py

+22-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from test import support
12
import unittest
23

34
def funcattrs(**kwds):
@@ -76,11 +77,28 @@ def foo(): return 42
7677
self.assertEqual(C.foo(), 42)
7778
self.assertEqual(C().foo(), 42)
7879

79-
def test_staticmethod_function(self):
80-
@staticmethod
81-
def notamethod(x):
80+
def check_wrapper_attrs(self, method_wrapper, format_str):
81+
def func(x):
8282
return x
83-
self.assertRaises(TypeError, notamethod, 1)
83+
wrapper = method_wrapper(func)
84+
85+
self.assertIs(wrapper.__func__, func)
86+
self.assertIs(wrapper.__wrapped__, func)
87+
88+
for attr in ('__module__', '__qualname__', '__name__',
89+
'__doc__', '__annotations__'):
90+
self.assertIs(getattr(wrapper, attr),
91+
getattr(func, attr))
92+
93+
self.assertEqual(repr(wrapper), format_str.format(func))
94+
95+
self.assertRaises(TypeError, wrapper, 1)
96+
97+
def test_staticmethod(self):
98+
self.check_wrapper_attrs(staticmethod, '<staticmethod({!r})>')
99+
100+
def test_classmethod(self):
101+
self.check_wrapper_attrs(classmethod, '<classmethod({!r})>')
84102

85103
def test_dotted(self):
86104
decorators = MiscDecorators()

Lib/test/test_descr.py

+13-5
Original file line numberDiff line numberDiff line change
@@ -1545,7 +1545,9 @@ class D(C):
15451545
self.assertEqual(d.foo(1), (d, 1))
15461546
self.assertEqual(D.foo(d, 1), (d, 1))
15471547
# Test for a specific crash (SF bug 528132)
1548-
def f(cls, arg): return (cls, arg)
1548+
def f(cls, arg):
1549+
"f docstring"
1550+
return (cls, arg)
15491551
ff = classmethod(f)
15501552
self.assertEqual(ff.__get__(0, int)(42), (int, 42))
15511553
self.assertEqual(ff.__get__(0)(42), (int, 42))
@@ -1571,10 +1573,16 @@ def f(cls, arg): return (cls, arg)
15711573
self.fail("classmethod shouldn't accept keyword args")
15721574

15731575
cm = classmethod(f)
1574-
self.assertEqual(cm.__dict__, {})
1576+
cm_dict = {'__annotations__': {},
1577+
'__doc__': "f docstring",
1578+
'__module__': __name__,
1579+
'__name__': 'f',
1580+
'__qualname__': f.__qualname__}
1581+
self.assertEqual(cm.__dict__, cm_dict)
1582+
15751583
cm.x = 42
15761584
self.assertEqual(cm.x, 42)
1577-
self.assertEqual(cm.__dict__, {"x" : 42})
1585+
self.assertEqual(cm.__dict__, {"x" : 42, **cm_dict})
15781586
del cm.x
15791587
self.assertNotHasAttr(cm, "x")
15801588

@@ -1654,10 +1662,10 @@ class D(C):
16541662
self.assertEqual(d.foo(1), (d, 1))
16551663
self.assertEqual(D.foo(d, 1), (d, 1))
16561664
sm = staticmethod(None)
1657-
self.assertEqual(sm.__dict__, {})
1665+
self.assertEqual(sm.__dict__, {'__doc__': None})
16581666
sm.x = 42
16591667
self.assertEqual(sm.x, 42)
1660-
self.assertEqual(sm.__dict__, {"x" : 42})
1668+
self.assertEqual(sm.__dict__, {"x" : 42, '__doc__': None})
16611669
del sm.x
16621670
self.assertNotHasAttr(sm, "x")
16631671

Lib/test/test_pydoc.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1142,7 +1142,8 @@ def sm(x, y):
11421142
'''A static method'''
11431143
...
11441144
self.assertEqual(self._get_summary_lines(X.__dict__['sm']),
1145-
"<staticmethod object>")
1145+
'sm(...)\n'
1146+
' A static method\n')
11461147
self.assertEqual(self._get_summary_lines(X.sm), """\
11471148
sm(x, y)
11481149
A static method
@@ -1162,7 +1163,8 @@ def cm(cls, x):
11621163
'''A class method'''
11631164
...
11641165
self.assertEqual(self._get_summary_lines(X.__dict__['cm']),
1165-
"<classmethod object>")
1166+
'cm(...)\n'
1167+
' A class method\n')
11661168
self.assertEqual(self._get_summary_lines(X.cm), """\
11671169
cm(x) method of builtins.type instance
11681170
A class method

Lib/test/test_reprlib.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,9 @@ def test_descriptors(self):
203203
class C:
204204
def foo(cls): pass
205205
x = staticmethod(C.foo)
206-
self.assertTrue(repr(x).startswith('<staticmethod object at 0x'))
206+
self.assertEqual(repr(x), f'<staticmethod({C.foo!r})>')
207207
x = classmethod(C.foo)
208-
self.assertTrue(repr(x).startswith('<classmethod object at 0x'))
208+
self.assertEqual(repr(x), f'<classmethod({C.foo!r})>')
209209

210210
def test_unsortable(self):
211211
# Repr.repr() used to call sorted() on sets, frozensets and dicts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Static methods (:func:`@staticmethod <staticmethod>`) and class methods
2+
(:func:`@classmethod <classmethod>`) now inherit the method attributes
3+
(``__module__``, ``__name__``, ``__qualname__``, ``__doc__``,
4+
``__annotations__``) and have a new ``__wrapped__`` attribute.
5+
Patch by Victor Stinner.

Objects/funcobject.c

+71-9
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,7 @@ static PyObject*
639639
func_repr(PyFunctionObject *op)
640640
{
641641
return PyUnicode_FromFormat("<function %U at %p>",
642-
op->func_qualname, op);
642+
op->func_qualname, op);
643643
}
644644

645645
static int
@@ -715,6 +715,50 @@ PyTypeObject PyFunction_Type = {
715715
};
716716

717717

718+
static int
719+
functools_copy_attr(PyObject *wrapper, PyObject *wrapped, PyObject *name)
720+
{
721+
PyObject *value = PyObject_GetAttr(wrapped, name);
722+
if (value == NULL) {
723+
if (PyErr_ExceptionMatches(PyExc_AttributeError)) {
724+
PyErr_Clear();
725+
return 0;
726+
}
727+
return -1;
728+
}
729+
730+
int res = PyObject_SetAttr(wrapper, name, value);
731+
Py_DECREF(value);
732+
return res;
733+
}
734+
735+
// Similar to functools.wraps(wrapper, wrapped)
736+
static int
737+
functools_wraps(PyObject *wrapper, PyObject *wrapped)
738+
{
739+
#define COPY_ATTR(ATTR) \
740+
do { \
741+
_Py_IDENTIFIER(ATTR); \
742+
PyObject *attr = _PyUnicode_FromId(&PyId_ ## ATTR); \
743+
if (attr == NULL) { \
744+
return -1; \
745+
} \
746+
if (functools_copy_attr(wrapper, wrapped, attr) < 0) { \
747+
return -1; \
748+
} \
749+
} while (0) \
750+
751+
COPY_ATTR(__module__);
752+
COPY_ATTR(__name__);
753+
COPY_ATTR(__qualname__);
754+
COPY_ATTR(__doc__);
755+
COPY_ATTR(__annotations__);
756+
return 0;
757+
758+
#undef COPY_ATTR
759+
}
760+
761+
718762
/* Class method object */
719763

720764
/* A class method receives the class as implicit first argument,
@@ -798,11 +842,16 @@ cm_init(PyObject *self, PyObject *args, PyObject *kwds)
798842
return -1;
799843
Py_INCREF(callable);
800844
Py_XSETREF(cm->cm_callable, callable);
845+
846+
if (functools_wraps((PyObject *)cm, cm->cm_callable) < 0) {
847+
return -1;
848+
}
801849
return 0;
802850
}
803851

804852
static PyMemberDef cm_memberlist[] = {
805853
{"__func__", T_OBJECT, offsetof(classmethod, cm_callable), READONLY},
854+
{"__wrapped__", T_OBJECT, offsetof(classmethod, cm_callable), READONLY},
806855
{NULL} /* Sentinel */
807856
};
808857

@@ -821,13 +870,17 @@ cm_get___isabstractmethod__(classmethod *cm, void *closure)
821870

822871
static PyGetSetDef cm_getsetlist[] = {
823872
{"__isabstractmethod__",
824-
(getter)cm_get___isabstractmethod__, NULL,
825-
NULL,
826-
NULL},
873+
(getter)cm_get___isabstractmethod__, NULL, NULL, NULL},
827874
{"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict, NULL, NULL},
828875
{NULL} /* Sentinel */
829876
};
830877

878+
static PyObject*
879+
cm_repr(classmethod *cm)
880+
{
881+
return PyUnicode_FromFormat("<classmethod(%R)>", cm->cm_callable);
882+
}
883+
831884
PyDoc_STRVAR(classmethod_doc,
832885
"classmethod(function) -> method\n\
833886
\n\
@@ -860,7 +913,7 @@ PyTypeObject PyClassMethod_Type = {
860913
0, /* tp_getattr */
861914
0, /* tp_setattr */
862915
0, /* tp_as_async */
863-
0, /* tp_repr */
916+
(reprfunc)cm_repr, /* tp_repr */
864917
0, /* tp_as_number */
865918
0, /* tp_as_sequence */
866919
0, /* tp_as_mapping */
@@ -980,11 +1033,16 @@ sm_init(PyObject *self, PyObject *args, PyObject *kwds)
9801033
return -1;
9811034
Py_INCREF(callable);
9821035
Py_XSETREF(sm->sm_callable, callable);
1036+
1037+
if (functools_wraps((PyObject *)sm, sm->sm_callable) < 0) {
1038+
return -1;
1039+
}
9831040
return 0;
9841041
}
9851042

9861043
static PyMemberDef sm_memberlist[] = {
9871044
{"__func__", T_OBJECT, offsetof(staticmethod, sm_callable), READONLY},
1045+
{"__wrapped__", T_OBJECT, offsetof(staticmethod, sm_callable), READONLY},
9881046
{NULL} /* Sentinel */
9891047
};
9901048

@@ -1003,13 +1061,17 @@ sm_get___isabstractmethod__(staticmethod *sm, void *closure)
10031061

10041062
static PyGetSetDef sm_getsetlist[] = {
10051063
{"__isabstractmethod__",
1006-
(getter)sm_get___isabstractmethod__, NULL,
1007-
NULL,
1008-
NULL},
1064+
(getter)sm_get___isabstractmethod__, NULL, NULL, NULL},
10091065
{"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict, NULL, NULL},
10101066
{NULL} /* Sentinel */
10111067
};
10121068

1069+
static PyObject*
1070+
sm_repr(staticmethod *sm)
1071+
{
1072+
return PyUnicode_FromFormat("<staticmethod(%R)>", sm->sm_callable);
1073+
}
1074+
10131075
PyDoc_STRVAR(staticmethod_doc,
10141076
"staticmethod(function) -> method\n\
10151077
\n\
@@ -1040,7 +1102,7 @@ PyTypeObject PyStaticMethod_Type = {
10401102
0, /* tp_getattr */
10411103
0, /* tp_setattr */
10421104
0, /* tp_as_async */
1043-
0, /* tp_repr */
1105+
(reprfunc)sm_repr, /* tp_repr */
10441106
0, /* tp_as_number */
10451107
0, /* tp_as_sequence */
10461108
0, /* tp_as_mapping */

0 commit comments

Comments
 (0)