From 96a82a73e616d1175e8832b659dc29832a7f30bd Mon Sep 17 00:00:00 2001 From: changjoon-park Date: Mon, 27 Apr 2026 17:27:29 +0900 Subject: [PATCH] Defer staticmethod/classmethod callable storage to __init__ CPython's staticmethod and classmethod set __func__ and copy wrapper attributes (__doc__, __name__, etc.) only inside __init__ (Objects/funcobject.c::sm_init / cm_init). RustPython did this work in slot_new and again in __init__, so subclasses that override __init__ without calling super().__init__() saw __func__ pointing at the original callable instead of None. Move the callable assignment and the wrapper-attribute copy into Initializer::init; slot_new now just validates the signature and stores None for the callable, matching the CPython contract. --- Lib/test/test_descr.py | 2 -- crates/vm/src/builtins/classmethod.rs | 50 ++++++++++++-------------- crates/vm/src/builtins/staticmethod.rs | 27 +++++++------- 3 files changed, 35 insertions(+), 44 deletions(-) diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 615212d6024..1fb477823bf 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -5193,7 +5193,6 @@ def foo(self): with self.assertRaisesRegex(NotImplementedError, "BAR"): B().foo - @unittest.expectedFailure # TODO: RUSTPYTHON; Wrong error message def test_staticmethod_new(self): class MyStaticMethod(staticmethod): def __init__(self, func): @@ -5204,7 +5203,6 @@ def func(): pass self.assertIsNone(sm.__func__) self.assertIsNone(sm.__wrapped__) - @unittest.expectedFailure # TODO: RUSTPYTHON; Wrong error message def test_classmethod_new(self): class MyClassMethod(classmethod): def __init__(self, func): diff --git a/crates/vm/src/builtins/classmethod.rs b/crates/vm/src/builtins/classmethod.rs index 8955d31ce40..f2821c3f16f 100644 --- a/crates/vm/src/builtins/classmethod.rs +++ b/crates/vm/src/builtins/classmethod.rs @@ -66,34 +66,15 @@ impl Constructor for PyClassMethod { type Args = PyObjectRef; fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { - let callable: Self::Args = args.bind(vm)?; - // Create a dictionary to hold copied attributes - let dict = vm.ctx.new_dict(); - - // Copy attributes from the callable to the dict - // This is similar to functools.wraps in CPython - if let Ok(doc) = callable.get_attr("__doc__", vm) { - dict.set_item(identifier!(vm.ctx, __doc__), doc, vm)?; - } - if let Ok(name) = callable.get_attr("__name__", vm) { - dict.set_item(identifier!(vm.ctx, __name__), name, vm)?; - } - if let Ok(qualname) = callable.get_attr("__qualname__", vm) { - dict.set_item(identifier!(vm.ctx, __qualname__), qualname, vm)?; - } - if let Ok(module) = callable.get_attr("__module__", vm) { - dict.set_item(identifier!(vm.ctx, __module__), module, vm)?; - } - if let Ok(annotations) = callable.get_attr("__annotations__", vm) { - dict.set_item(identifier!(vm.ctx, __annotations__), annotations, vm)?; - } - - // Create PyClassMethod instance with the pre-populated dict + // Validate the signature here, but defer storing the callable and + // copying its attributes to `__init__` so that subclasses overriding + // `__init__` without calling `super().__init__()` see `__func__` as + // `None`, matching CPython. + let _: Self::Args = args.bind(vm)?; let classmethod = Self { - callable: PyMutex::new(callable), + callable: PyMutex::new(vm.ctx.none()), }; - - let result = PyRef::new_ref(classmethod, cls, Some(dict)); + let result = PyRef::new_ref(classmethod, cls, Some(vm.ctx.new_dict())); Ok(PyObjectRef::from(result)) } @@ -105,8 +86,21 @@ impl Constructor for PyClassMethod { impl Initializer for PyClassMethod { type Args = PyObjectRef; - fn init(zelf: PyRef, callable: Self::Args, _vm: &VirtualMachine) -> PyResult<()> { - *zelf.callable.lock() = callable; + fn init(zelf: PyRef, callable: Self::Args, vm: &VirtualMachine) -> PyResult<()> { + *zelf.callable.lock() = callable.clone(); + // Copy wrapper attributes from the callable, mirroring functools.wraps. + let dict = zelf.as_object().dict().expect("classmethod has __dict__"); + for attr in [ + identifier!(vm.ctx, __doc__), + identifier!(vm.ctx, __name__), + identifier!(vm.ctx, __qualname__), + identifier!(vm.ctx, __module__), + identifier!(vm.ctx, __annotations__), + ] { + if let Ok(value) = callable.get_attr(attr, vm) { + dict.set_item(attr, value, vm)?; + } + } Ok(()) } } diff --git a/crates/vm/src/builtins/staticmethod.rs b/crates/vm/src/builtins/staticmethod.rs index 2554fa816aa..551e1cb4b88 100644 --- a/crates/vm/src/builtins/staticmethod.rs +++ b/crates/vm/src/builtins/staticmethod.rs @@ -1,6 +1,6 @@ use super::{PyGenericAlias, PyStr, PyType, PyTypeRef}; use crate::{ - Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, + AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, class::PyClassImpl, common::lock::PyMutex, function::{FuncArgs, PySetterValue}, @@ -44,20 +44,16 @@ impl Constructor for PyStaticMethod { type Args = PyObjectRef; fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { - let callable: Self::Args = args.bind(vm)?; - let doc = callable.get_attr("__doc__", vm); - + // Validate the signature here, but defer storing the callable and + // copying its attributes to `__init__` so that subclasses overriding + // `__init__` without calling `super().__init__()` see `__func__` as + // `None`, matching CPython. + let _: Self::Args = args.bind(vm)?; let result = Self { - callable: PyMutex::new(callable), + callable: PyMutex::new(vm.ctx.none()), } .into_ref_with_type(vm, cls)?; - let obj = PyObjectRef::from(result); - - if let Ok(doc) = doc { - obj.set_attr("__doc__", doc, vm)?; - } - - Ok(obj) + Ok(PyObjectRef::from(result)) } fn py_new(_cls: &Py, _args: Self::Args, _vm: &VirtualMachine) -> PyResult { @@ -80,8 +76,11 @@ impl PyStaticMethod { impl Initializer for PyStaticMethod { type Args = PyObjectRef; - fn init(zelf: PyRef, callable: Self::Args, _vm: &VirtualMachine) -> PyResult<()> { - *zelf.callable.lock() = callable; + fn init(zelf: PyRef, callable: Self::Args, vm: &VirtualMachine) -> PyResult<()> { + *zelf.callable.lock() = callable.clone(); + if let Ok(doc) = callable.get_attr("__doc__", vm) { + zelf.as_object().set_attr("__doc__", doc, vm)?; + } Ok(()) } }