diff --git a/AUTHORS b/AUTHORS index d1a2d3e7911..8f1b40615e6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -55,6 +55,7 @@ Antony Lee Arel Cordero Arias Emmanuel Ariel Pillemer +Arpan Sahu Armin Rigo Aron Coyle Aron Curzon diff --git a/changelog/10644.bugfix.rst b/changelog/10644.bugfix.rst new file mode 100644 index 00000000000..ea6ce732af0 --- /dev/null +++ b/changelog/10644.bugfix.rst @@ -0,0 +1 @@ +Fix :class:`pytest.MonkeyPatch` restoring inherited attributes into an instance's ``__dict__`` after undoing ``setattr`` on an object. diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index d6db72455a8..7d8128d4083 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -244,6 +244,14 @@ def setattr( # avoid class descriptors like staticmethod/classmethod if inspect.isclass(target): oldval = target.__dict__.get(name, NOTSET) + else: + try: + target_vars = vars(target) + except TypeError: + pass + else: + if name not in target_vars: + oldval = NOTSET setattr(target, name, value) self._setattr.append((target, name, oldval)) diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 04b16a1e8c2..b8b6ee6e5a2 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Generator +import inspect import os from pathlib import Path import re @@ -51,6 +52,28 @@ class A: monkeypatch.setattr(A, "y") # type: ignore[call-overload] +def test_setattr_inherited_attribute_undo_restores_instance_dict() -> None: + class Descriptor: + def __get__(self, obj, objtype=None): + return 1 + + class Parent: + x = 1 + descriptor = Descriptor() + + for name in ("x", "descriptor"): + target = Parent() + monkeypatch = MonkeyPatch() + assert name not in vars(target) + assert isinstance(inspect.getattr_static(target, "descriptor"), Descriptor) + + monkeypatch.setattr(target, name, 2) + monkeypatch.undo() + + assert name not in vars(target) + assert isinstance(inspect.getattr_static(target, "descriptor"), Descriptor) + + class TestSetattrWithImportPath: def test_string_expression(self, monkeypatch: MonkeyPatch) -> None: with monkeypatch.context() as mp: