From cb63d4f85f7193b17e9330d3f6c92f312afd21e1 Mon Sep 17 00:00:00 2001 From: Dane Parin Date: Tue, 15 Sep 2026 12:12:48 +0700 Subject: [PATCH] Fix monkeypatch.setattr undo for type-level attributes like __bases__ Co-authored-by: Claude Sonnet 5 --- changelog/1938.bugfix.rst | 1 + src/_pytest/monkeypatch.py | 13 +++++++++++-- testing/test_monkeypatch.py | 20 ++++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 changelog/1938.bugfix.rst diff --git a/changelog/1938.bugfix.rst b/changelog/1938.bugfix.rst new file mode 100644 index 00000000000..3273b7eb48d --- /dev/null +++ b/changelog/1938.bugfix.rst @@ -0,0 +1 @@ +``monkeypatch.undo()`` now restores ``__bases__`` and similar type-level attributes patched via ``monkeypatch.setattr()``, instead of raising ``TypeError``. diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 453c6728ee2..66f6d24e08a 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -256,8 +256,10 @@ def setattr( raise AttributeError(f"{target!r} has no attribute {name!r}") # avoid class descriptors like staticmethod/classmethod - if inspect.isclass(target): - oldval = target.__dict__.get(name, NOTSET) + is_class = inspect.isclass(target) + if is_class: + in_class_dict = name in target.__dict__ + oldval = target.__dict__.get(name, oldval) elif not _is_data_descriptor(type(target), name): # With no data descriptor in the way, the `setattr()` below writes # into the instance `__dict__`, so `undo()` has to restore that @@ -269,6 +271,13 @@ def setattr( if isinstance(target_dict, Mapping): oldval = target_dict.get(name, NOTSET) setattr(target, name, value) + if is_class and not in_class_dict: + if name in target.__dict__: + # setattr() created a new entry shadowing an inherited + # attribute: undo() must remove it with delattr() (#156). + oldval = NOTSET + # setattr() did not add a __dict__ entry: undo() restores the old + # value via setattr() (#1938). self._setattr.append((target, name, oldval)) def delattr( diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 0d07783b05b..d448f1b0796 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -443,6 +443,26 @@ def test_issue156_undo_staticmethod(Sample: type[Sample]) -> None: assert Sample.hello() +def test_issue1938_patch_class_bases() -> None: + class Loud: + def thing(self): + return "!!!!" + + class Quiet: + def thing(self): + return "sssh..." + + class ThingToTest(Loud): + pass + + monkeypatch = MonkeyPatch() + monkeypatch.setattr(ThingToTest, "__bases__", (Quiet,)) + assert ThingToTest().thing() == "sssh..." + monkeypatch.undo() + assert ThingToTest.__bases__ == (Loud,) + assert ThingToTest().thing() == "!!!!" + + def test_undo_class_descriptors_delattr() -> None: class SampleParent: @classmethod