"""
General matchers.
These don't belong to any broader category, and include matchers for common
Python objects, like functions or classes.
"""
import inspect
from callee._compat import IS_PY3, STRING_TYPES
from callee.base import BaseMatcher
__all__ = [
'Any', 'Matching', 'ArgThat', 'Captor',
]
# TODO: introduce custom exception types rather than using built-ins
# TODO: matchers for positional & keyword arguments,
# e.g. *Args(Integer(), min=1, max=10), **Kwargs(foo=String(), bar=Float())
[docs]class Any(BaseMatcher):
"""Matches any object."""
def match(self, value):
return True
def __repr__(self):
return "<Any>"
[docs]class Matching(BaseMatcher):
"""Matches an object that satisfies given predicate."""
MAX_DESC_LENGTH = 32
def __init__(self, predicate, desc=None):
"""
:param predicate: Callable taking a single argument
and returning True or False
:param desc: Optional description of the predicate.
This will be displayed as a part of the error message
on failed assertion.
"""
if not callable(predicate):
raise TypeError(
"Matching requires a predicate, got %r" % (predicate,))
self.predicate = predicate
self.desc = self._validate_desc(desc)
def _validate_desc(self, desc):
"""Validate the predicate description."""
if desc is None:
return desc
if not isinstance(desc, STRING_TYPES):
raise TypeError(
"predicate description for Matching must be a string, "
"got %r" % (type(desc),))
# Python 2 mandates __repr__ to be an ASCII string,
# so if Unicode is passed (usually due to unicode_literals),
# it should be ASCII-encodable.
if not IS_PY3 and isinstance(desc, unicode):
try:
desc = desc.encode('ascii', errors='strict')
except UnicodeEncodeError:
raise TypeError("predicate description must be "
"an ASCII string in Python 2")
return desc
def match(self, value):
# Note that any possible exceptions from ``predicate``
# are intentionally let through, to make it easier to diagnose errors
# than a plain "no match" response would.
return bool(self.predicate(value))
# TODO: translate exceptions from the predicate into our own
# exception type to not clutter user-visible stracktraces with our code
def __repr__(self):
"""Return a representation of the matcher."""
name = getattr(self.predicate, '__name__', None)
desc = self.desc
# When no user-provided description is available,
# use function's own name or even its repr().
if desc is None:
# If not a lambda function, we can probably make the representation
# more readable by showing just the function's own name.
if name and name != '<lambda>':
# Where possible, make it a fully qualified name, including
# the module path. This is either on Python 3.3+
# (via __qualname__), or when the predicate is
# a standalone function (not a method).
qualname = getattr(self.predicate, '__qualname__', name)
is_method = inspect.ismethod(self.predicate) or \
isinstance(self.predicate, staticmethod)
if qualname != name or not is_method:
# Note that this shows inner functions (those defined
# locally inside other functions) as if they were global
# to the module.
# This is why we use colon (:) as separator here, as to not
# suggest this is an evaluatable identifier.
name = '%s:%s' % (self.predicate.__module__, qualname)
else:
# For lambdas and other callable objects,
# we'll just default to the Python repr().
name = None
else:
# Quote and possibly ellipsize the provided description.
if len(desc) > self.MAX_DESC_LENGTH:
ellipsis = '...'
desc = desc[:self.MAX_DESC_LENGTH - len(ellipsis)] + ellipsis
desc = '"%s"' % desc
return "<Matching %s>" % (desc or name or repr(self.predicate))
ArgThat = Matching
[docs]class Captor(BaseMatcher):
"""Argument captor.
You can use :class:`Captor` to "capture" the original argument
that the mock was called with, and perform custom assertions on it.
Example::
captor = Captor()
mock_foo.assert_called_with(captor)
# captured value is available as the `arg` attribute
self.assertEquals(captor.arg.some_method(), 42)
self.assertEquals(captor.arg.some_other_method(), "foo")
.. versionadded:: 0.2
"""
__slots__ = ('matcher', 'value')
def __init__(self, matcher=None):
"""
:param matcher: Optional matcher to validate the argument against
before it's captured
"""
if matcher is None:
matcher = Any()
if not isinstance(matcher, BaseMatcher):
raise TypeError("expected a matcher, got %r" % (type(matcher),))
if isinstance(matcher, Captor):
raise TypeError("cannot pass a captor to another captor")
self.matcher = matcher
def has_value(self):
"""Returns whether the :class:`Captor` has captured a value."""
return hasattr(self, 'value')
@property
def arg(self):
"""The captured argument value."""
if not self.has_value():
raise ValueError("no value captured")
return self.value
def match(self, value):
if self.has_value():
raise ValueError("a value has already been captured")
if not self.matcher.match(value):
return False
self.value = value
return True
def __repr__(self):
"""Return a representation of the captor."""
return "<Captor %r%s>" % (self.matcher,
" (*)" if self.has_value() else "")