https://github.com/python/cpython/commit/ffa68529b40210d4c55ad3a4114e48007ae0e55a
commit: ffa68529b40210d4c55ad3a4114e48007ae0e55a
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-02-05T11:32:56+01:00
summary:
gh-144330: Initialize classmethod and staticmethod in new (#144469)
Move classmethod and staticmethod initialization from __init__() to
__new__().
PyClassMethod_New() and PyStaticMethod_New() now copy attributes of
the wrapped functions: __module__, __name__, __qualname__ and
__doc__.
Change static type initialization: initialize PyStaticMethod_Type and
PyCFunction_Type earlier.
Remove test_refleaks_in_classmethod___init__() and
test_refleaks_in_staticmethod___init__() tests from test_descr since
classmethod and staticmethod have no __init__() method anymore.
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-02-04-11-19-45.gh-issue-144330.kOowSb.rst
M Lib/test/test_descr.py
M Objects/funcobject.c
M Objects/object.c
diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py
index 0dc61ca7fb0da3..d6e3719479a214 100644
--- a/Lib/test/test_descr.py
+++ b/Lib/test/test_descr.py
@@ -1727,6 +1727,18 @@ def annotated(cls) -> int: pass
del method.__annotate__
self.assertIs(method.__annotate__, original_annotate)
+ def test_classmethod_without_dict_access(self):
+ class Spam:
+ @classmethod
+ def method(cls, x, y):
+ pass
+
+ obj = Spam.__dict__['method']
+ self.assertIsInstance(obj, classmethod)
+ self.assertEqual(obj.__annotations__, {})
+ self.assertEqual(obj.__name__, 'method')
+ self.assertEqual(obj.__module__, __name__)
+
def test_staticmethod_annotations_without_dict_access(self):
# gh-125017: this used to crash
class Spam:
@@ -1737,15 +1749,8 @@ def __new__(cls, x, y):
obj = Spam.__dict__['__new__']
self.assertIsInstance(obj, staticmethod)
self.assertEqual(obj.__annotations__, {})
-
- @support.refcount_test
- def test_refleaks_in_classmethod___init__(self):
- gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount')
- cm = classmethod(None)
- refs_before = gettotalrefcount()
- for i in range(100):
- cm.__init__(None)
- self.assertAlmostEqual(gettotalrefcount() - refs_before, 0, delta=10)
+ self.assertEqual(obj.__name__, '__new__')
+ self.assertEqual(obj.__module__, __name__)
@support.impl_detail("the module 'xxsubtype' is internal")
@unittest.skipIf(xxsubtype is None, "requires xxsubtype module")
@@ -1822,15 +1827,6 @@ class D(C):
del sm.x
self.assertNotHasAttr(sm, "x")
- @support.refcount_test
- def test_refleaks_in_staticmethod___init__(self):
- gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount')
- sm = staticmethod(None)
- refs_before = gettotalrefcount()
- for i in range(100):
- sm.__init__(None)
- self.assertAlmostEqual(gettotalrefcount() - refs_before, 0, delta=10)
-
@support.impl_detail("the module 'xxsubtype' is internal")
@unittest.skipIf(xxsubtype is None, "requires xxsubtype module")
def test_staticmethods_in_c(self):
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-04-11-19-45.gh-issue-144330.kOowSb.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-04-11-19-45.gh-issue-144330.kOowSb.rst
new file mode 100644
index 00000000000000..b3c61e162f4ffb
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-04-11-19-45.gh-issue-144330.kOowSb.rst
@@ -0,0 +1,2 @@
+Move ``classmethod`` and ``staticmethod`` initialization from ``__init__()``
+to ``__new__()``. Patch by Victor Stinner.
diff --git a/Objects/funcobject.c b/Objects/funcobject.c
index b659ac8023373b..2bf21fa045e3f1 100644
--- a/Objects/funcobject.c
+++ b/Objects/funcobject.c
@@ -1466,33 +1466,59 @@ static PyObject *
cm_descr_get(PyObject *self, PyObject *obj, PyObject *type)
{
classmethod *cm = (classmethod *)self;
-
- if (cm->cm_callable == NULL) {
- PyErr_SetString(PyExc_RuntimeError,
- "uninitialized classmethod object");
- return NULL;
- }
if (type == NULL)
type = (PyObject *)(Py_TYPE(obj));
return PyMethod_New(cm->cm_callable, type);
}
static int
-cm_init(PyObject *self, PyObject *args, PyObject *kwds)
+cm_set_callable(classmethod *cm, PyObject *callable)
{
- classmethod *cm = (classmethod *)self;
- PyObject *callable;
+ assert(callable != NULL);
+ if (cm->cm_callable == callable) {
+ // cm_init() sets the same callable than cm_new()
+ return 0;
+ }
- if (!_PyArg_NoKeywords("classmethod", kwds))
- return -1;
- if (!PyArg_UnpackTuple(args, "classmethod", 1, 1, &callable))
- return -1;
Py_XSETREF(cm->cm_callable, Py_NewRef(callable));
+ return functools_wraps((PyObject *)cm, cm->cm_callable);
+}
+
+static PyObject *
+cm_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+ if (!_PyArg_NoKeywords("classmethod", kwds)) {
+ return NULL;
+ }
+ PyObject *callable; // borrowed ref
+ if (!PyArg_UnpackTuple(args, "classmethod", 1, 1, &callable)) {
+ return NULL;
+ }
- if (functools_wraps((PyObject *)cm, cm->cm_callable) < 0) {
+ classmethod *cm = (classmethod *)PyType_GenericAlloc(type, 0);
+ if (cm == NULL) {
+ return NULL;
+ }
+ if (cm_set_callable(cm, callable) < 0) {
+ Py_DECREF(cm);
+ return NULL;
+ }
+ return (PyObject *)cm;
+}
+
+static int
+cm_init(PyObject *self, PyObject *args, PyObject *kwds)
+{
+ if (!_PyArg_NoKeywords("classmethod", kwds)) {
return -1;
}
- return 0;
+ PyObject *callable; // borrowed ref
+ if (!PyArg_UnpackTuple(args, "classmethod", 1, 1, &callable)) {
+ return -1;
+ }
+
+ classmethod *cm = (classmethod *)self;
+ return cm_set_callable(cm, callable);
}
static PyMemberDef cm_memberlist[] = {
@@ -1623,7 +1649,7 @@ PyTypeObject PyClassMethod_Type = {
offsetof(classmethod, cm_dict), /* tp_dictoffset */
cm_init, /* tp_init */
PyType_GenericAlloc, /* tp_alloc */
- PyType_GenericNew, /* tp_new */
+ cm_new, /* tp_new */
PyObject_GC_Del, /* tp_free */
};
@@ -1632,8 +1658,12 @@ PyClassMethod_New(PyObject *callable)
{
classmethod *cm = (classmethod *)
PyType_GenericAlloc(&PyClassMethod_Type, 0);
- if (cm != NULL) {
- cm->cm_callable = Py_NewRef(callable);
+ if (cm == NULL) {
+ return NULL;
+ }
+ if (cm_set_callable(cm, callable) < 0) {
+ Py_DECREF(cm);
+ return NULL;
}
return (PyObject *)cm;
}
@@ -1699,31 +1729,57 @@ static PyObject *
sm_descr_get(PyObject *self, PyObject *obj, PyObject *type)
{
staticmethod *sm = (staticmethod *)self;
+ return Py_NewRef(sm->sm_callable);
+}
- if (sm->sm_callable == NULL) {
- PyErr_SetString(PyExc_RuntimeError,
- "uninitialized staticmethod object");
+static int
+sm_set_callable(staticmethod *sm, PyObject *callable)
+{
+ assert(callable != NULL);
+ if (sm->sm_callable == callable) {
+ // sm_init() sets the same callable than sm_new()
+ return 0;
+ }
+
+ Py_XSETREF(sm->sm_callable, Py_NewRef(callable));
+ return functools_wraps((PyObject *)sm, sm->sm_callable);
+}
+
+static PyObject *
+sm_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+ if (!_PyArg_NoKeywords("staticmethod", kwds)) {
return NULL;
}
- return Py_NewRef(sm->sm_callable);
+ PyObject *callable; // borrowed ref
+ if (!PyArg_UnpackTuple(args, "staticmethod", 1, 1, &callable)) {
+ return NULL;
+ }
+
+ staticmethod *sm = (staticmethod *)PyType_GenericAlloc(type, 0);
+ if (sm == NULL) {
+ return NULL;
+ }
+ if (sm_set_callable(sm, callable) < 0) {
+ Py_DECREF(sm);
+ return NULL;
+ }
+ return (PyObject *)sm;
}
static int
sm_init(PyObject *self, PyObject *args, PyObject *kwds)
{
- staticmethod *sm = (staticmethod *)self;
- PyObject *callable;
-
- if (!_PyArg_NoKeywords("staticmethod", kwds))
+ if (!_PyArg_NoKeywords("staticmethod", kwds)) {
return -1;
- if (!PyArg_UnpackTuple(args, "staticmethod", 1, 1, &callable))
- return -1;
- Py_XSETREF(sm->sm_callable, Py_NewRef(callable));
-
- if (functools_wraps((PyObject *)sm, sm->sm_callable) < 0) {
+ }
+ PyObject *callable; // borrowed ref
+ if (!PyArg_UnpackTuple(args, "staticmethod", 1, 1, &callable)) {
return -1;
}
- return 0;
+
+ staticmethod *sm = (staticmethod *)self;
+ return sm_set_callable(sm, callable);
}
static PyObject*
@@ -1858,7 +1914,7 @@ PyTypeObject PyStaticMethod_Type = {
offsetof(staticmethod, sm_dict), /* tp_dictoffset */
sm_init, /* tp_init */
PyType_GenericAlloc, /* tp_alloc */
- PyType_GenericNew, /* tp_new */
+ sm_new, /* tp_new */
PyObject_GC_Del, /* tp_free */
};
@@ -1867,8 +1923,12 @@ PyStaticMethod_New(PyObject *callable)
{
staticmethod *sm = (staticmethod *)
PyType_GenericAlloc(&PyStaticMethod_Type, 0);
- if (sm != NULL) {
- sm->sm_callable = Py_NewRef(callable);
+ if (sm == NULL) {
+ return NULL;
+ }
+ if (sm_set_callable(sm, callable) < 0) {
+ Py_DECREF(sm);
+ return NULL;
}
return (PyObject *)sm;
}
diff --git a/Objects/object.c b/Objects/object.c
index 649b109d5cb0bc..38717def24239f 100644
--- a/Objects/object.c
+++ b/Objects/object.c
@@ -2446,13 +2446,17 @@ static PyTypeObject* static_types[] = {
&PyBaseObject_Type,
&PyType_Type,
+ // PyStaticMethod_Type and PyCFunction_Type are used by PyType_Ready()
+ // on other types and so must be initialized first.
+ &PyStaticMethod_Type,
+ &PyCFunction_Type,
+
// Static types with base=&PyBaseObject_Type
&PyAsyncGen_Type,
&PyByteArrayIter_Type,
&PyByteArray_Type,
&PyBytesIter_Type,
&PyBytes_Type,
- &PyCFunction_Type,
&PyCallIter_Type,
&PyCapsule_Type,
&PyCell_Type,
@@ -2509,7 +2513,6 @@ static PyTypeObject* static_types[] = {
&PySetIter_Type,
&PySet_Type,
&PySlice_Type,
- &PyStaticMethod_Type,
&PyStdPrinter_Type,
&PySuper_Type,
&PyTraceBack_Type,
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]