fix(monkeypatch): restore attributes on objects with custom __setattr__ - #15100
hardikkaurani wants to merge 3 commits into
Conversation
In pytest-dev#14969, MonkeyPatch.setattr was changed to record the old value from the instance __dict__ instead of getattr() to avoid leaving behind an inherited class attribute in the instance dict upon undo(). However, that change assumed setattr() always writes into the instance __dict__. When an object defines a custom __setattr__, attribute storage is handled by custom machinery and attributes may not exist in __dict__. Consequently, oldval was recorded as NOTSET, causing undo() to call delattr(), raising AttributeError and leaving the attribute patched. Check that type(target).__setattr__ is object.__setattr__ before looking in the instance __dict__. When __setattr__ is overridden, retain the resolved getattr() value so undo() restores it through setattr(). Closes pytest-dev#15099. Co-authored-by: Antigravity <antigravity@google.com>
for more information, see https://pre-commit.ci
MateehUllah
left a comment
There was a problem hiding this comment.
Reviewed the current patch. Restricting the direct dict restoration path to objects using object.setattr preserves the inherited-descriptor fix while allowing objects with custom attribute-storage machinery to restore values through their own setattr. The regression exercises storage outside dict, verifies the patched value, and confirms restoration during undo. I did not find a blocking correctness issue.
kilisamemarisaaa
left a comment
There was a problem hiding this comment.
Reproduced and reviewed at head 8d1f543f973da02eda6f90f6ed276d3751efab84 on Windows/Python 3.12.6.
The guard keeps the inherited-attribute restoration path only for instances with object.__setattr__, while custom storage routes through getattr/setattr so undo() restores the value instead of calling delattr on a missing dictionary key. The regression also checks the missing-attribute raising=True behavior.
Validation: SETUPTOOLS_SCM_PRETEND_VERSION=9.2.0.dev999 uv run --reinstall pytest testing/test_monkeypatch.py -q -> 47 passed. I found no blocking issue.
changelogdirectory (changelog/15099.bugfix.rst).AUTHORSin alphabetical order.Closes #15099.
Problem
In #14969 (0c601d5),
MonkeyPatch.setattrwas updated to record the previous attribute value from the instance__dict__instead ofgetattr()to prevent leaving inherited attributes behind in instance dictionaries uponundo()(#10644).However, this assumed that
setattr()on instances without data descriptors always targets the instance__dict__. When an object overrides__setattr__(e.g. proxy objects or configuration objects that route attribute access through internal backing stores), attributes are not stored directly in__dict__.As a result:
oldvalwas evaluated asNOTSETviatarget_dict.get(name, NOTSET).undo()executed, it attempteddelattr(target, name)instead of resetting the attribute value viasetattr(target, name, oldval).AttributeError: '<Class>' object has no attribute '<name>'on objects lacking custom__delattr__, causing test failures and leaving the attribute patched for subsequent tests.Solution
Check that
type(target).__setattr__ is object.__setattr__before inspecting the instance__dict__.If
__setattr__has been overridden by the object's class, we retain the resolvedgetattr(target, name, NOTSET)value soundo()restores the attribute cleanly viasetattr(target, name, oldval).All existing descriptor, slot, inheritance, and instance tests continue to pass, alongside a new regression test covering custom
__setattr__targets.