Source code for callee.objects

"""
Matchers for various common kinds of objects.
"""
import inspect
import sys

from callee._compat import asyncio, getargspec
from callee.base import BaseMatcher


__all__ = ['Bytes', 'Coroutine', 'FileLike']


class ObjectMatcher(BaseMatcher):
    """Base class for object matchers.
    This class shouldn't be used directly.
    """
    def __repr__(self):
        return "<%s>" % (self.__class__.__name__,)


# TODO: Date, DateTime, and Time matchers (with before=/after= params)
# TODO: TimeDelta matcher


[docs]class Bytes(ObjectMatcher): """Matches a byte array, i.e. the :class:`bytes` type. | On Python 2, :class:`bytes` class is identical to :class:`str` class. | On Python 3, byte strings are separate class, distinct from :class:`str`. """ def match(self, value): return isinstance(value, bytes)
[docs]class Coroutine(ObjectMatcher): """Matches an asynchronous coroutine. A coroutine is a result of an asynchronous function call, where the async function has been defined using ``@asyncio.coroutine`` or the ``async def`` syntax. These are only available in Python 3.4 and above. On previous versions of Python, no object will match this matcher. """ def match(self, value): return asyncio and asyncio.iscoroutine(value)
[docs]class FileLike(ObjectMatcher): """Matches a file-like object. In general, a `file-like object` is an object you can ``read`` data from, or ``write`` data to. """ def __init__(self, read=True, write=None): """ :param read: Whether only to match objects that do support (``True``) or don't support (``False``) reading from them. If ``None`` is passed, reading capability is not matched against. :param write: Whether only to match objects that do support (``True``) or don't support (``False``) writing to them. If ``None`` is passed, writing capability is not matched against. """ if read is None and write is None: raise ValueError("cannot match file-like objects " "that are neither readable nor writable") self.read = read if read is None else bool(read) self.write = write if write is None else bool(write) def match(self, value): if self.read is not None: if self.read != self._is_readable(value): return False if self.write is not None: if self.write != self._is_writable(value): return False return True def _is_readable(self, obj): """Check if the argument is a readable file-like object.""" try: read = getattr(obj, 'read') except AttributeError: return False else: return is_method(read, max_arity=1) def _is_writable(self, obj): """Check if the argument is a writable file-like object.""" try: write = getattr(obj, 'write') except AttributeError: return False else: return is_method(write, min_arity=1, max_arity=1) def __repr__(self): """Return a representation of this matcher.""" requirements = [] if self.read is not None: requirements.append("read" if self.read else "noread") if self.write is not None: requirements.append("write" if self.write else "nowrite") return "<FileLike %s>" % "(%s)" % ",".join(requirements)
# Utility functions def is_method(arg, min_arity=None, max_arity=None): """Check if argument is a method. Optionally, we can also check if minimum or maximum arities (number of accepted arguments) match given minimum and/or maximum. """ if not callable(arg): return False if not any(is_(arg) for is_ in (inspect.ismethod, inspect.ismethoddescriptor, inspect.isbuiltin)): return False try: argnames, varargs, kwargs, defaults = getargspec(arg) except TypeError: # On CPython 2.x, built-in methods of file aren't inspectable, # so if it's file.read() or file.write(), we can't tell it for sure. # Given how this check is being used, assuming the best is probably # all we can do here. return True else: if argnames and argnames[0] == 'self': argnames = argnames[1:] if min_arity is not None: actual_min_arity = len(argnames) - len(defaults or ()) assert actual_min_arity >= 0, ( "Minimum arity of %r found to be negative (got %s)!" % ( arg, actual_min_arity)) if int(min_arity) != actual_min_arity: return False if max_arity is not None: actual_max_arity = sys.maxsize if varargs or kwargs else len(argnames) if int(max_arity) != actual_max_arity: return False return True