diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py index c82ffd053..78db6064d 100644 --- a/_pytest/monkeypatch.py +++ b/_pytest/monkeypatch.py @@ -4,6 +4,8 @@ from __future__ import absolute_import, division, print_function import os import sys import re +from contextlib import contextmanager + import six from _pytest.fixtures import fixture @@ -106,6 +108,29 @@ class MonkeyPatch(object): self._cwd = None self._savesyspath = None + @contextmanager + def context(self): + """ + Context manager that returns a new :class:`MonkeyPatch` object which + undoes any patching done inside the ``with`` block upon exit: + + .. code-block:: python + + import functools + def test_partial(monkeypatch): + with monkeypatch.context() as m: + m.setattr(functools, "partial", 3) + + Useful in situations where it is desired to undo some patches before the test ends, + such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples + of this see `#3290 `_. + """ + m = MonkeyPatch() + try: + yield m + finally: + m.undo() + def setattr(self, target, name, value=notset, raising=True): """ Set attribute value on target, memorizing the old value. By default raise AttributeError if the attribute did not exist. diff --git a/changelog/3290.feature b/changelog/3290.feature new file mode 100644 index 000000000..a40afcb1a --- /dev/null +++ b/changelog/3290.feature @@ -0,0 +1,2 @@ +``monkeypatch`` now supports a ``context()`` function which acts as a context manager which undoes all patching done +within the ``with`` block. diff --git a/doc/en/monkeypatch.rst b/doc/en/monkeypatch.rst index a2327d4fa..b25e07f9a 100644 --- a/doc/en/monkeypatch.rst +++ b/doc/en/monkeypatch.rst @@ -61,6 +61,22 @@ so that any attempts within tests to create http requests will fail. ``compile``, etc., because it might break pytest's internals. If that's unavoidable, passing ``--tb=native``, ``--assert=plain`` and ``--capture=no`` might help although there's no guarantee. + +.. note:: + + Mind that patching ``stdlib`` functions and some third-party libraries used by pytest + might break pytest itself, therefore in those cases it is recommended to use + :meth:`MonkeyPatch.context` to limit the patching to the block you want tested: + + .. code-block:: python + + import functools + def test_partial(monkeypatch): + with monkeypatch.context() as m: + m.setattr(functools, "partial", 3) + assert functools.partial == 3 + + See issue `#3290 `_ for details. .. currentmodule:: _pytest.monkeypatch diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 4427908ab..36ef083f7 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -327,3 +327,15 @@ def test_issue1338_name_resolving(): monkeypatch.delattr('requests.sessions.Session.request') finally: monkeypatch.undo() + + +def test_context(): + monkeypatch = MonkeyPatch() + + import functools + import inspect + + with monkeypatch.context() as m: + m.setattr(functools, "partial", 3) + assert not inspect.isclass(functools.partial) + assert inspect.isclass(functools.partial)