From 659c044372ece204146fee9164fb6144c494eb58 Mon Sep 17 00:00:00 2001 From: feuillemorte Date: Tue, 10 Apr 2018 20:17:51 +0300 Subject: [PATCH 1/6] #3290 Improved monkeypatch to support some form of with statement --- _pytest/monkeypatch.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py index c82ffd053b4..126c9ca16a4 100644 --- a/_pytest/monkeypatch.py +++ b/_pytest/monkeypatch.py @@ -4,6 +4,8 @@ import os import sys import re +from contextlib import contextmanager + import six from _pytest.fixtures import fixture @@ -106,6 +108,14 @@ def __init__(self): self._cwd = None self._savesyspath = None + @contextmanager + def context(self): + 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. From 3d60f955f037e53a4be0a6561d664b8cb43367a9 Mon Sep 17 00:00:00 2001 From: feuillemorte Date: Tue, 10 Apr 2018 20:18:05 +0300 Subject: [PATCH 2/6] #3290 Added test --- testing/test_monkeypatch.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 4427908ab3b..b524df89b78 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -327,3 +327,16 @@ def test_issue1338_name_resolving(): monkeypatch.delattr('requests.sessions.Session.request') finally: 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 + """) + + result = testdir.runpytest() + result.stdout.fnmatch_lines("*1 passed*") From 97be076f2930adc6bd67f4bc4690a7ccf2540c41 Mon Sep 17 00:00:00 2001 From: feuillemorte Date: Tue, 10 Apr 2018 20:18:20 +0300 Subject: [PATCH 3/6] #3290 Added changelog file --- changelog/3290.bugfix | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog/3290.bugfix diff --git a/changelog/3290.bugfix b/changelog/3290.bugfix new file mode 100644 index 00000000000..c036501c8f5 --- /dev/null +++ b/changelog/3290.bugfix @@ -0,0 +1,2 @@ +Improved `monkeypatch` to support some form of with statement. Now you can use `with monkeypatch.context() as m:` +construction to avoid damage of Pytest. From a4daac7eb03f934752929297fe9d9329a4e86850 Mon Sep 17 00:00:00 2001 From: feuillemorte Date: Tue, 10 Apr 2018 20:30:10 +0300 Subject: [PATCH 4/6] #3290 Fix doc --- doc/en/monkeypatch.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/en/monkeypatch.rst b/doc/en/monkeypatch.rst index a2327d4fa98..221a8efe315 100644 --- a/doc/en/monkeypatch.rst +++ b/doc/en/monkeypatch.rst @@ -61,6 +61,17 @@ 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. + + To avoid damage of pytest from patching python stdlib functions use ``with`` + construction:: + + # content of test_module.py + + import functools + def test_partial(monkeypatch): + with monkeypatch.context() as m: + m.setattr(functools, "partial", 3) + assert functools.partial == 3 .. currentmodule:: _pytest.monkeypatch From ba7cad3962d646768351bb12fb9922720c46f8f8 Mon Sep 17 00:00:00 2001 From: feuillemorte Date: Fri, 13 Apr 2018 16:00:07 +0300 Subject: [PATCH 5/6] #3290 Fix comments --- _pytest/monkeypatch.py | 15 +++++++++++++++ changelog/3290.bugfix | 2 -- changelog/3290.feature | 2 ++ doc/en/monkeypatch.rst | 10 +++++++--- testing/test_monkeypatch.py | 17 ++++++++--------- 5 files changed, 32 insertions(+), 14 deletions(-) delete mode 100644 changelog/3290.bugfix create mode 100644 changelog/3290.feature diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py index 126c9ca16a4..78db6064df5 100644 --- a/_pytest/monkeypatch.py +++ b/_pytest/monkeypatch.py @@ -110,6 +110,21 @@ def __init__(self): @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 c036501c8f5..00000000000 --- 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 00000000000..a40afcb1a6b --- /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 221a8efe315..3dc311dd167 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 b524df89b78..36ef083f781 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(""" +def test_context(): + monkeypatch = MonkeyPatch() + import functools - def test_partial(monkeypatch): - with monkeypatch.context() as m: - m.setattr(functools, "partial", 3) - assert functools.partial == 3 - """) + import inspect - result = testdir.runpytest() - result.stdout.fnmatch_lines("*1 passed*") + with monkeypatch.context() as m: + m.setattr(functools, "partial", 3) + assert not inspect.isclass(functools.partial) + assert inspect.isclass(functools.partial) From 283ac8bbf4e8c9d270419815802a6a4460418a8e Mon Sep 17 00:00:00 2001 From: feuillemorte Date: Sat, 14 Apr 2018 21:06:58 +0300 Subject: [PATCH 6/6] #3290 Fix doc --- doc/en/monkeypatch.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/en/monkeypatch.rst b/doc/en/monkeypatch.rst index 3dc311dd167..b25e07f9a69 100644 --- a/doc/en/monkeypatch.rst +++ b/doc/en/monkeypatch.rst @@ -69,6 +69,7 @@ so that any attempts within tests to create http requests will fail. :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: