monkeypatch: add type annotations
This commit is contained in:
parent
f84ffd9747
commit
b4f046b777
|
@ -4,17 +4,29 @@ import re
|
|||
import sys
|
||||
import warnings
|
||||
from contextlib import contextmanager
|
||||
from typing import Any
|
||||
from typing import Generator
|
||||
from typing import List
|
||||
from typing import MutableMapping
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
import pytest
|
||||
from _pytest.compat import overload
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.pathlib import Path
|
||||
|
||||
RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$")
|
||||
|
||||
|
||||
K = TypeVar("K")
|
||||
V = TypeVar("V")
|
||||
|
||||
|
||||
@fixture
|
||||
def monkeypatch():
|
||||
def monkeypatch() -> Generator["MonkeyPatch", None, None]:
|
||||
"""The returned ``monkeypatch`` fixture provides these
|
||||
helper methods to modify objects, dictionaries or os.environ::
|
||||
|
||||
|
@ -37,7 +49,7 @@ def monkeypatch():
|
|||
mpatch.undo()
|
||||
|
||||
|
||||
def resolve(name):
|
||||
def resolve(name: str) -> object:
|
||||
# simplified from zope.dottedname
|
||||
parts = name.split(".")
|
||||
|
||||
|
@ -66,7 +78,7 @@ def resolve(name):
|
|||
return found
|
||||
|
||||
|
||||
def annotated_getattr(obj, name, ann):
|
||||
def annotated_getattr(obj: object, name: str, ann: str) -> object:
|
||||
try:
|
||||
obj = getattr(obj, name)
|
||||
except AttributeError:
|
||||
|
@ -78,7 +90,7 @@ def annotated_getattr(obj, name, ann):
|
|||
return obj
|
||||
|
||||
|
||||
def derive_importpath(import_path, raising):
|
||||
def derive_importpath(import_path: str, raising: bool) -> Tuple[str, object]:
|
||||
if not isinstance(import_path, str) or "." not in import_path:
|
||||
raise TypeError(
|
||||
"must be absolute import path string, not {!r}".format(import_path)
|
||||
|
@ -91,7 +103,7 @@ def derive_importpath(import_path, raising):
|
|||
|
||||
|
||||
class Notset:
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return "<notset>"
|
||||
|
||||
|
||||
|
@ -102,11 +114,13 @@ class MonkeyPatch:
|
|||
""" Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._setattr = []
|
||||
self._setitem = []
|
||||
self._cwd = None
|
||||
self._savesyspath = None
|
||||
def __init__(self) -> None:
|
||||
self._setattr = [] # type: List[Tuple[object, str, object]]
|
||||
self._setitem = (
|
||||
[]
|
||||
) # type: List[Tuple[MutableMapping[Any, Any], object, object]]
|
||||
self._cwd = None # type: Optional[str]
|
||||
self._savesyspath = None # type: Optional[List[str]]
|
||||
|
||||
@contextmanager
|
||||
def context(self) -> Generator["MonkeyPatch", None, None]:
|
||||
|
@ -133,7 +147,25 @@ class MonkeyPatch:
|
|||
finally:
|
||||
m.undo()
|
||||
|
||||
def setattr(self, target, name, value=notset, raising=True):
|
||||
@overload
|
||||
def setattr(
|
||||
self, target: str, name: object, value: Notset = ..., raising: bool = ...,
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@overload # noqa: F811
|
||||
def setattr( # noqa: F811
|
||||
self, target: object, name: str, value: object, raising: bool = ...,
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def setattr( # noqa: F811
|
||||
self,
|
||||
target: Union[str, object],
|
||||
name: Union[object, str],
|
||||
value: object = notset,
|
||||
raising: bool = True,
|
||||
) -> None:
|
||||
""" Set attribute value on target, memorizing the old value.
|
||||
By default raise AttributeError if the attribute did not exist.
|
||||
|
||||
|
@ -150,7 +182,7 @@ class MonkeyPatch:
|
|||
__tracebackhide__ = True
|
||||
import inspect
|
||||
|
||||
if value is notset:
|
||||
if isinstance(value, Notset):
|
||||
if not isinstance(target, str):
|
||||
raise TypeError(
|
||||
"use setattr(target, name, value) or "
|
||||
|
@ -159,6 +191,13 @@ class MonkeyPatch:
|
|||
)
|
||||
value = name
|
||||
name, target = derive_importpath(target, raising)
|
||||
else:
|
||||
if not isinstance(name, str):
|
||||
raise TypeError(
|
||||
"use setattr(target, name, value) with name being a string or "
|
||||
"setattr(target, value) with target being a dotted "
|
||||
"import string"
|
||||
)
|
||||
|
||||
oldval = getattr(target, name, notset)
|
||||
if raising and oldval is notset:
|
||||
|
@ -170,7 +209,12 @@ class MonkeyPatch:
|
|||
self._setattr.append((target, name, oldval))
|
||||
setattr(target, name, value)
|
||||
|
||||
def delattr(self, target, name=notset, raising=True):
|
||||
def delattr(
|
||||
self,
|
||||
target: Union[object, str],
|
||||
name: Union[str, Notset] = notset,
|
||||
raising: bool = True,
|
||||
) -> None:
|
||||
""" Delete attribute ``name`` from ``target``, by default raise
|
||||
AttributeError it the attribute did not previously exist.
|
||||
|
||||
|
@ -184,7 +228,7 @@ class MonkeyPatch:
|
|||
__tracebackhide__ = True
|
||||
import inspect
|
||||
|
||||
if name is notset:
|
||||
if isinstance(name, Notset):
|
||||
if not isinstance(target, str):
|
||||
raise TypeError(
|
||||
"use delattr(target, name) or "
|
||||
|
@ -204,12 +248,12 @@ class MonkeyPatch:
|
|||
self._setattr.append((target, name, oldval))
|
||||
delattr(target, name)
|
||||
|
||||
def setitem(self, dic, name, value):
|
||||
def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None:
|
||||
""" Set dictionary entry ``name`` to value. """
|
||||
self._setitem.append((dic, name, dic.get(name, notset)))
|
||||
dic[name] = value
|
||||
|
||||
def delitem(self, dic, name, raising=True):
|
||||
def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None:
|
||||
""" Delete ``name`` from dict. Raise KeyError if it doesn't exist.
|
||||
|
||||
If ``raising`` is set to False, no exception will be raised if the
|
||||
|
@ -222,7 +266,7 @@ class MonkeyPatch:
|
|||
self._setitem.append((dic, name, dic.get(name, notset)))
|
||||
del dic[name]
|
||||
|
||||
def setenv(self, name, value, prepend=None):
|
||||
def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
|
||||
""" Set environment variable ``name`` to ``value``. If ``prepend``
|
||||
is a character, read the current environment variable value
|
||||
and prepend the ``value`` adjoined with the ``prepend`` character."""
|
||||
|
@ -241,16 +285,17 @@ class MonkeyPatch:
|
|||
value = value + prepend + os.environ[name]
|
||||
self.setitem(os.environ, name, value)
|
||||
|
||||
def delenv(self, name, raising=True):
|
||||
def delenv(self, name: str, raising: bool = True) -> None:
|
||||
""" Delete ``name`` from the environment. Raise KeyError if it does
|
||||
not exist.
|
||||
|
||||
If ``raising`` is set to False, no exception will be raised if the
|
||||
environment variable is missing.
|
||||
"""
|
||||
self.delitem(os.environ, name, raising=raising)
|
||||
environ = os.environ # type: MutableMapping[str, str]
|
||||
self.delitem(environ, name, raising=raising)
|
||||
|
||||
def syspath_prepend(self, path):
|
||||
def syspath_prepend(self, path) -> None:
|
||||
""" Prepend ``path`` to ``sys.path`` list of import locations. """
|
||||
from pkg_resources import fixup_namespace_packages
|
||||
|
||||
|
@ -272,7 +317,7 @@ class MonkeyPatch:
|
|||
|
||||
invalidate_caches()
|
||||
|
||||
def chdir(self, path):
|
||||
def chdir(self, path) -> None:
|
||||
""" Change the current working directory to the specified path.
|
||||
Path can be a string or a py.path.local object.
|
||||
"""
|
||||
|
@ -286,7 +331,7 @@ class MonkeyPatch:
|
|||
else:
|
||||
os.chdir(path)
|
||||
|
||||
def undo(self):
|
||||
def undo(self) -> None:
|
||||
""" Undo previous changes. This call consumes the
|
||||
undo stack. Calling it a second time has no effect unless
|
||||
you do more monkeypatching after the undo call.
|
||||
|
@ -306,14 +351,14 @@ class MonkeyPatch:
|
|||
else:
|
||||
delattr(obj, name)
|
||||
self._setattr[:] = []
|
||||
for dictionary, name, value in reversed(self._setitem):
|
||||
for dictionary, key, value in reversed(self._setitem):
|
||||
if value is notset:
|
||||
try:
|
||||
del dictionary[name]
|
||||
del dictionary[key]
|
||||
except KeyError:
|
||||
pass # was already deleted, so we have the desired state
|
||||
else:
|
||||
dictionary[name] = value
|
||||
dictionary[key] = value
|
||||
self._setitem[:] = []
|
||||
if self._savesyspath is not None:
|
||||
sys.path[:] = self._savesyspath
|
||||
|
|
|
@ -5,9 +5,12 @@ import textwrap
|
|||
from typing import Dict
|
||||
from typing import Generator
|
||||
|
||||
import py
|
||||
|
||||
import pytest
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
from _pytest.pytester import Testdir
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Type
|
||||
|
@ -45,9 +48,12 @@ def test_setattr() -> None:
|
|||
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):
|
||||
def test_string_expression(self, monkeypatch: MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("os.path.abspath", lambda x: "hello2")
|
||||
assert os.path.abspath("123") == "hello2"
|
||||
|
||||
|
@ -64,30 +70,31 @@ class TestSetattrWithImportPath:
|
|||
assert _pytest.config.Config == 42 # type: ignore
|
||||
monkeypatch.delattr("_pytest.config.Config")
|
||||
|
||||
def test_wrong_target(self, monkeypatch):
|
||||
pytest.raises(TypeError, lambda: monkeypatch.setattr(None, None))
|
||||
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):
|
||||
pytest.raises(ImportError, lambda: monkeypatch.setattr("unkn123.classx", None))
|
||||
def test_unknown_import(self, monkeypatch: MonkeyPatch) -> None:
|
||||
with pytest.raises(ImportError):
|
||||
monkeypatch.setattr("unkn123.classx", None)
|
||||
|
||||
def test_unknown_attr(self, monkeypatch):
|
||||
pytest.raises(
|
||||
AttributeError, lambda: monkeypatch.setattr("os.path.qweqwe", 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):
|
||||
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():
|
||||
def test_delattr() -> None:
|
||||
class A:
|
||||
x = 1
|
||||
|
||||
|
@ -107,7 +114,7 @@ def test_delattr():
|
|||
assert A.x == 1
|
||||
|
||||
|
||||
def test_setitem():
|
||||
def test_setitem() -> None:
|
||||
d = {"x": 1}
|
||||
monkeypatch = MonkeyPatch()
|
||||
monkeypatch.setitem(d, "x", 2)
|
||||
|
@ -135,7 +142,7 @@ def test_setitem_deleted_meanwhile() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("before", [True, False])
|
||||
def test_setenv_deleted_meanwhile(before):
|
||||
def test_setenv_deleted_meanwhile(before: bool) -> None:
|
||||
key = "qwpeoip123"
|
||||
if before:
|
||||
os.environ[key] = "world"
|
||||
|
@ -167,10 +174,10 @@ def test_delitem() -> None:
|
|||
assert d == {"hello": "world", "x": 1}
|
||||
|
||||
|
||||
def test_setenv():
|
||||
def test_setenv() -> None:
|
||||
monkeypatch = MonkeyPatch()
|
||||
with pytest.warns(pytest.PytestWarning):
|
||||
monkeypatch.setenv("XYZ123", 2)
|
||||
monkeypatch.setenv("XYZ123", 2) # type: ignore[arg-type]
|
||||
import os
|
||||
|
||||
assert os.environ["XYZ123"] == "2"
|
||||
|
@ -178,7 +185,7 @@ def test_setenv():
|
|||
assert "XYZ123" not in os.environ
|
||||
|
||||
|
||||
def test_delenv():
|
||||
def test_delenv() -> None:
|
||||
name = "xyz1234"
|
||||
assert name not in os.environ
|
||||
monkeypatch = MonkeyPatch()
|
||||
|
@ -208,31 +215,28 @@ class TestEnvironWarnings:
|
|||
|
||||
VAR_NAME = "PYTEST_INTERNAL_MY_VAR"
|
||||
|
||||
def test_setenv_non_str_warning(self, monkeypatch):
|
||||
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)
|
||||
monkeypatch.setenv(str(self.VAR_NAME), value) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_setenv_prepend():
|
||||
def test_setenv_prepend() -> None:
|
||||
import os
|
||||
|
||||
monkeypatch = MonkeyPatch()
|
||||
with pytest.warns(pytest.PytestWarning):
|
||||
monkeypatch.setenv("XYZ123", 2, prepend="-")
|
||||
assert os.environ["XYZ123"] == "2"
|
||||
with pytest.warns(pytest.PytestWarning):
|
||||
monkeypatch.setenv("XYZ123", 3, prepend="-")
|
||||
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(testdir):
|
||||
def test_monkeypatch_plugin(testdir: Testdir) -> None:
|
||||
reprec = testdir.inline_runsource(
|
||||
"""
|
||||
def test_method(monkeypatch):
|
||||
|
@ -243,7 +247,7 @@ def test_monkeypatch_plugin(testdir):
|
|||
assert tuple(res) == (1, 0, 0), res
|
||||
|
||||
|
||||
def test_syspath_prepend(mp: MonkeyPatch):
|
||||
def test_syspath_prepend(mp: MonkeyPatch) -> None:
|
||||
old = list(sys.path)
|
||||
mp.syspath_prepend("world")
|
||||
mp.syspath_prepend("hello")
|
||||
|
@ -255,7 +259,7 @@ def test_syspath_prepend(mp: MonkeyPatch):
|
|||
assert sys.path == old
|
||||
|
||||
|
||||
def test_syspath_prepend_double_undo(mp: MonkeyPatch):
|
||||
def test_syspath_prepend_double_undo(mp: MonkeyPatch) -> None:
|
||||
old_syspath = sys.path[:]
|
||||
try:
|
||||
mp.syspath_prepend("hello world")
|
||||
|
@ -267,24 +271,24 @@ def test_syspath_prepend_double_undo(mp: MonkeyPatch):
|
|||
sys.path[:] = old_syspath
|
||||
|
||||
|
||||
def test_chdir_with_path_local(mp: MonkeyPatch, tmpdir):
|
||||
def test_chdir_with_path_local(mp: MonkeyPatch, tmpdir: py.path.local) -> None:
|
||||
mp.chdir(tmpdir)
|
||||
assert os.getcwd() == tmpdir.strpath
|
||||
|
||||
|
||||
def test_chdir_with_str(mp: MonkeyPatch, tmpdir):
|
||||
def test_chdir_with_str(mp: MonkeyPatch, tmpdir: py.path.local) -> None:
|
||||
mp.chdir(tmpdir.strpath)
|
||||
assert os.getcwd() == tmpdir.strpath
|
||||
|
||||
|
||||
def test_chdir_undo(mp: MonkeyPatch, tmpdir):
|
||||
def test_chdir_undo(mp: MonkeyPatch, tmpdir: py.path.local) -> None:
|
||||
cwd = os.getcwd()
|
||||
mp.chdir(tmpdir)
|
||||
mp.undo()
|
||||
assert os.getcwd() == cwd
|
||||
|
||||
|
||||
def test_chdir_double_undo(mp: MonkeyPatch, tmpdir):
|
||||
def test_chdir_double_undo(mp: MonkeyPatch, tmpdir: py.path.local) -> None:
|
||||
mp.chdir(tmpdir.strpath)
|
||||
mp.undo()
|
||||
tmpdir.chdir()
|
||||
|
@ -292,7 +296,7 @@ def test_chdir_double_undo(mp: MonkeyPatch, tmpdir):
|
|||
assert os.getcwd() == tmpdir.strpath
|
||||
|
||||
|
||||
def test_issue185_time_breaks(testdir):
|
||||
def test_issue185_time_breaks(testdir: Testdir) -> None:
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import time
|
||||
|
@ -310,7 +314,7 @@ def test_issue185_time_breaks(testdir):
|
|||
)
|
||||
|
||||
|
||||
def test_importerror(testdir):
|
||||
def test_importerror(testdir: Testdir) -> None:
|
||||
p = testdir.mkpydir("package")
|
||||
p.join("a.py").write(
|
||||
textwrap.dedent(
|
||||
|
@ -360,7 +364,7 @@ def test_issue156_undo_staticmethod(Sample: "Type[Sample]") -> None:
|
|||
assert Sample.hello()
|
||||
|
||||
|
||||
def test_undo_class_descriptors_delattr():
|
||||
def test_undo_class_descriptors_delattr() -> None:
|
||||
class SampleParent:
|
||||
@classmethod
|
||||
def hello(_cls):
|
||||
|
@ -387,7 +391,7 @@ def test_undo_class_descriptors_delattr():
|
|||
assert original_world == SampleChild.world
|
||||
|
||||
|
||||
def test_issue1338_name_resolving():
|
||||
def test_issue1338_name_resolving() -> None:
|
||||
pytest.importorskip("requests")
|
||||
monkeypatch = MonkeyPatch()
|
||||
try:
|
||||
|
@ -396,7 +400,7 @@ def test_issue1338_name_resolving():
|
|||
monkeypatch.undo()
|
||||
|
||||
|
||||
def test_context():
|
||||
def test_context() -> None:
|
||||
monkeypatch = MonkeyPatch()
|
||||
|
||||
import functools
|
||||
|
@ -408,7 +412,9 @@ def test_context():
|
|||
assert inspect.isclass(functools.partial)
|
||||
|
||||
|
||||
def test_syspath_prepend_with_namespace_packages(testdir, monkeypatch):
|
||||
def test_syspath_prepend_with_namespace_packages(
|
||||
testdir: Testdir, monkeypatch: MonkeyPatch
|
||||
) -> None:
|
||||
for dirname in "hello", "world":
|
||||
d = testdir.mkdir(dirname)
|
||||
ns = d.mkdir("ns_pkg")
|
||||
|
|
Loading…
Reference in New Issue