Fix del obj.__dict__ to match CPython behavior (fixes #5355)#7568
Fix del obj.__dict__ to match CPython behavior (fixes #5355)#7568alok-108 wants to merge 13 commits intoRustPython:mainfrom
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughMake per-object dict presence explicit (optional), add lazy dict materialization via InstanceDict::get_or_insert, and propagate PySetterValue through dict setters/getters so assign/delete flows are handled uniformly and avoid eager per-instance dict allocation. Changes
Sequence Diagram(s)sequenceDiagram
actor Caller
participant subtype_set_dict as "subtype_set_dict"
participant Descriptor as "base_type.__dict__ descriptor"
participant object_set_dict as "object::object_set_dict"
participant Obj as "PyObject"
participant InstDict as "InstanceDict"
participant VM as "VirtualMachine"
Caller->>subtype_set_dict: set __dict__ (PySetterValue)
alt descriptor exists
subtype_set_dict->>Descriptor: call setter(PySetterValue)
Descriptor-->>Caller: return / error
else descriptor missing (fallback)
subtype_set_dict->>object_set_dict: forward PySetterValue (convert if needed)
object_set_dict->>Obj: set_dict(Some(dict)/None)
alt Obj has direct dict storage
Obj->>Obj: replace or clear dict
Obj-->>object_set_dict: Ok / Err
else Obj only has InstanceDict
object_set_dict->>InstDict: instance_dict().get_or_insert(vm) if Assign
InstDict->>VM: allocate new dict (vm.ctx.new_dict())
VM-->>InstDict: new dict
InstDict-->>Obj: store dict
Obj-->>object_set_dict: Ok / Err
end
object_set_dict-->>Caller: return / error
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 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 |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@crates/vm/src/builtins/object.rs`:
- Around line 560-564: The lazy __dict__ recreation is doing a check-then-set
across separate lock acquisitions (obj.instance_dict() followed by d.set(...)),
allowing two threads to allocate distinct dicts and one to overwrite the other;
fix by performing the allocation/check under the InstanceDict write lock:
acquire the write lock on the InstanceDict returned by obj.instance_dict(),
inside the lock check whether the slot is still empty and only then create (or
assign) a single vm.ctx.new_dict() and return that dict (cloning the stored
value to return) so allocation and set happen atomically under the same write
lock rather than across separate lock boundaries.
In `@crates/vm/src/object/core.rs`:
- Around line 946-949: The GC clear path currently calls old_dict.into_inner()
unconditionally in gc_clear_raw, but into_inner now returns Option<PyDictRef> so
deleted-dict cases must be skipped; update gc_clear_raw to call
old_dict.into_inner() and match on the Option (e.g., if let Some(dict) =
old_dict.into_inner() { push dict to gc } ), ensuring you only push when Some
and do nothing for None; reference InstanceDict::into_inner and the gc_clear_raw
logic that currently uses old_dict.into_inner().
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: c2b32e62-e40e-48b5-8a82-a01309c97b2f
📒 Files selected for processing (8)
build_error.txtbuild_error_gnu.txtbuild_error_gnu_explicit.txtbuild_error_u8.txtcrates/vm/src/builtins/object.rscrates/vm/src/builtins/type.rscrates/vm/src/object/core.rstest.py
…y of lazy __dict__ recreation
There was a problem hiding this comment.
Please find a test under Lib/test/ and make sure it is working; otherwise put a test code under extra_tests/snippets to ensure this is fixing the reported issue. Also do not submit test files.
also, do NOT submit patch without proper human review. we expect PR authors are responsible for their patches.
…n setattr, and enable passing CPython tests
📦 Library DependenciesThe following Lib/ modules were modified. Here are their dependencies: [ ] test: cpython/Lib/test/test_class.py (TODO: 13) dependencies: dependent tests: (no tests depend on class) Legend:
|
|
Hi @youknowone, thanks for the feedback. I've updated the PR with the following:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
extra_tests/snippets/test_del_dict.py (1)
5-20: Please make the regression prove replacement semantics.This still passes if
del obj.__dict__just clears the original dict in place. Keeping anold_dictreference before deletion, then assertingobj.__dict__ is not old_dictwould lock down the “drop and recreate” behavior, and a quick post-delete assignment would cover the new rematerialization-on-assign path too.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@extra_tests/snippets/test_del_dict.py` around lines 5 - 20, Capture the dict before deletion (old_dict = obj.__dict__), then delete it with del obj.__dict__ and assert that obj.__dict__ is a new dict object (obj.__dict__ is not old_dict) and empty; next assign obj.x = 99 and assert that obj.__dict__ is not old_dict and contains the new value (and that old_dict was not mutated) to prove the drop-and-recreate semantics for obj, obj.__dict__, and attribute assignment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@crates/vm/src/object/core.rs`:
- Around line 310-316: The call to ObjExt::new in init_type_hierarchy uses the
old 2-argument signature but the constructor now requires a third has_dict:
bool; update the call site to pass the correct boolean (e.g. use the existing
type spec or flag that indicates whether the type has an instance dict such as a
TypeSpec::has_dict / class_has_dict / has_dict variable in scope), i.e. replace
the two-arg call with ObjExt::new(..., has_dict) and if no such value is
available in that scope, compute it from the type metadata or default to false.
---
Nitpick comments:
In `@extra_tests/snippets/test_del_dict.py`:
- Around line 5-20: Capture the dict before deletion (old_dict = obj.__dict__),
then delete it with del obj.__dict__ and assert that obj.__dict__ is a new dict
object (obj.__dict__ is not old_dict) and empty; next assign obj.x = 99 and
assert that obj.__dict__ is not old_dict and contains the new value (and that
old_dict was not mutated) to prove the drop-and-recreate semantics for obj,
obj.__dict__, and attribute assignment.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: cbe3bfc6-e325-4f88-95a0-9ac56b547624
⛔ Files ignored due to path filters (1)
Lib/test/test_class.pyis excluded by!Lib/**
📒 Files selected for processing (3)
crates/vm/src/object/core.rscrates/vm/src/protocol/object.rsextra_tests/snippets/test_del_dict.py
|
@ShaharNaveh thanks for the review. I've removed the .txt files from the root and cleaned up the TODO comments in test_class.py. Only the expectedFailure removals remain now. |
ShaharNaveh
left a comment
There was a problem hiding this comment.
Great! lgtm if CI passes
|
@alok-108 please fix the CI errors and push:) |
This is a proposed fix for issue #5355 where deleting
__dict__on an object failed to match CPython's behavior.Goal
Match CPython behavior where
del obj.__dict__successfully removes the dictionary, and subsequent access to__dict__re-creates it lazily as a fresh empty dictionary.Technical Changes
InstanceDictincrates/vm/src/object/core.rsto hold anOption<PyDictRef>. This allows the dictionary field to be set toNoneupon deletion.object_set_dictincrates/vm/src/builtins/object.rsand relevant parts ofcrates/vm/src/builtins/type.rsto handlePySetterValue::Delete.object_get_dictto re-initialize the dictionary if it was previously deleted, ensuring compatibility with CPython.Note on Verification
The logic follows established RustPython patterns (like
transpose()andtry_into_value()). However, I was unable to fully verify the build locally due to a missing Windows linker (link.exe) in my current environment. I've manually traced the logic and believe it to be correct.@youknowone - Could you please review this proposed fix?
Summary by CodeRabbit
Refactor
Bug Fixes
Tests