Defer staticmethod/classmethod callable storage to __init__ - #7697
Conversation
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.
📝 WalkthroughWalkthroughThe PR refactors Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
📦 Library DependenciesThe following Lib/ modules were modified. Here are their dependencies: [ ] test: cpython/Lib/test/test_descr.py (TODO: 39) dependencies: dependent tests: (no tests depend on descr) Legend:
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/vm/src/builtins/classmethod.rs (1)
92-92: Consider a non-panicking fallback for the__dict__lookup.
expect("classmethod has __dict__")is currently safe becauseslot_newalways attaches a dict, but it couplesinit's correctness to that invariant. If a future refactor ofslot_new(or a subclass path that constructs via a different code route) ever lands an instance without a dict, this becomes a hard panic in user code.A small defensive change keeps the happy path identical and falls back gracefully:
♻️ Suggested refactor
- 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)?; - } - } + if let Some(dict) = zelf.as_object().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)?; + } + } + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/vm/src/builtins/classmethod.rs` at line 92, Replace the panicking lookup `zelf.as_object().dict().expect("classmethod has __dict__")` with a non-panicking branch: call `zelf.as_object().dict()` and if it returns Some(dict) use it, otherwise create a new empty dict (or obtain a canonical empty/allocated PyDict), attach it to the instance if appropriate, and proceed; update the code in classmethod handling in classmethod.rs around the `zelf.as_object().dict()` use so the happy path is unchanged but missing-__dict__ cases gracefully allocate/attach and avoid panics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@crates/vm/src/builtins/classmethod.rs`:
- Line 92: Replace the panicking lookup
`zelf.as_object().dict().expect("classmethod has __dict__")` with a
non-panicking branch: call `zelf.as_object().dict()` and if it returns
Some(dict) use it, otherwise create a new empty dict (or obtain a canonical
empty/allocated PyDict), attach it to the instance if appropriate, and proceed;
update the code in classmethod handling in classmethod.rs around the
`zelf.as_object().dict()` use so the happy path is unchanged but
missing-__dict__ cases gracefully allocate/attach and avoid panics.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 5305d229-9f35-4b8a-a29b-98717202f258
⛔ Files ignored due to path filters (1)
Lib/test/test_descr.pyis excluded by!Lib/**
📒 Files selected for processing (2)
crates/vm/src/builtins/classmethod.rscrates/vm/src/builtins/staticmethod.rs
thanks for the review :) |
Background
Python's two-phase object construction splits responsibility between
__new__(allocation, no required state) and__init__(initialization). Subclasses can override either phase independently.CPython's
staticmethod/classmethodfollow this contract strictly:__new__allocates a bare descriptor with__func__ = None, and__init__assigns the callable plus copies wrapper attributes (__doc__,__name__,__qualname__,__module__,__annotations__) viafunctools_wraps(Objects/funcobject.c::sm_init/cm_init).RustPython instead did the same work twice — once in
slot_newand again in theInitializer— so a subclass overriding__init__without callingsuper().__init__()still had__func__set fromslot_new.Repro
This pattern shows up in libraries that subclass the descriptor types to customize their state (e.g. property-like wrappers, mocking libraries, internal CPython tests for descriptor semantics).
Fix
slot_newnow validates the signature (sostaticmethod()/staticmethod(a, b)still error eagerly) but storesNonefor the callable and skips the wrapper-attribute copy.Initializer::initstores the callable and runs the wrapper copy, mirroring CPython's split.Net diff:
+35 / -44(the wrapper-attr setup just moves between methods, and theslot_newpath collapses).Tests unmasked
test_descr.ClassPropertiesAndMethods.test_staticmethod_newtest_descr.ClassPropertiesAndMethods.test_classmethod_new(Both were marked
@unittest.expectedFailure # TODO: RUSTPYTHON; Wrong error message— the failure was actually behavioral, not a wording issue, but the new behavior matches the test's assertions exactly.)Verification
test_descr test_decorators test_funcattrs test_inspect test_functools test_class test_super test_abc test_property test_pickle test_dataclasses test_dis test_metaclass test_call test_module test_typing test_keywordonlyarg(1,071+ tests)sm.__init__(other), subclass that does callsuper().__init__(), pickling round-trip,@staticmethod/@classmethoddecorator usage — all match CPython