import collections.abc
import operator
import re
import dateutil.parser
class Any:
"""Matches any value."""
def __repr__(self):
return '<Any>'
def __eq__(self, other):
return True
class AnyString:
"""Matches any string."""
def __repr__(self):
return '<AnyString>'
def __eq__(self, other):
return isinstance(other, str)
[docs]class RegexString(AnyString):
"""Match string with regular expression.
.. code-block:: python
assert response.json() == {
'order_id': matching.RegexString('^[0-9a-f]*$'),
...
}
"""
def __init__(self, pattern):
self._pattern = re.compile(pattern)
def __repr__(self):
return f'<{self.__class__.__name__} pattern={self._pattern!r}>'
def __eq__(self, other):
if not super().__eq__(other):
return False
return self._pattern.match(other) is not None
class UuidString(RegexString):
"""Matches lower-case hexadecimal uuid string."""
def __init__(self):
super().__init__('^[0-9a-f]{32}$')
class ObjectIdString(RegexString):
"""Matches lower-case hexadecimal objectid string."""
def __init__(self):
super().__init__('^[0-9a-f]{24}$')
class DatetimeString(AnyString):
"""Matches datetime string in any format."""
def __repr__(self):
return '<DatetimeString>'
def __eq__(self, other):
if not super().__eq__(other):
return False
try:
dateutil.parser.parse(other)
return True
except ValueError:
return False
[docs]class IsInstance:
"""Match value by its type.
Use this class when you only need to check value type.
.. code-block:: python
assert response.json() == {
# order_id must be a string
'order_id': matching.IsInstance(str),
# int or float is acceptable here
'weight': matching.IsInstance([int, float]),
...
}
"""
def __init__(self, types):
self.types = types
def __repr__(self):
if isinstance(self.types, (list, tuple)):
type_names = [t.__name__ for t in self.types]
else:
type_names = [self.types.__name__]
return f'<of-type {", ".join(type_names)}>'
def __eq__(self, other):
return isinstance(other, self.types)
[docs]class And:
"""Logical AND on conditions.
.. code-block:: python
# match integer is in range [10, 100)
assert num == matching.And([matching.Ge(10), matching.Lt(100)])
"""
def __init__(self, *conditions):
self.conditions = conditions
def __repr__(self):
conditions = [repr(cond) for cond in self.conditions]
return f'<And {" ".join(conditions)}>'
def __eq__(self, other):
for condition in self.conditions:
if condition != other:
return False
return True
[docs]class Or:
"""Logical OR on conditions.
.. code-block:: python
# match integers abs(num) >= 10
assert num == matching.Or([matching.Ge(10), matching.Le(-10)])
"""
def __init__(self, *conditions):
self.conditions = conditions
def __repr__(self):
conditions = [repr(cond) for cond in self.conditions]
return f'<Or {" ".join(conditions)}>'
def __eq__(self, other):
for condition in self.conditions:
if condition == other:
return True
return False
[docs]class Not:
"""Condition inversion.
Example:
.. code-block:: python
# check value is not 1
assert value == matching.Not(1)
"""
def __init__(self, condition):
self.condition = condition
def __repr__(self):
return f'<Not {self.condition!r}>'
def __eq__(self, other):
return self.condition != other
class Comparator:
op = operator.eq
def __init__(self, value):
self.value = value
def __repr__(self):
return f'<{self.op.__name__} {self.value}>'
def __eq__(self, other):
try:
return self.op(other, self.value)
except TypeError:
return False
[docs]class Gt(Comparator):
"""Value is greater than.
Example:
.. code-block:: python
# Value must be > 10
assert value == matching.Gt(10)
"""
op = operator.gt
[docs]class Ge(Comparator):
"""Value is greater or equal.
Example:
.. code-block:: python
# Value must be >= 10
assert value == matching.Ge(10)
"""
op = operator.ge
[docs]class Lt(Comparator):
"""Value is less than.
Example:
.. code-block:: python
# Value must be < 10
assert value == matching.Lt(10)
"""
op = operator.lt
[docs]class Le(Comparator):
"""Value is less or equal.
Example:
.. code-block:: python
# Value must be <= 10
assert value == matching.Le(10)
"""
op = operator.le
[docs]class PartialDict(collections.abc.Mapping):
"""Partial dictionary comparison.
Sometimes you only need to check dictionary subset ignoring all
other keys. :py:class:`PartialDict` is there for this purpose.
`PartialDict` is wrapper around regular `dict()` when instantiated
all arguments are passed as is to internal dict object.
Example:
.. code-block:: python
assert {'foo': 1, 'bar': 2} == matching.PartialDict({
# Only check for foo >= 1 ignoring other keys
'foo': matching.Ge(1),
})
"""
def __init__(self, *args, **kwargs):
self._dict = dict(*args, **kwargs)
def __contains__(self, item):
return True
def __getitem__(self, item):
return self._dict.get(item, any_value)
def __iter__(self):
return iter(self._dict)
def __len__(self):
return len(self._dict)
def __repr__(self):
return f'<PartialDict {self._dict!r}>'
def __eq__(self, other):
if not isinstance(other, collections.abc.Mapping):
return False
for key in self:
if other.get(key) != self.get(key):
return False
return True
class UnorderedList:
def __init__(self, sequence, key):
self.value = sorted(sequence, key=key)
self.key = key
def __repr__(self):
return f'<UnorderedList: {self.value}>'
def __eq__(self, other):
return sorted(other, key=self.key) == self.value
[docs]def unordered_list(sequence, *, key=None):
"""Unordered list comparison.
You may want to compare lists without respect to order. For instance,
when your service is serializing std::unordered_map to array.
`unordered_list` can help you with that. It sorts both array before
comparison.
:param sequence: Initial sequence
:param key: Sorting key function
Example:
.. code-block:: python
assert [3, 2, 1] == matching.unordered_list([1, 2, 3])
"""
return UnorderedList(sequence, key)
any_value = Any()
any_float = IsInstance(float)
any_integer = IsInstance(int)
any_numeric = IsInstance((int, float))
positive_float = And(any_float, Gt(0))
positive_integer = And(any_integer, Gt(0))
positive_numeric = And(any_numeric, Gt(0))
negative_float = And(any_float, Lt(0))
negative_integer = And(any_integer, Lt(0))
negative_numeric = And(any_numeric, Lt(0))
non_negative_float = And(any_float, Ge(0))
non_negative_integer = And(any_integer, Ge(0))
non_negative_numeric = And(any_numeric, Ge(0))
any_string = AnyString()
datetime_string = DatetimeString()
objectid_string = ObjectIdString()
uuid_string = UuidString()