aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/pytest/py3/_pytest/monkeypatch.py
diff options
context:
space:
mode:
authorarcadia-devtools <arcadia-devtools@yandex-team.ru>2022-02-09 12:00:52 +0300
committerDaniil Cherednik <dcherednik@yandex-team.ru>2022-02-10 15:58:17 +0300
commit8e1413fed79d1e8036e65228af6c93399ccf5502 (patch)
tree502c9df7b2614d20541c7a2d39d390e9a51877cc /contrib/python/pytest/py3/_pytest/monkeypatch.py
parent6b813c17d56d1d05f92c61ddc347d0e4d358fe85 (diff)
downloadydb-8e1413fed79d1e8036e65228af6c93399ccf5502.tar.gz
intermediate changes
ref:614ed510ddd3cdf86a8c5dbf19afd113397e0172
Diffstat (limited to 'contrib/python/pytest/py3/_pytest/monkeypatch.py')
-rw-r--r--contrib/python/pytest/py3/_pytest/monkeypatch.py223
1 files changed, 140 insertions, 83 deletions
diff --git a/contrib/python/pytest/py3/_pytest/monkeypatch.py b/contrib/python/pytest/py3/_pytest/monkeypatch.py
index ce1c0f6510..a052f693ac 100644
--- a/contrib/python/pytest/py3/_pytest/monkeypatch.py
+++ b/contrib/python/pytest/py3/_pytest/monkeypatch.py
@@ -1,22 +1,37 @@
-""" monkeypatching and mocking functionality. """
+"""Monkeypatching and mocking functionality."""
import os
import re
import sys
import warnings
from contextlib import contextmanager
+from pathlib import Path
+from typing import Any
from typing import Generator
-
-import pytest
+from typing import List
+from typing import MutableMapping
+from typing import Optional
+from typing import overload
+from typing import Tuple
+from typing import TypeVar
+from typing import Union
+
+from _pytest.compat import final
from _pytest.fixtures import fixture
-from _pytest.pathlib import Path
+from _pytest.warning_types import PytestWarning
RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$")
+K = TypeVar("K")
+V = TypeVar("V")
+
+
@fixture
-def monkeypatch():
- """The returned ``monkeypatch`` fixture provides these
- helper methods to modify objects, dictionaries or os.environ::
+def monkeypatch() -> Generator["MonkeyPatch", None, None]:
+ """A convenient fixture for monkey-patching.
+
+ The fixture provides these methods to modify objects, dictionaries or
+ os.environ::
monkeypatch.setattr(obj, name, value, raising=True)
monkeypatch.delattr(obj, name, raising=True)
@@ -27,18 +42,17 @@ def monkeypatch():
monkeypatch.syspath_prepend(path)
monkeypatch.chdir(path)
- All modifications will be undone after the requesting
- test function or fixture has finished. The ``raising``
- parameter determines if a KeyError or AttributeError
- will be raised if the set/deletion operation has no target.
+ All modifications will be undone after the requesting test function or
+ fixture has finished. The ``raising`` parameter determines if a KeyError
+ or AttributeError will be raised if the set/deletion operation has no target.
"""
mpatch = MonkeyPatch()
yield mpatch
mpatch.undo()
-def resolve(name):
- # simplified from zope.dottedname
+def resolve(name: str) -> object:
+ # Simplified from zope.dottedname.
parts = name.split(".")
used = parts.pop(0)
@@ -51,38 +65,35 @@ def resolve(name):
pass
else:
continue
- # we use explicit un-nesting of the handling block in order
- # to avoid nested exceptions on python 3
+ # We use explicit un-nesting of the handling block in order
+ # to avoid nested exceptions.
try:
__import__(used)
except ImportError as ex:
- # str is used for py2 vs py3
expected = str(ex).split()[-1]
if expected == used:
raise
else:
- raise ImportError("import error in {}: {}".format(used, ex))
+ raise ImportError(f"import error in {used}: {ex}") from ex
found = annotated_getattr(found, part, used)
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:
+ except AttributeError as e:
raise AttributeError(
"{!r} object at {} has no attribute {!r}".format(
type(obj).__name__, ann, name
)
- )
+ ) from e
return obj
-def derive_importpath(import_path, raising):
- if not isinstance(import_path, str) or "." not in import_path:
- raise TypeError(
- "must be absolute import path string, not {!r}".format(import_path)
- )
+def derive_importpath(import_path: str, raising: bool) -> Tuple[str, object]:
+ if not isinstance(import_path, str) or "." not in import_path: # type: ignore[unreachable]
+ raise TypeError(f"must be absolute import path string, not {import_path!r}")
module, attr = import_path.rsplit(".", 1)
target = resolve(module)
if raising:
@@ -91,32 +102,46 @@ def derive_importpath(import_path, raising):
class Notset:
- def __repr__(self):
+ def __repr__(self) -> str:
return "<notset>"
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: <context>` or remember to call
+ :meth:`undo` explicitly.
"""
- def __init__(self):
- self._setattr = []
- self._setitem = []
- self._cwd = None
- self._savesyspath = None
+ def __init__(self) -> None:
+ self._setattr: List[Tuple[object, str, object]] = []
+ self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = ([])
+ self._cwd: Optional[str] = None
+ self._savesyspath: Optional[List[str]] = None
+ @classmethod
@contextmanager
- def context(self) -> Generator["MonkeyPatch", None, None]:
- """
- Context manager that returns a new :class:`MonkeyPatch` object which
- undoes any patching done inside the ``with`` block upon exit:
+ 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.
+
+ Example:
.. code-block:: python
import functools
+
+
def test_partial(monkeypatch):
with monkeypatch.context() as m:
m.setattr(functools, "partial", 3)
@@ -125,30 +150,46 @@ class MonkeyPatch:
such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples
of this see `#3290 <https://github.com/pytest-dev/pytest/issues/3290>`_.
"""
- m = MonkeyPatch()
+ m = cls()
try:
yield m
finally:
m.undo()
- def setattr(self, target, name, value=notset, raising=True):
- """ Set attribute value on target, memorizing the old value.
- By default raise AttributeError if the attribute did not exist.
+ @overload
+ def setattr(
+ self, target: str, name: object, value: Notset = ..., raising: bool = ...,
+ ) -> None:
+ ...
+
+ @overload
+ def setattr(
+ self, target: object, name: str, value: object, raising: bool = ...,
+ ) -> None:
+ ...
+
+ def setattr(
+ 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.
For convenience you can specify a string as ``target`` which
will be interpreted as a dotted import path, with the last part
- being the attribute name. Example:
+ being the attribute name. For example,
``monkeypatch.setattr("os.getcwd", lambda: "/")``
would set the ``getcwd`` function of the ``os`` module.
- The ``raising`` value determines if the setattr should fail
- if the attribute is not already present (defaults to True
- which means it will raise).
+ Raises AttributeError if the attribute does not exist, unless
+ ``raising`` is set to False.
"""
__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 "
@@ -157,10 +198,17 @@ 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:
- raise AttributeError("{!r} has no attribute {!r}".format(target, name))
+ raise AttributeError(f"{target!r} has no attribute {name!r}")
# avoid class descriptors like staticmethod/classmethod
if inspect.isclass(target):
@@ -168,21 +216,25 @@ class MonkeyPatch:
self._setattr.append((target, name, oldval))
setattr(target, name, value)
- def delattr(self, target, name=notset, raising=True):
- """ Delete attribute ``name`` from ``target``, by default raise
- AttributeError it the attribute did not previously exist.
+ def delattr(
+ self,
+ target: Union[object, str],
+ name: Union[str, Notset] = notset,
+ raising: bool = True,
+ ) -> None:
+ """Delete attribute ``name`` from ``target``.
If no ``name`` is specified and ``target`` is a string
it will be interpreted as a dotted import path with the
last part being the attribute name.
- If ``raising`` is set to False, no exception will be raised if the
- attribute is missing.
+ Raises AttributeError it the attribute does not exist, unless
+ ``raising`` is set to False.
"""
__tracebackhide__ = True
import inspect
- if name is notset:
+ if isinstance(name, Notset):
if not isinstance(target, str):
raise TypeError(
"use delattr(target, name) or "
@@ -202,16 +254,16 @@ class MonkeyPatch:
self._setattr.append((target, name, oldval))
delattr(target, name)
- def setitem(self, dic, name, value):
- """ Set dictionary entry ``name`` to 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):
- """ Delete ``name`` from dict. Raise KeyError if it doesn't exist.
+ def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None:
+ """Delete ``name`` from dict.
- If ``raising`` is set to False, no exception will be raised if the
- key is missing.
+ Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
+ False.
"""
if name not in dic:
if raising:
@@ -220,13 +272,16 @@ class MonkeyPatch:
self._setitem.append((dic, name, dic.get(name, notset)))
del dic[name]
- def setenv(self, name, value, prepend=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."""
+ 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.
+ """
if not isinstance(value, str):
- warnings.warn(
- pytest.PytestWarning(
+ warnings.warn( # type: ignore[unreachable]
+ PytestWarning(
"Value of environment variable {name} type should be str, but got "
"{value!r} (type: {type}); converted to str implicitly".format(
name=name, value=value, type=type(value).__name__
@@ -239,17 +294,17 @@ class MonkeyPatch:
value = value + prepend + os.environ[name]
self.setitem(os.environ, name, value)
- def delenv(self, name, raising=True):
- """ Delete ``name`` from the environment. Raise KeyError if it does
- not exist.
+ def delenv(self, name: str, raising: bool = True) -> None:
+ """Delete ``name`` from the environment.
- If ``raising`` is set to False, no exception will be raised if the
- environment variable is missing.
+ Raises ``KeyError`` if it does not exist, unless ``raising`` is set to
+ False.
"""
- self.delitem(os.environ, name, raising=raising)
+ environ: MutableMapping[str, str] = os.environ
+ self.delitem(environ, name, raising=raising)
- def syspath_prepend(self, path):
- """ Prepend ``path`` to ``sys.path`` list of import locations. """
+ def syspath_prepend(self, path) -> None:
+ """Prepend ``path`` to ``sys.path`` list of import locations."""
from pkg_resources import fixup_namespace_packages
if self._savesyspath is None:
@@ -270,8 +325,9 @@ class MonkeyPatch:
invalidate_caches()
- def chdir(self, path):
- """ Change the current working directory to the specified 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.
"""
if self._cwd is None:
@@ -279,15 +335,16 @@ class MonkeyPatch:
if hasattr(path, "chdir"):
path.chdir()
elif isinstance(path, Path):
- # modern python uses the fspath protocol here LEGACY
+ # Modern python uses the fspath protocol here LEGACY
os.chdir(str(path))
else:
os.chdir(path)
- def undo(self):
- """ 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.
+ 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.
There is generally no need to call `undo()`, since it is
called automatically during tear-down.
@@ -304,14 +361,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
+ 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