Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/15287.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed a regression in pytest 9.2.0 where :meth:`MonkeyPatch.setattr <pytest.MonkeyPatch.setattr>` ``undo()`` deleted child attributes on :class:`unittest.mock.MagicMock` (or other dynamic attributes not present on the class) instead of restoring them.
5 changes: 4 additions & 1 deletion src/_pytest/monkeypatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,10 @@ def setattr(
# class attribute, which permanently freezes descriptors that
# resolve dynamically (#10644).
target_dict = getattr(target, "__dict__", None)
if isinstance(target_dict, Mapping):
if isinstance(target_dict, Mapping) and (
name in target_dict
or inspect.getattr_static(type(target), name, NOTSET) is not NOTSET
):
oldval = target_dict.get(name, NOTSET)
setattr(target, name, value)
self._setattr.append((target, name, oldval))
Expand Down
38 changes: 38 additions & 0 deletions testing/test_monkeypatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,44 @@ def __init__(self) -> None:
assert obj.x == 1


def test_undo_magicmock_child_attribute() -> None:
"""Undoing a patch on a MagicMock's child mock must not delete it.

See #15287.
"""
from unittest.mock import MagicMock

shared = MagicMock()
orig_child = shared.connect # create child mock
monkeypatch = MonkeyPatch()
monkeypatch.setattr(shared, "connect", MagicMock(return_value=1))
assert shared.connect() == 1

monkeypatch.undo()
assert shared.connect is orig_child
shared.connect.return_value = 2
assert shared.connect() == 2


def test_undo_dynamic_getattr_attribute() -> None:
"""Undoing a patch on an attribute resolved via __getattr__ must restore it."""

class Dynamic:
def __getattr__(self, name: str) -> int:
if name == "dyn":
return 42
raise AttributeError(name)

d = Dynamic()
assert d.dyn == 42
monkeypatch = MonkeyPatch()
monkeypatch.setattr(d, "dyn", 100)
assert d.dyn == 100

monkeypatch.undo()
assert d.dyn == 42


def test_issue1338_name_resolving() -> None:
pytest.importorskip("requests")
monkeypatch = MonkeyPatch()
Expand Down