Skip to content

Commit f10f441

Browse files
Defer staticmethod/classmethod callable storage to __init__ (#7697)
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.
1 parent 1fa676f commit f10f441

3 files changed

Lines changed: 35 additions & 44 deletions

File tree

Lib/test/test_descr.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5193,7 +5193,6 @@ def foo(self):
51935193
with self.assertRaisesRegex(NotImplementedError, "BAR"):
51945194
B().foo
51955195

5196-
@unittest.expectedFailure # TODO: RUSTPYTHON; Wrong error message
51975196
def test_staticmethod_new(self):
51985197
class MyStaticMethod(staticmethod):
51995198
def __init__(self, func):
@@ -5204,7 +5203,6 @@ def func(): pass
52045203
self.assertIsNone(sm.__func__)
52055204
self.assertIsNone(sm.__wrapped__)
52065205

5207-
@unittest.expectedFailure # TODO: RUSTPYTHON; Wrong error message
52085206
def test_classmethod_new(self):
52095207
class MyClassMethod(classmethod):
52105208
def __init__(self, func):

crates/vm/src/builtins/classmethod.rs

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -66,34 +66,15 @@ impl Constructor for PyClassMethod {
6666
type Args = PyObjectRef;
6767

6868
fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
69-
let callable: Self::Args = args.bind(vm)?;
70-
// Create a dictionary to hold copied attributes
71-
let dict = vm.ctx.new_dict();
72-
73-
// Copy attributes from the callable to the dict
74-
// This is similar to functools.wraps in CPython
75-
if let Ok(doc) = callable.get_attr("__doc__", vm) {
76-
dict.set_item(identifier!(vm.ctx, __doc__), doc, vm)?;
77-
}
78-
if let Ok(name) = callable.get_attr("__name__", vm) {
79-
dict.set_item(identifier!(vm.ctx, __name__), name, vm)?;
80-
}
81-
if let Ok(qualname) = callable.get_attr("__qualname__", vm) {
82-
dict.set_item(identifier!(vm.ctx, __qualname__), qualname, vm)?;
83-
}
84-
if let Ok(module) = callable.get_attr("__module__", vm) {
85-
dict.set_item(identifier!(vm.ctx, __module__), module, vm)?;
86-
}
87-
if let Ok(annotations) = callable.get_attr("__annotations__", vm) {
88-
dict.set_item(identifier!(vm.ctx, __annotations__), annotations, vm)?;
89-
}
90-
91-
// Create PyClassMethod instance with the pre-populated dict
69+
// Validate the signature here, but defer storing the callable and
70+
// copying its attributes to `__init__` so that subclasses overriding
71+
// `__init__` without calling `super().__init__()` see `__func__` as
72+
// `None`, matching CPython.
73+
let _: Self::Args = args.bind(vm)?;
9274
let classmethod = Self {
93-
callable: PyMutex::new(callable),
75+
callable: PyMutex::new(vm.ctx.none()),
9476
};
95-
96-
let result = PyRef::new_ref(classmethod, cls, Some(dict));
77+
let result = PyRef::new_ref(classmethod, cls, Some(vm.ctx.new_dict()));
9778
Ok(PyObjectRef::from(result))
9879
}
9980

@@ -105,8 +86,21 @@ impl Constructor for PyClassMethod {
10586
impl Initializer for PyClassMethod {
10687
type Args = PyObjectRef;
10788

108-
fn init(zelf: PyRef<Self>, callable: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
109-
*zelf.callable.lock() = callable;
89+
fn init(zelf: PyRef<Self>, callable: Self::Args, vm: &VirtualMachine) -> PyResult<()> {
90+
*zelf.callable.lock() = callable.clone();
91+
// Copy wrapper attributes from the callable, mirroring functools.wraps.
92+
let dict = zelf.as_object().dict().expect("classmethod has __dict__");
93+
for attr in [
94+
identifier!(vm.ctx, __doc__),
95+
identifier!(vm.ctx, __name__),
96+
identifier!(vm.ctx, __qualname__),
97+
identifier!(vm.ctx, __module__),
98+
identifier!(vm.ctx, __annotations__),
99+
] {
100+
if let Ok(value) = callable.get_attr(attr, vm) {
101+
dict.set_item(attr, value, vm)?;
102+
}
103+
}
110104
Ok(())
111105
}
112106
}

crates/vm/src/builtins/staticmethod.rs

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::{PyGenericAlias, PyStr, PyType, PyTypeRef};
22
use crate::{
3-
Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
3+
AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
44
class::PyClassImpl,
55
common::lock::PyMutex,
66
function::{FuncArgs, PySetterValue},
@@ -44,20 +44,16 @@ impl Constructor for PyStaticMethod {
4444
type Args = PyObjectRef;
4545

4646
fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
47-
let callable: Self::Args = args.bind(vm)?;
48-
let doc = callable.get_attr("__doc__", vm);
49-
47+
// Validate the signature here, but defer storing the callable and
48+
// copying its attributes to `__init__` so that subclasses overriding
49+
// `__init__` without calling `super().__init__()` see `__func__` as
50+
// `None`, matching CPython.
51+
let _: Self::Args = args.bind(vm)?;
5052
let result = Self {
51-
callable: PyMutex::new(callable),
53+
callable: PyMutex::new(vm.ctx.none()),
5254
}
5355
.into_ref_with_type(vm, cls)?;
54-
let obj = PyObjectRef::from(result);
55-
56-
if let Ok(doc) = doc {
57-
obj.set_attr("__doc__", doc, vm)?;
58-
}
59-
60-
Ok(obj)
56+
Ok(PyObjectRef::from(result))
6157
}
6258

6359
fn py_new(_cls: &Py<PyType>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<Self> {
@@ -80,8 +76,11 @@ impl PyStaticMethod {
8076
impl Initializer for PyStaticMethod {
8177
type Args = PyObjectRef;
8278

83-
fn init(zelf: PyRef<Self>, callable: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
84-
*zelf.callable.lock() = callable;
79+
fn init(zelf: PyRef<Self>, callable: Self::Args, vm: &VirtualMachine) -> PyResult<()> {
80+
*zelf.callable.lock() = callable.clone();
81+
if let Ok(doc) = callable.get_attr("__doc__", vm) {
82+
zelf.as_object().set_attr("__doc__", doc, vm)?;
83+
}
8584
Ok(())
8685
}
8786
}

0 commit comments

Comments
 (0)