461 lines
12 KiB
Python
461 lines
12 KiB
Python
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:
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr("os.path.abspath", lambda x: "hello2")
|
|
assert os.path.abspath("123") == "hello2"
|
|
|
|
def test_string_expression_class(self, monkeypatch: MonkeyPatch) -> None:
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr("_pytest.config.Config", 42)
|
|
import _pytest
|
|
|
|
assert _pytest.config.Config == 42 # type: ignore
|
|
|
|
def test_unicode_string(self, monkeypatch: MonkeyPatch) -> None:
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr("_pytest.config.Config", 42)
|
|
import _pytest
|
|
|
|
assert _pytest.config.Config == 42 # type: ignore
|
|
mp.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
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr("os.path.qweqwe", 42, raising=False)
|
|
assert os.path.qweqwe == 42 # type: ignore
|
|
|
|
def test_delattr(self, monkeypatch: MonkeyPatch) -> None:
|
|
with monkeypatch.context() as mp:
|
|
mp.delattr("os.path.abspath")
|
|
assert not hasattr(os.path, "abspath")
|
|
mp.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
|