Source code for callee.collections

"""
Matchers for collections.
"""
from __future__ import absolute_import

import collections
import inspect

from callee._compat import OrderedDict as _OrderedDict
from callee.base import BaseMatcher
from callee.general import Any
from callee.types import InstanceOf


__all__ = [
    'Iterable', 'Generator',
    'Sequence', 'List', 'Set',
    'Mapping', 'Dict', 'OrderedDict',
]


class CollectionMatcher(BaseMatcher):
    """Base class for collections' matchers.
    This class shouldn't be used directly.
    """
    #: Collection class to match.
    #: Must be overridden in subclasses.
    CLASS = None

    def __init__(self, of=None):
        """
        :param of: Optional matcher for the elements,
                   or the expected type of the elements.
        """
        assert self.CLASS, "must specify collection type to match"
        self.of = self._validate_argument(of)

    def _validate_argument(self, arg):
        """Validate a type or matcher argument to the constructor."""
        if arg is None:
            return arg

        if isinstance(arg, type):
            return InstanceOf(arg)
        if not isinstance(arg, BaseMatcher):
            raise TypeError(
                "argument of %s can be a type or a matcher (got %r)" % (
                    self.__class__.__name__, type(arg)))

        return arg

    def match(self, value):
        if not isinstance(value, self.CLASS):
            return False
        if self.of is not None:
            return all(self.of == item for item in value)
        return True

    def __repr__(self):
        """Return a readable representation of the matcher.
        Used mostly for AssertionError messages in failed tests.

        Example::

            <List[<Integer>]>
        """
        of = "" if self.of is None else "[%r]" % (self.of,)
        return "<%s%s>" % (self.__class__.__name__, of)


[docs]class Iterable(CollectionMatcher): """Matches any iterable.""" CLASS = collections.Iterable def __init__(self): # Unfortunately, we can't allow an ``of`` argument to this matcher. # # An otherwise unspecified iterable can't be iterated upon # more than once safely, because it could be a one-off iterable # (e.g. generator comprehension) that's exhausted after a single pass. # # Thus the sole act of checking the element types would alter # the object we're trying to match, and potentially cause all sorts # of unexpected behaviors (e.g. tests passing/failing depending on # the order of assertions). #
super(Iterable, self).__init__(of=None)
[docs]class Generator(BaseMatcher): """Matches an iterable that's a generator. A generator can be a generator expression ("comprehension") or an invocation of a generator function (one that ``yield``\ s objects). .. note:: To match a *generator function* itself, you should use the :class:`~callee.functions.GeneratorFunction` matcher instead. """ def match(self, value): return inspect.isgenerator(value) def __repr__(self):
return "<Generator>" # Ordinary collections
[docs]class Sequence(CollectionMatcher): """Matches a sequence of given items. A sequence is an iterable that has a length and can be indexed. """
CLASS = collections.Sequence
[docs]class List(CollectionMatcher): """Matches a :class:`list` of given items."""
CLASS = list
[docs]class Set(CollectionMatcher): """Matches a :class:`set` of given items."""
CLASS = collections.Set # TODO: Tuple matcher, with of= that accepts a tuple of matchers # so that tuple elements can be also matched on # Mappings class MappingMatcher(CollectionMatcher): """Base class for mapping matchers. This class shouldn't be used directly. """ #: Mapping class to match. #: Must be overridden in subclasses. CLASS = None def __init__(self, *args, **kwargs): """Constructor can be invoked either with parameters described below (given as keyword arguments), or with two positional arguments: matchers/types for dictionary keys & values:: Dict(String(), int) # dict mapping strings to ints :param keys: Matcher for dictionary keys. :param values: Matcher for dictionary values. :param of: Matcher for dictionary items, or a tuple of matchers for keys & values, e.g. ``(String(), Integer())``. Cannot be provided if either ``keys`` or ``values`` is also passed. """ assert self.CLASS, "must specify mapping type to match" self._initialize(*args, **kwargs) def _initialize(self, *args, **kwargs): """Initiaize the mapping matcher with constructor arguments.""" self.items = None self.keys = None self.values = None if args: if len(args) != 2: raise TypeError("expected exactly two positional arguments, " "got %s" % len(args)) if kwargs: raise TypeError( "expected positional or keyword arguments, not both") # got positional arguments only self.keys, self.values = map(self._validate_argument, args) elif kwargs: has_kv = 'keys' in kwargs and 'values' in kwargs has_of = 'of' in kwargs if not (has_kv or has_of): raise TypeError("expected keys/values or items matchers, " "but got: %s" % list(kwargs.keys())) if has_kv and has_of: raise TypeError( "expected keys & values, or items matchers, not both") if has_kv: # got keys= and values= matchers self.keys = self._validate_argument(kwargs['keys']) self.values = self._validate_argument(kwargs['values']) else: # got of= matcher, which can be a tuple of matchers, # or a single matcher for dictionary items of = kwargs['of'] if isinstance(of, tuple): try: # got of= as tuple of matchers self.keys, self.values = \ map(self._validate_argument, of) except ValueError: raise TypeError( "of= tuple has to be a pair of matchers/types" % ( self.__class__.__name__,)) else: # got of= as a single matcher self.items = self._validate_argument(of) def match(self, value): if not isinstance(value, self.CLASS): return False if self.items is not None: return all(self.items == i for i in value.items()) if self.keys is not None and self.values is not None: return all(self.keys == k and self.values == v for k, v in value.items()) return True def __repr__(self): """Return a readable representation of the matcher Used mostly for AssertionError messages in failed tests. Example:: <Dict[<String> => <Any>]> """ of = "" if self.items is not None: of = "[%r]" % self.items if self.keys is not None or self.values is not None: keys = repr(Any() if self.keys is None else self.keys) values = repr(Any() if self.values is None else self.values) of = "[%s => %s]" % (keys, values) return "<%s%s>" % (self.__class__.__name__, of)
[docs]class Mapping(MappingMatcher): """Matches a mapping of given items."""
CLASS = collections.Mapping
[docs]class Dict(MappingMatcher): """Matches a dictionary (:class:`dict`) of given items."""
CLASS = dict
[docs]class OrderedDict(MappingMatcher): """Matches an ordered dictionary (:class:`collections.OrderedDict`) of given items. On Python 2.6, this requires the ordereddict backport package. Otherwise, no object will match this matcher. """ CLASS = _OrderedDict def __init__(self, *args, **kwargs): """For more information about arguments, see the documentation of :class:`Dict`. """ # Override the constructor from the base matcher class # without asserting that CLASS is not None, because it legimately will # be on Python 2.6 without the ordereddict package. self._initialize(*args, **kwargs) def match(self, value): if self.CLASS is None: return False
return super(OrderedDict, self).match(value)