Skip to content

fix(monkeypatch): restore attributes on objects with custom __setattr__ - #15100

Open
hardikkaurani wants to merge 3 commits into
pytest-dev:mainfrom
hardikkaurani:fix/monkeypatch-custom-setattr
Open

hardikkaurani wants to merge 3 commits into
pytest-dev:mainfrom
hardikkaurani:fix/monkeypatch-custom-setattr

Conversation

@hardikkaurani

Copy link
Copy Markdown
  • Include new tests or update existing tests when applicable.
  • Allow maintainers to push and squash when merging my commits.
  • Create a new changelog file in the changelog directory (changelog/15099.bugfix.rst).
  • Add yourself to AUTHORS in alphabetical order.

Closes #15099.

Problem

In #14969 (0c601d5), MonkeyPatch.setattr was updated to record the previous attribute value from the instance __dict__ instead of getattr() to prevent leaving inherited attributes behind in instance dictionaries upon undo() (#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:

  1. oldval was evaluated as NOTSET via target_dict.get(name, NOTSET).
  2. When undo() executed, it attempted delattr(target, name) instead of resetting the attribute value via setattr(target, name, oldval).
  3. This raised 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 resolved getattr(target, name, NOTSET) value so undo() restores the attribute cleanly via setattr(target, name, oldval).

All existing descriptor, slot, inheritance, and instance tests continue to pass, alongside a new regression test covering custom __setattr__ targets.

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>
Copilot AI lite review requested due to automatic review settings September 24, 2026 18:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Sep 24, 2026

@MateehUllah MateehUllah left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 kilisamemarisaaa left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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)

4 participants