From 6f13d1b03b1e1af7def99505234075878407767d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 6 Nov 2020 19:27:33 +0200 Subject: [PATCH] Export MonkeyPatch as pytest.MonkeyPatch We want to export `pytest.MonkeyPatch` for the purpose of type-annotating the `monkeypatch` fixture. For other fixtures we export in this way, we also make direct construction of them (e.g. `MonkeyPatch()`) private. But unlike the others, `MonkeyPatch` is also widely used directly already, mostly because the `monkeypatch` fixture only works in `function` scope (issue #363), but also in other cases. So making it private will be annoying and we don't offer a decent replacement yet. So, let's just make direct construction public & documented. --- changelog/8006.feature.rst | 8 ++++++++ doc/en/reference.rst | 6 ++---- src/_pytest/monkeypatch.py | 18 ++++++++++++++---- src/pytest/__init__.py | 2 ++ testing/test_monkeypatch.py | 10 ++++++++++ 5 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 changelog/8006.feature.rst diff --git a/changelog/8006.feature.rst b/changelog/8006.feature.rst new file mode 100644 index 00000000000..0203689ba4b --- /dev/null +++ b/changelog/8006.feature.rst @@ -0,0 +1,8 @@ +It is now possible to construct a :class:`~pytest.MonkeyPatch` object directly as ``pytest.MonkeyPatch()``, +in cases when the :fixture:`monkeypatch` fixture cannot be used. Previously some users imported it +from the private `_pytest.monkeypatch.MonkeyPatch` namespace. + +Additionally, :meth:`MonkeyPatch.context ` is now a classmethod, +and can be used as ``with MonkeyPatch.context() as mp: ...``. This is the recommended way to use +``MonkeyPatch`` directly, since unlike the ``monkeypatch`` fixture, an instance created directly +is not ``undo()``-ed automatically. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index c04b8da0b1b..cbe89fe0bf0 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -486,16 +486,14 @@ caplog monkeypatch ~~~~~~~~~~~ -.. currentmodule:: _pytest.monkeypatch - **Tutorial**: :doc:`monkeypatch`. .. autofunction:: _pytest.monkeypatch.monkeypatch() :no-auto-options: - Returns a :class:`MonkeyPatch` instance. + Returns a :class:`~pytest.MonkeyPatch` instance. -.. autoclass:: _pytest.monkeypatch.MonkeyPatch +.. autoclass:: pytest.MonkeyPatch :members: diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index df4726705d1..31b7b125b3c 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -111,8 +111,17 @@ def __repr__(self) -> str: @final class MonkeyPatch: - """Object returned by the ``monkeypatch`` fixture keeping a record of - setattr/item/env/syspath changes.""" + """Helper to conveniently monkeypatch attributes/items/environment + variables/syspath. + + Returned by the :fixture:`monkeypatch` fixture. + + :versionchanged:: 6.2 + Can now also be used directly as `pytest.MonkeyPatch()`, for when + the fixture is not available. In this case, use + :meth:`with MonkeyPatch.context() as mp: ` or remember to call + :meth:`undo` explicitly. + """ def __init__(self) -> None: self._setattr: List[Tuple[object, str, object]] = [] @@ -120,8 +129,9 @@ def __init__(self) -> None: self._cwd: Optional[str] = None self._savesyspath: Optional[List[str]] = None + @classmethod @contextmanager - def context(self) -> Generator["MonkeyPatch", None, None]: + def context(cls) -> Generator["MonkeyPatch", None, None]: """Context manager that returns a new :class:`MonkeyPatch` object which undoes any patching done inside the ``with`` block upon exit. @@ -140,7 +150,7 @@ def test_partial(monkeypatch): such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples of this see `#3290 `_. """ - m = MonkeyPatch() + m = cls() try: yield m finally: diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index a9c1ee0282b..d7a5b22997f 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -19,6 +19,7 @@ from _pytest.main import Session from _pytest.mark import MARK_GEN as mark from _pytest.mark import param +from _pytest.monkeypatch import MonkeyPatch from _pytest.nodes import Collector from _pytest.nodes import File from _pytest.nodes import Item @@ -74,6 +75,7 @@ "main", "mark", "Module", + "MonkeyPatch", "Package", "param", "PytestAssertRewriteWarning", diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 73fe313e5c9..c20ff7480a8 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -409,6 +409,16 @@ def test_context() -> None: assert inspect.isclass(functools.partial) +def test_context_classmethod() -> None: + class A: + x = 1 + + with MonkeyPatch.context() as m: + m.setattr(A, "x", 2) + assert A.x == 2 + assert A.x == 1 + + def test_syspath_prepend_with_namespace_packages( testdir: Testdir, monkeypatch: MonkeyPatch ) -> None: