From 18c08af1ff915082d7366debd46d7d363f419aa7 Mon Sep 17 00:00:00 2001 From: Kartik Date: Sat, 7 Mar 2026 07:29:22 +0530 Subject: [PATCH] Fix monkeypatch undo for missing delattr/delitem --- changelog/14094.bugfix.rst | 1 + src/_pytest/monkeypatch.py | 7 ++++++- testing/test_monkeypatch.py | 22 ++++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 changelog/14094.bugfix.rst diff --git a/changelog/14094.bugfix.rst b/changelog/14094.bugfix.rst new file mode 100644 index 00000000000..16b28c48e20 --- /dev/null +++ b/changelog/14094.bugfix.rst @@ -0,0 +1 @@ +Fixed ``MonkeyPatch.delattr(..., raising=False)`` and ``MonkeyPatch.delitem(..., raising=False)`` so ``undo()`` restores the original missing state. diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 6c033f36fda..7bc4035e645 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -277,6 +277,7 @@ def delattr( if not hasattr(target, name): if raising: raise AttributeError(name) + self._setattr.append((target, name, NOTSET)) else: oldval = getattr(target, name, NOTSET) # Avoid class descriptors like staticmethod/classmethod. @@ -300,6 +301,7 @@ def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None: if name not in dic: if raising: raise KeyError(name) + self._setitem.append((dic, name, NOTSET)) else: self._setitem.append((dic, name, dic.get(name, NOTSET))) # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict @@ -408,7 +410,10 @@ def undo(self) -> None: if value is not NOTSET: setattr(obj, name, value) else: - delattr(obj, name) + try: + delattr(obj, name) + except AttributeError: + pass # Was already deleted, so we have the desired state. self._setattr[:] = [] for dictionary, key, value in reversed(self._setitem): if value is NOTSET: diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index c321439e398..55544d63147 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -117,6 +117,18 @@ class A: assert A.x == 1 +def test_delattr_non_existing_with_raising_false() -> None: + class A: + pass + + monkeypatch = MonkeyPatch() + monkeypatch.delattr(A, "x", raising=False) + monkeypatch.setattr(A, "x", 1, raising=False) + assert A.x == 1 # type: ignore[attr-defined] + monkeypatch.undo() + assert not hasattr(A, "x") + + def test_setitem() -> None: d = {"x": 1} monkeypatch = MonkeyPatch() @@ -177,6 +189,16 @@ def test_delitem() -> None: assert d == {"hello": "world", "x": 1} +def test_delitem_non_existing_with_raising_false() -> None: + d: dict[str, object] = {} + monkeypatch = MonkeyPatch() + monkeypatch.delitem(d, "x", raising=False) + monkeypatch.setitem(d, "x", 1) + assert d["x"] == 1 + monkeypatch.undo() + assert "x" not in d + + def test_setenv() -> None: monkeypatch = MonkeyPatch() with pytest.warns(pytest.PytestWarning):