Since #14969 (0c601d5, not released yet), monkeypatch.setattr doesn't restore attributes on objects that store them somewhere other than __dict__ through a custom __setattr__/__getattr__. Undo raises AttributeError, and the patched value leaks into later tests.
class Config:
"""Stores attributes in a private dict instead of __dict__."""
def __init__(self):
object.__setattr__(self, "_data", {"debug": False})
def __getattr__(self, name):
try:
return self._data[name]
except KeyError:
raise AttributeError(name) from None
def __setattr__(self, name, value):
self._data[name] = value
cfg = Config()
def test_patch(monkeypatch):
monkeypatch.setattr(cfg, "debug", True)
assert cfg.debug is True
def test_restored():
assert cfg.debug is False
On main:
ERROR test_proxy.py::test_patch - AttributeError: 'Config' object has no attribute 'debug'
FAILED test_proxy.py::test_restored - assert True is False
1 failed, 1 passed, 1 error
Just before 0c601d5, the same file passes (2 passed).
The new branch in MonkeyPatch.setattr takes the old value from target.__dict__.get(name, NOTSET) whenever target has a __dict__. It assumes that a plain setattr() writes into that dict. With a custom __setattr__ it doesn't, so the old value is recorded as NOTSET. undo() then calls delattr(cfg, "debug") instead of setting it back to False. That delattr fails, and the value stays patched.
@Shriprasad-P pointed out this exact case in a review on #14969, but it wasn't addressed before the merge. I'm opening this issue so it gets fixed before the next release.
One possible direction: only use the __dict__ lookup when name is actually present in the instance __dict__, or when the type uses object.__setattr__. Otherwise fall back to the value from getattr(), as before.
pytest 9.2.0.dev345+g872117358, Python 3.13.5, Windows 11.
Since #14969 (0c601d5, not released yet),
monkeypatch.setattrdoesn't restore attributes on objects that store them somewhere other than__dict__through a custom__setattr__/__getattr__. Undo raisesAttributeError, and the patched value leaks into later tests.On main:
Just before 0c601d5, the same file passes (
2 passed).The new branch in
MonkeyPatch.setattrtakes the old value fromtarget.__dict__.get(name, NOTSET)whenevertargethas a__dict__. It assumes that a plainsetattr()writes into that dict. With a custom__setattr__it doesn't, so the old value is recorded asNOTSET.undo()then callsdelattr(cfg, "debug")instead of setting it back toFalse. Thatdelattrfails, and the value stays patched.@Shriprasad-P pointed out this exact case in a review on #14969, but it wasn't addressed before the merge. I'm opening this issue so it gets fixed before the next release.
One possible direction: only use the
__dict__lookup whennameis actually present in the instance__dict__, or when the type usesobject.__setattr__. Otherwise fall back to the value fromgetattr(), as before.pytest 9.2.0.dev345+g872117358, Python 3.13.5, Windows 11.