diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py index 126c9ca16..78db6064d 100644 --- a/_pytest/monkeypatch.py +++ b/_pytest/monkeypatch.py @@ -110,6 +110,21 @@ class MonkeyPatch(object): @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 diff --git a/changelog/3290.bugfix b/changelog/3290.bugfix deleted file mode 100644 index c036501c8..000000000 --- a/changelog/3290.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Improved `monkeypatch` to support some form of with statement. Now you can use `with monkeypatch.context() as m:` -construction to avoid damage of Pytest. 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 221a8efe3..3dc311dd1 100644 --- a/doc/en/monkeypatch.rst +++ b/doc/en/monkeypatch.rst @@ -62,16 +62,20 @@ so that any attempts within tests to create http requests will fail. unavoidable, passing ``--tb=native``, ``--assert=plain`` and ``--capture=no`` might help although there's no guarantee. - To avoid damage of pytest from patching python stdlib functions use ``with`` - construction:: +.. note:: - # content of test_module.py + 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 b524df89b..36ef083f7 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -329,14 +329,13 @@ def test_issue1338_name_resolving(): monkeypatch.undo() -def test_context(testdir): - testdir.makepyfile(""" - import functools - def test_partial(monkeypatch): - with monkeypatch.context() as m: - m.setattr(functools, "partial", 3) - assert functools.partial == 3 - """) +def test_context(): + monkeypatch = MonkeyPatch() - result = testdir.runpytest() - result.stdout.fnmatch_lines("*1 passed*") + 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)