import os import re import sys import textwrap from pathlib import Path from typing import Dict from typing import Generator from typing import Type import pytest from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester @pytest.fixture def mp() -> Generator[MonkeyPatch, None, None]: cwd = os.getcwd() sys_path = list(sys.path) yield MonkeyPatch() sys.path[:] = sys_path os.chdir(cwd) def test_setattr() -> None: class A: x = 1 monkeypatch = MonkeyPatch() pytest.raises(AttributeError, monkeypatch.setattr, A, "notexists", 2) monkeypatch.setattr(A, "y", 2, raising=False) assert A.y == 2 # type: ignore monkeypatch.undo() assert not hasattr(A, "y") monkeypatch = MonkeyPatch() monkeypatch.setattr(A, "x", 2) assert A.x == 2 monkeypatch.setattr(A, "x", 3) assert A.x == 3 monkeypatch.undo() assert A.x == 1 A.x = 5 monkeypatch.undo() # double-undo makes no modification assert A.x == 5 with pytest.raises(TypeError): monkeypatch.setattr(A, "y") # type: ignore[call-overload] class TestSetattrWithImportPath: def test_string_expression(self, monkeypatch: MonkeyPatch) -> None: monkeypatch.setattr("os.path.abspath", lambda x: "hello2") assert os.path.abspath("123") == "hello2" def test_string_expression_class(self, monkeypatch: MonkeyPatch) -> None: monkeypatch.setattr("_pytest.config.Config", 42) import _pytest assert _pytest.config.Config == 42 # type: ignore def test_unicode_string(self, monkeypatch: MonkeyPatch) -> None: monkeypatch.setattr("_pytest.config.Config", 42) import _pytest assert _pytest.config.Config == 42 # type: ignore monkeypatch.delattr("_pytest.config.Config") def test_wrong_target(self, monkeypatch: MonkeyPatch) -> None: with pytest.raises(TypeError): monkeypatch.setattr(None, None) # type: ignore[call-overload] def test_unknown_import(self, monkeypatch: MonkeyPatch) -> None: with pytest.raises(ImportError): monkeypatch.setattr("unkn123.classx", None) def test_unknown_attr(self, monkeypatch: MonkeyPatch) -> None: with pytest.raises(AttributeError): monkeypatch.setattr("os.path.qweqwe", None) def test_unknown_attr_non_raising(self, monkeypatch: MonkeyPatch) -> None: # https://github.com/pytest-dev/pytest/issues/746 monkeypatch.setattr("os.path.qweqwe", 42, raising=False) assert os.path.qweqwe == 42 # type: ignore def test_delattr(self, monkeypatch: MonkeyPatch) -> None: monkeypatch.delattr("os.path.abspath") assert not hasattr(os.path, "abspath") monkeypatch.undo() assert os.path.abspath def test_delattr() -> None: class A: x = 1 monkeypatch = MonkeyPatch() monkeypatch.delattr(A, "x") assert not hasattr(A, "x") monkeypatch.undo() assert A.x == 1 monkeypatch = MonkeyPatch() monkeypatch.delattr(A, "x") pytest.raises(AttributeError, monkeypatch.delattr, A, "y") monkeypatch.delattr(A, "y", raising=False) monkeypatch.setattr(A, "x", 5, raising=False) assert A.x == 5 monkeypatch.undo() assert A.x == 1 def test_setitem() -> None: d = {"x": 1} monkeypatch = MonkeyPatch() monkeypatch.setitem(d, "x", 2) monkeypatch.setitem(d, "y", 1700) monkeypatch.setitem(d, "y", 1700) assert d["x"] == 2 assert d["y"] == 1700 monkeypatch.setitem(d, "x", 3) assert d["x"] == 3 monkeypatch.undo() assert d["x"] == 1 assert "y" not in d d["x"] = 5 monkeypatch.undo() assert d["x"] == 5 def test_setitem_deleted_meanwhile() -> None: d: Dict[str, object] = {} monkeypatch = MonkeyPatch() monkeypatch.setitem(d, "x", 2) del d["x"] monkeypatch.undo() assert not d @pytest.mark.parametrize("before", [True, False]) def test_setenv_deleted_meanwhile(before: bool) -> None: key = "qwpeoip123" if before: os.environ[key] = "world" monkeypatch = MonkeyPatch() monkeypatch.setenv(key, "hello") del os.environ[key] monkeypatch.undo() if before: assert os.environ[key] == "world" del os.environ[key] else: assert key not in os.environ def test_delitem() -> None: d: Dict[str, object] = {"x": 1} monkeypatch = MonkeyPatch() monkeypatch.delitem(d, "x") assert "x" not in d monkeypatch.delitem(d, "y", raising=False) pytest.raises(KeyError, monkeypatch.delitem, d, "y") assert not d monkeypatch.setitem(d, "y", 1700) assert d["y"] == 1700 d["hello"] = "world" monkeypatch.setitem(d, "x", 1500) assert d["x"] == 1500 monkeypatch.undo() assert d == {"hello": "world", "x": 1} def test_setenv() -> None: monkeypatch = MonkeyPatch() with pytest.warns(pytest.PytestWarning): monkeypatch.setenv("XYZ123", 2) # type: ignore[arg-type] import os assert os.environ["XYZ123"] == "2" monkeypatch.undo() assert "XYZ123" not in os.environ def test_delenv() -> None: name = "xyz1234" assert name not in os.environ monkeypatch = MonkeyPatch() pytest.raises(KeyError, monkeypatch.delenv, name, raising=True) monkeypatch.delenv(name, raising=False) monkeypatch.undo() os.environ[name] = "1" try: monkeypatch = MonkeyPatch() monkeypatch.delenv(name) assert name not in os.environ monkeypatch.setenv(name, "3") assert os.environ[name] == "3" monkeypatch.undo() assert os.environ[name] == "1" finally: if name in os.environ: del os.environ[name] class TestEnvironWarnings: """ os.environ keys and values should be native strings, otherwise it will cause problems with other modules (notably subprocess). On Python 2 os.environ accepts anything without complaining, while Python 3 does the right thing and raises an error. """ VAR_NAME = "PYTEST_INTERNAL_MY_VAR" def test_setenv_non_str_warning(self, monkeypatch: MonkeyPatch) -> None: value = 2 msg = ( "Value of environment variable PYTEST_INTERNAL_MY_VAR type should be str, " "but got 2 (type: int); converted to str implicitly" ) with pytest.warns(pytest.PytestWarning, match=re.escape(msg)): monkeypatch.setenv(str(self.VAR_NAME), value) # type: ignore[arg-type] def test_setenv_prepend() -> None: import os monkeypatch = MonkeyPatch() monkeypatch.setenv("XYZ123", "2", prepend="-") monkeypatch.setenv("XYZ123", "3", prepend="-") assert os.environ["XYZ123"] == "3-2" monkeypatch.undo() assert "XYZ123" not in os.environ def test_monkeypatch_plugin(pytester: Pytester) -> None: reprec = pytester.inline_runsource( """ def test_method(monkeypatch): assert monkeypatch.__class__.__name__ == "MonkeyPatch" """ ) res = reprec.countoutcomes() assert tuple(res) == (1, 0, 0), res def test_syspath_prepend(mp: MonkeyPatch) -> None: old = list(sys.path) mp.syspath_prepend("world") mp.syspath_prepend("hello") assert sys.path[0] == "hello" assert sys.path[1] == "world" mp.undo() assert sys.path == old mp.undo() assert sys.path == old def test_syspath_prepend_double_undo(mp: MonkeyPatch) -> None: old_syspath = sys.path[:] try: mp.syspath_prepend("hello world") mp.undo() sys.path.append("more hello world") mp.undo() assert sys.path[-1] == "more hello world" finally: sys.path[:] = old_syspath def test_chdir_with_path_local(mp: MonkeyPatch, tmp_path: Path) -> None: mp.chdir(tmp_path) assert os.getcwd() == str(tmp_path) def test_chdir_with_str(mp: MonkeyPatch, tmp_path: Path) -> None: mp.chdir(str(tmp_path)) assert os.getcwd() == str(tmp_path) def test_chdir_undo(mp: MonkeyPatch, tmp_path: Path) -> None: cwd = os.getcwd() mp.chdir(tmp_path) mp.undo() assert os.getcwd() == cwd def test_chdir_double_undo(mp: MonkeyPatch, tmp_path: Path) -> None: mp.chdir(str(tmp_path)) mp.undo() os.chdir(tmp_path) mp.undo() assert os.getcwd() == str(tmp_path) def test_issue185_time_breaks(pytester: Pytester) -> None: pytester.makepyfile( """ import time def test_m(monkeypatch): def f(): raise Exception monkeypatch.setattr(time, "time", f) """ ) result = pytester.runpytest() result.stdout.fnmatch_lines( """ *1 passed* """ ) def test_importerror(pytester: Pytester) -> None: p = pytester.mkpydir("package") p.joinpath("a.py").write_text( textwrap.dedent( """\ import doesnotexist x = 1 """ ) ) pytester.path.joinpath("test_importerror.py").write_text( textwrap.dedent( """\ def test_importerror(monkeypatch): monkeypatch.setattr('package.a.x', 2) """ ) ) result = pytester.runpytest() result.stdout.fnmatch_lines( """ *import error in package.a: No module named 'doesnotexist'* """ ) class Sample: @staticmethod def hello() -> bool: return True class SampleInherit(Sample): pass @pytest.mark.parametrize( "Sample", [Sample, SampleInherit], ids=["new", "new-inherit"], ) def test_issue156_undo_staticmethod(Sample: Type[Sample]) -> None: monkeypatch = MonkeyPatch() monkeypatch.setattr(Sample, "hello", None) assert Sample.hello is None monkeypatch.undo() # type: ignore[unreachable] assert Sample.hello() def test_undo_class_descriptors_delattr() -> None: class SampleParent: @classmethod def hello(_cls): pass @staticmethod def world(): pass class SampleChild(SampleParent): pass monkeypatch = MonkeyPatch() original_hello = SampleChild.hello original_world = SampleChild.world monkeypatch.delattr(SampleParent, "hello") monkeypatch.delattr(SampleParent, "world") assert getattr(SampleParent, "hello", None) is None assert getattr(SampleParent, "world", None) is None monkeypatch.undo() assert original_hello == SampleChild.hello assert original_world == SampleChild.world def test_issue1338_name_resolving() -> None: pytest.importorskip("requests") monkeypatch = MonkeyPatch() try: monkeypatch.delattr("requests.sessions.Session.request") finally: monkeypatch.undo() def test_context() -> None: 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) 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( pytester: Pytester, monkeypatch: MonkeyPatch ) -> None: for dirname in "hello", "world": d = pytester.mkdir(dirname) ns = d.joinpath("ns_pkg") ns.mkdir() ns.joinpath("__init__.py").write_text( "__import__('pkg_resources').declare_namespace(__name__)" ) lib = ns.joinpath(dirname) lib.mkdir() lib.joinpath("__init__.py").write_text("def check(): return %r" % dirname) monkeypatch.syspath_prepend("hello") import ns_pkg.hello assert ns_pkg.hello.check() == "hello" with pytest.raises(ImportError): import ns_pkg.world # Prepending should call fixup_namespace_packages. monkeypatch.syspath_prepend("world") import ns_pkg.world assert ns_pkg.world.check() == "world" # Should invalidate caches via importlib.invalidate_caches. modules_tmpdir = pytester.mkdir("modules_tmpdir") monkeypatch.syspath_prepend(str(modules_tmpdir)) modules_tmpdir.joinpath("main_app.py").write_text("app = True") from main_app import app # noqa: F401