From 799a398c6e6e99e5d45242f8e57a256714cbd6c4 Mon Sep 17 00:00:00 2001 From: Raghav Dhoot Date: Sun, 23 Aug 2026 21:36:07 +0530 Subject: [PATCH 1/2] Fix monkeypatch recording failed mutations --- src/_pytest/monkeypatch.py | 8 +++--- testing/test_monkeypatch.py | 50 +++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index d6db72455a8..9ae9e2ba9c8 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -282,14 +282,15 @@ def delattr( # Avoid class descriptors like staticmethod/classmethod. if inspect.isclass(target): oldval = target.__dict__.get(name, NOTSET) - self._setattr.append((target, name, oldval)) delattr(target, name) + self._setattr.append((target, name, oldval)) def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None: """Set dictionary entry ``name`` to value.""" - self._setitem.append((dic, name, dic.get(name, NOTSET))) + oldval = dic.get(name, NOTSET) # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict dic[name] = value # type: ignore[index] + self._setitem.append((dic, name, oldval)) def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None: """Delete ``name`` from dict. @@ -301,9 +302,10 @@ def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None: if raising: raise KeyError(name) else: - self._setitem.append((dic, name, dic.get(name, NOTSET))) + oldval = dic.get(name, NOTSET) # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict del dic[name] # type: ignore[attr-defined] + self._setitem.append((dic, name, oldval)) def setenv(self, name: str, value: str, prepend: str | None = None) -> None: """Set environment variable ``name`` to ``value``. diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 04b16a1e8c2..7f2a6cbe370 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -135,6 +135,22 @@ class A: assert A.x == 1 +def test_delattr_does_not_record_failed_mutation() -> None: + class BrokenDelete: + value = 1 + + def __delattr__(self, name: str) -> None: + raise RuntimeError("delete failed") + + monkeypatch = MonkeyPatch() + obj = BrokenDelete() + + with pytest.raises(RuntimeError, match="delete failed"): + monkeypatch.delattr(obj, "value") + + monkeypatch.undo() + + def test_setitem() -> None: d = {"x": 1} monkeypatch = MonkeyPatch() @@ -162,6 +178,23 @@ def test_setitem_deleted_meanwhile() -> None: assert not d +def test_setitem_does_not_record_failed_mutation() -> None: + class BrokenDict(dict[str, object]): + def __setitem__(self, key: str, value: object) -> None: + raise RuntimeError("set failed") + + def __delitem__(self, key: str) -> None: + raise RuntimeError("delete failed") + + monkeypatch = MonkeyPatch() + d = BrokenDict() + + with pytest.raises(RuntimeError, match="set failed"): + monkeypatch.setitem(d, "x", 1) + + monkeypatch.undo() + + @pytest.mark.parametrize("before", [True, False]) def test_setenv_deleted_meanwhile(before: bool) -> None: key = "qwpeoip123" @@ -196,6 +229,23 @@ def test_delitem() -> None: assert d == {"hello": "world", "x": 1} +def test_delitem_does_not_record_failed_mutation() -> None: + class BrokenDict(dict[str, object]): + def __setitem__(self, key: str, value: object) -> None: + raise RuntimeError("set failed") + + def __delitem__(self, key: str) -> None: + raise RuntimeError("delete failed") + + monkeypatch = MonkeyPatch() + d = BrokenDict({"x": 1}) + + with pytest.raises(RuntimeError, match="delete failed"): + monkeypatch.delitem(d, "x") + + monkeypatch.undo() + + def test_setenv() -> None: monkeypatch = MonkeyPatch() with pytest.warns(pytest.PytestWarning): From 60a62e86914b936f19ab86a0fe6b058591f5fae0 Mon Sep 17 00:00:00 2001 From: Raghav Dhoot Date: Sun, 23 Aug 2026 21:41:40 +0530 Subject: [PATCH 2/2] Add changelog for monkeypatch failed mutations --- changelog/14927.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/14927.bugfix.rst diff --git a/changelog/14927.bugfix.rst b/changelog/14927.bugfix.rst new file mode 100644 index 00000000000..592fa839ecf --- /dev/null +++ b/changelog/14927.bugfix.rst @@ -0,0 +1 @@ +Fixes MonkeyPatch so failed mutations are not recorded for undo.