-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from iterative/matchers
introduce matchers
- Loading branch information
Showing
13 changed files
with
480 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
[run] | ||
branch = True | ||
source = pytest_test_utils | ||
|
||
[report] | ||
exclude_lines = | ||
if TYPE_CHECKING: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,9 @@ | ||
from ._any import ANY | ||
from . import matchers | ||
from .tmp_dir import TmpDir | ||
from .tmp_dir_factory import TempDirFactory | ||
|
||
__all__ = ["ANY", "TmpDir", "TempDirFactory"] | ||
__all__ = [ | ||
"matchers", | ||
"TmpDir", | ||
"TempDirFactory", | ||
] |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
from datetime import datetime, timedelta | ||
from typing import Optional | ||
|
||
from _pytest.python_api import ApproxBase | ||
|
||
# pylint: disable=invalid-name | ||
|
||
|
||
class approx_datetime(ApproxBase): # pylint: disable=abstract-method | ||
"""Perform approximate comparisons between datetime or timedelta.""" | ||
|
||
default_tolerance = timedelta(seconds=1) | ||
expected: datetime | ||
abs: timedelta | ||
|
||
def __init__( | ||
self, | ||
expected: datetime, | ||
abs: Optional[timedelta] = None, # pylint: disable=redefined-builtin | ||
) -> None: | ||
"""Initialize the approx_datetime with `abs` as tolerance.""" | ||
assert isinstance(expected, datetime) | ||
abs = abs or self.default_tolerance | ||
assert abs >= timedelta( | ||
0 | ||
), f"absolute tolerance can't be negative: {abs}" | ||
super().__init__(expected, abs=abs) | ||
|
||
def __repr__(self) -> str: # pragma: no cover | ||
"""String repr for approx_datetime, shown during failure.""" | ||
return f"approx_datetime({self.expected!r} ± {self.abs!r})" | ||
|
||
def __eq__(self, actual: object) -> bool: | ||
"""Checking for equality with certain amount of tolerance.""" | ||
assert isinstance(actual, datetime), "expected type of datetime" | ||
return abs(self.expected - actual) <= self.abs |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
import collections.abc | ||
import re | ||
from datetime import datetime | ||
from typing import ( | ||
TYPE_CHECKING, | ||
Any, | ||
AnyStr, | ||
Dict, | ||
Mapping, | ||
Optional, | ||
Pattern, | ||
Tuple, | ||
Type, | ||
Union, | ||
) | ||
|
||
if TYPE_CHECKING: | ||
from _pytest.python_api import ApproxBase | ||
|
||
|
||
# pylint: disable=invalid-name, too-few-public-methods | ||
|
||
|
||
class regex: | ||
"""Special class to eq by matching regex""" | ||
|
||
def __init__( | ||
self, | ||
pattern: Union[AnyStr, Pattern[AnyStr]], | ||
flags: Union[int, re.RegexFlag] = 0, | ||
) -> None: | ||
self._regex: Pattern[AnyStr] = re.compile( | ||
pattern, flags # type: ignore[arg-type] | ||
) | ||
|
||
def __repr__(self) -> str: | ||
flags = self._regex.flags & ~32 # 32 is default | ||
flags_repr = f", {flags}" if flags else "" | ||
return f"regex(r'{self._regex.pattern!s}'{flags_repr})" | ||
|
||
def __eq__(self, other: Any) -> bool: | ||
assert isinstance(other, (str, bytes)) | ||
return bool(self._regex.search(other)) # type: ignore | ||
|
||
|
||
class any: # pylint: disable=redefined-builtin | ||
"""Equals to anything. | ||
A way to ignore parts of data structures on comparison""" | ||
|
||
def __repr__(self) -> str: | ||
return "any" | ||
|
||
def __eq__(self, other: Any) -> bool: | ||
return True | ||
|
||
|
||
class dict: # pylint: disable=redefined-builtin | ||
"""Special class to eq by matching only presented dict keys""" | ||
|
||
def __init__( | ||
self, d: Optional[Mapping[Any, Any]] = None, **keys: Any | ||
) -> None: | ||
self.d: Dict[Any, Any] = {} | ||
if d: | ||
self.d.update(d) | ||
self.d.update(keys) | ||
|
||
def __repr__(self) -> str: | ||
inner = ", ".join(f"{k}={repr(v)}" for k, v in self.d.items()) | ||
return f"dict({inner})" | ||
|
||
def __eq__(self, other: object) -> bool: | ||
assert isinstance(other, collections.abc.Mapping) | ||
return all(other.get(name) == v for name, v in self.d.items()) | ||
|
||
|
||
class unordered: | ||
"""Compare list contents, but do not care about ordering. | ||
(E.g. sort lists first, then compare.) | ||
If you care about ordering, then just compare lists directly.""" | ||
|
||
def __init__(self, *items: Any) -> None: | ||
self.items = items | ||
|
||
def __repr__(self) -> str: | ||
inner = ", ".join(map(repr, self.items)) | ||
return f"unordered({inner})" | ||
|
||
def __eq__(self, other: object) -> bool: | ||
assert isinstance(other, collections.abc.Iterable) | ||
return sorted(self.items) == sorted(other) | ||
|
||
|
||
class attrs: | ||
def __init__(self, **attribs: Any) -> None: | ||
self.attribs = attribs | ||
|
||
def __repr__(self) -> str: | ||
inner = ", ".join(f"{k}={repr(v)}" for k, v in self.attribs.items()) | ||
return f"attrs({inner})" | ||
|
||
def __eq__(self, other: Any) -> bool: | ||
# Unforturnately this doesn't work with classes with slots | ||
# self.__class__ = other.__class__ | ||
return all( | ||
getattr(other, name) == v for name, v in self.attribs.items() | ||
) | ||
|
||
|
||
class any_of: | ||
def __init__(self, *items: Any) -> None: | ||
self.items = sorted(items) | ||
|
||
def __repr__(self) -> str: | ||
inner = ", ".join(map(repr, self.items)) | ||
return f"any_of({inner})" | ||
|
||
def __eq__(self, other: object) -> bool: | ||
return other in self.items | ||
|
||
|
||
class instance_of: | ||
def __init__( | ||
self, | ||
expected_type: Union[Type[object], Tuple[Type[object], ...]], | ||
) -> None: | ||
self.expected_type = expected_type | ||
|
||
def __repr__(self) -> str: | ||
if isinstance(self.expected_type, tuple): | ||
inner = f"({', '.join(t.__name__ for t in self.expected_type)})" | ||
else: | ||
inner = self.expected_type.__name__ | ||
return f"{self.__class__.__name__}({inner})" | ||
|
||
def __eq__(self, other: Any) -> bool: | ||
return isinstance(other, self.expected_type) | ||
|
||
|
||
def approx( # type: ignore[no-untyped-def] | ||
expected, | ||
rel=None, | ||
abs=None, # pylint: disable=redefined-builtin | ||
nan_ok: bool = False, | ||
) -> "ApproxBase": | ||
# pylint: disable=import-outside-toplevel | ||
|
||
if isinstance(expected, datetime): | ||
from ._approx import approx_datetime | ||
|
||
return approx_datetime(expected, abs=abs) | ||
|
||
import pytest | ||
|
||
return pytest.approx(expected, rel=rel, abs=abs, nan_ok=nan_ok) | ||
|
||
|
||
class Matcher(attrs): | ||
"""Special class to eq by existing attrs. | ||
The purpose is to simplify asserts containing objects, i.e.: | ||
assert ( | ||
result.errors == | ||
[M(message=M.re("^Something went wrong:"), extensions={"code": 523})] | ||
) | ||
Here all the structures like lists and dicts are followed as usual both | ||
outside and inside a mather object. These could be freely intermixed. | ||
""" | ||
|
||
any = any() | ||
|
||
@staticmethod | ||
def attrs(**attribs: Any) -> attrs: | ||
return attrs(**attribs) | ||
|
||
@staticmethod | ||
def regex( | ||
pattern: Union[AnyStr, Pattern[AnyStr]], | ||
flags: Union[int, re.RegexFlag] = 0, | ||
) -> regex: | ||
return regex(pattern, flags=flags) | ||
|
||
re = regex | ||
|
||
@staticmethod | ||
def dict(d: Optional[Mapping[Any, Any]] = None, **keys: Any) -> dict: | ||
return dict(d=d, **keys) | ||
|
||
@staticmethod | ||
def unordered(*items: Any) -> unordered: | ||
return unordered(*items) | ||
|
||
@staticmethod | ||
def any_of(*items: Any) -> any_of: | ||
return any_of(*items) | ||
|
||
@staticmethod | ||
def instance_of( | ||
expected_type: Union[Type[object], Tuple[Type[object], ...]] | ||
) -> instance_of: | ||
return instance_of(expected_type) | ||
|
||
@staticmethod | ||
def approx( # type: ignore[no-untyped-def] | ||
expected, | ||
rel=None, | ||
abs=None, # pylint: disable=redefined-builtin | ||
nan_ok: bool = False, | ||
) -> "ApproxBase": | ||
return approx(expected, rel=rel, abs=abs, nan_ok=nan_ok) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.