Skip to content

bpo-43682: @staticmethod inherits attributes #25268

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Doc/library/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.10.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
===========
Expand Down
26 changes: 22 additions & 4 deletions Lib/test/test_decorators.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from test import support
import unittest

def funcattrs(**kwds):
Expand Down Expand Up @@ -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()
Expand Down
18 changes: 13 additions & 5 deletions Lib/test/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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")

Expand Down Expand Up @@ -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")

Expand Down
6 changes: 4 additions & 2 deletions Lib/test/test_pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_reprlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
80 changes: 71 additions & 9 deletions Objects/funcobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Comment on lines +721 to +724
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is better to use _PyObject_LookupAttr() here. See #106303.

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,
Expand Down Expand Up @@ -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 */
};

Expand All @@ -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\
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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 */
};

Expand All @@ -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\
Expand Down Expand Up @@ -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 */
Expand Down