diff --git a/changelog/8006.feature.rst b/changelog/8006.feature.rst new file mode 100644 index 000000000..0203689ba --- /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 c04b8da0b..cbe89fe0b 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 a50c7c8d5..a052f693a 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -111,8 +111,17 @@ notset = Notset() @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 @@ class MonkeyPatch: 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 @@ class 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 a9c1ee028..d7a5b2299 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -19,6 +19,7 @@ from _pytest.freeze_support import freeze_includes 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 @@ __all__ = [ "main", "mark", "Module", + "MonkeyPatch", "Package", "param", "PytestAssertRewriteWarning", diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 73fe313e5..c20ff7480 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: