Skip to content

fix(monkeypatch): restore attributes of objects with a custom __setattr__ - #15118

Open
Tejas5405 wants to merge 1 commit into
pytest-dev:mainfrom
Tejas5405:fix-15099-monkeypatch-custom-setattr
Open

Tejas5405 wants to merge 1 commit into
pytest-dev:mainfrom
Tejas5405:fix-15099-monkeypatch-custom-setattr

Conversation

@Tejas5405

Copy link
Copy Markdown

closes #15099

Problem

MonkeyPatch.setattr() records the old value from the instance __dict__ whenever no data descriptor is in the way, on the assumption that setattr() writes there:

elif not _is_data_descriptor(type(target), name):
    target_dict = getattr(target, "__dict__", None)
    if isinstance(target_dict, Mapping):
        oldval = target_dict.get(name, NOTSET)

A class which defines __setattr__ can store the value anywhere -- a proxy object's private mapping, for example. For those, oldval is recorded as NOTSET, so undo() calls delattr() instead of restoring the value. That raises AttributeError during teardown, and the patched value then leaks into later tests:

ERROR test_patch - AttributeError: 'Config' object has no attribute 'debug'
FAILED test_restored - assert True is False

Fix

Restrict the __dict__ lookup to types which use object.__setattr__, the only case where setattr() is known to write into the instance __dict__. For every other type the getattr() value stays the right one to restore, because undo() writes it back through the same __setattr__ hook.

This keeps what #14969 added for #10644 intact: those cases use plain classes, so they still take the __dict__ path and undo() still removes the entry it created.

Tests

  • testing/test_monkeypatch.py::test_undo_attribute_stored_outside_instance_dict, a new regression test modelled on the reproducer in the issue. Without the source change it fails with AttributeError; with it, undo() restores the original value.
  • changelog/15099.bugfix.rst, the news fragment.

Executed before submitting:

  • pytest testing/test_monkeypatch.py -> 46 passed, 1 skipped
  • pytest testing/ -> 4632 passed, 51 skipped, 15 xfailed, 5 xpassed, 1 failed, 1 error. Both the failure (test_fixture_doctest_skip_has_line_number) and the error (test_cache_makedir) reproduce identically on the unmodified checkout in this environment, so they are unrelated to this change.
  • ruff check and ruff format --check with 0.16.9, the version pinned in .pre-commit-config.yaml: clean.
  • mypy: no new diagnostics for the changed files. (The one remaining report, an unused type: ignore further down testing/test_monkeypatch.py, is present on the unmodified file too.)

AI assistance disclosure

Per the project's AI/LLM-Assisted Contributions Policy: this change -- the reproduction analysis, the fix, the regression test and the changelog entry -- was produced with AI assistance, and I validated it by hand before submitting. I reproduced the failure and confirmed the fix on Python 3.13, verified the new test fails without the source change, and ran the full suite plus ruff and mypy. I am happy to go through any part of the diff.

@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Oct 1, 2026
@Tejas5405
Tejas5405 force-pushed the fix-15099-monkeypatch-custom-setattr branch from bf147d8 to 8932bb1 Compare October 1, 2026 00:26
…tr__

Since 0c601d5 (pytest-dev#14969), `MonkeyPatch.setattr()` looks the old value up
in the instance `__dict__` whenever no data descriptor is in the way,
because that is where `setattr()` is assumed to write. A class which
defines `__setattr__` can store the value anywhere -- a proxy object's
private mapping, for example -- so the old value is recorded as `NOTSET`,
`undo()` then tries to delete the attribute instead of restoring it, and
the patched value leaks into later tests.

Restrict that lookup to types which use `object.__setattr__`. For the
others the `getattr()` value stays the right one to restore, since
`undo()` writes it back through the same hook.

Adds a regression test and a changelog entry. The test also asserts that
the proxy object converts a missing attribute to `AttributeError`, which
keeps the whole diff covered.

Closes pytest-dev#15099
@Tejas5405
Tejas5405 force-pushed the fix-15099-monkeypatch-custom-setattr branch from 8932bb1 to 68cb1be Compare October 1, 2026 00:43

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided (automation) changelog entry is part of PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

monkeypatch.setattr fails to undo on objects with a custom __setattr__ (regression from #14969)

1 participant