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,