Fork me on GitHub

Source code for predicates

from operator import (
    # comparisons
    lt, le, eq, ne, ge, gt,

    # booleans
    not_, truth,

    # object identity
    is_, is_not,
    )

from collections import (
    Callable,
    Container,
    Hashable,
    Iterable,
    Iterator,
    Mapping,
    MutableMapping,
    MappingView,
    ItemsView,
    KeysView,
    ValuesView,
    Sequence,
    MutableSequence,
    Set,
    MutableSet,
    Sized,
    )


# Predicate composition
# ---------------------

[docs]def _and (*predicates): """ Returns a `callable` which returns `True` if *all* ``predicates`` are true. This *is* short-circuiting. """ def _and (*args, **kwargs): return all(pred(*args, **kwargs) for pred in predicates) return _and
[docs]def _or (*predicates): """ Returns a `callable` which returns `True` if *any* ``predicates`` are true. This *is* short-circuiting. """ def _or (*args, **kwargs): return any(pred(*args, **kwargs) for pred in predicates) return _or
[docs]def _not (*predicates): """ Returns a `callable` which returns `True` if *none* of the ``predicates`` are true. """ def _not (*args, **kwargs): return not any(pred(*args, **kwargs) for pred in predicates) return _not
[docs]def _zip (*predicates): """ Returns a `callable` which returns `True` if each application of a predicate to its corresponding argument returns `True`. I.e., it applies ``predicates[0]`` to ``args[0]``, ``predicates[1]`` to ``args[1]``, and so on. Technically, this lives in the grey area between :ref:`predicate_composition` and :ref:`predicate_application`. It inherits the truncation behaviour of :func:`zip` (i.e., it truncates to the shorter of `predicates` or `args`). The callable ignores keyword arguments, if present. **TODO:** What's the correct mathematical term for this? The `cross-product` would be *all predicates applied to all arguments* (i.e., `n x m`). While we're on the subject, should we add a cross-product factory? """ def _zip (*args, **kwargs): return all(predicate(arg) for (predicate, arg) in zip(predicates, args)) return _zip # Predicate application # ---------------------
[docs]def _all (predicate): """ Returns a `callable` which returns `True` if ``predicate`` returns `True` for *all* of its *positional* arguments. """ def _all (*args, **kwargs): return all(predicate(arg) for arg in args) return _all
[docs]def _any (predicate): """ Returns a `callable` which returns `True` if ``predicate`` returns `True` for *any* of its *positional* arguments. """ def _any (*args, **kwargs): return any(predicate(arg) for arg in args) return _any
[docs]def _none (predicate): """ Returns a `callable` which returns `True` if ``predicate`` returns `True` for *none* of its *positional* arguments. """ def _none (*args, **kwargs): return all(not predicate(arg) for arg in args) return _none # Argument predicates # -------------------
[docs]class ArgSlicer (object): """ Flexible `predicate factory` to convert :meth:`~object.__getitem__` slices and direct calls (i.e., :meth:`~object.__call__`) into `predicate partials` which apply a set of predicates to a subset of the callables' arguments. All access is through its (unenforced) singleton instance, :func:`_args`, but we expose the class for subclassing, monkeypatching, etc. Examples are the best way to explain this beast. * :meth:`~object.__getitem__` access to positional arguments. Ensure that first positional arg (`args[0]`), if present, is a string. Imposes no constraints on other posiitional or keyword args. .. code-block:: python >>> fn = _args[0](isstring) >>> fn() True >>> fn('jack') True >>> fn(4) False >>> fn('jack', 8, kate=15) True Ensure that the first two positional args (`args[0:2]`), if present, are strings. Imposes no constraints on other positional or keyword args. .. code-block:: python >>> fn = _args[0:2](isstring) >>> fn() True >>> fn('jack') True >>> fn('jack', 8) False >>> fn('jack', 'sawyer', 15, kate=15) True Ensure that any positional args are strings. Imposes no constraints on keyword args. .. code-block:: python >>> fn = _args[:](isstring) >>> fn() True >>> fn('jack') True >>> fn('jack', 8) False >>> fn('jack', 'sawyer', kate=15) * :meth:`~object.__call__` access to positional arguments: Ensure all positional arguments are strings. Imposes no constraints on keyword args. This is equivalent to (and, in fact, is implemented as) the ``_args[:](isstring)`` example, above. .. code-block:: python >>> fn = _args(isstring) >>> fn() True >>> fn('jack') True >>> fn('jack', 8) False >>> fn('jack', 'sawyer', kate=15) * :meth:`~object.__call__` access to keyword arguments: Ensure that keyword arguments ``jack`` and ``kate`` *exist*, and that `jack` is a string, and `kate` is an integer. Imposes no constraints on positional or other keyword args. .. code-block:: python >>> fn = _args(jack=isstring, kate=isint) >>> fn() False >>> fn(jack='', kate=15) True >>> fn(jack='') False >>> fn(jack=4) False >>> fn(4, 8, jack='', kate=15) True * :meth:`~object.__call__` access to positional and keyword arguments: Ensure that keyword arguments ``jack`` and ``kate`` *exist*, that `jack` is a string, that `kate` is an integer, and that any positional arguments are strings. Imposes no constraints on other keyword args. .. code-block:: python >>> fn = _args(isstring, jack=isstring, kate=isint) >>> fn() False >>> fn(jack='', kate=15) True >>> fn("bad robot!", jack='', kate=15) True >>> fn("bad robot!", jack='') False >>> fn(jack=4) False >>> fn(4, 8, jack='', kate=15) False * Mixed :meth:`~object.__getitem__` and :meth:`~object.__call__` access to positional and keyword arguments: Ensure that keyword arguments ``jack`` and ``kate`` *exist*, that `jack` is a string, that `kate` is an integer, and that the first two positional arguments (`args[0:2]`), if present, are strings. Imposes no constraints on other positional or keyword arguments. .. code-block:: python >>> fn = _args[0:2](isstring, jack=isstring, kate=isint) >>> fn() False >>> fn(jack='', kate=15) True >>> fn("bad robot!", jack='', kate=15) True >>> fn("bad robot!", 'sawyer', jack='', kate=15) True >>> fn("bad robot!", 'sawyer', 23, jack='', kate=15) True >>> fn(4, "bad robot!", jack='', kate=15) False >>> fn("bad robot!", 'sawyer', 23, jack=4, kate='15') False .. note:: **TODO:** use multi-dimensional extended slice syntax to apply predicates to specific args. E.g., .. code-block:: python >>> fn = _args[0, 1, 3:5](isstring, isint, isfloat, hurley=isint) >>> fn("bad robot!", 4, (), 8.0, 15.0, hurley=16) True Another option is to put the keyword args into the slice: .. code-block:: python >>> fn = _args[0, 1, 3:5, 'hurley']( ... isstring, isint, isfloat, isint) >>> fn("bad robot!", 4, (), 8.0, 15.0, hurley=16) True ...or is it starting to get ridiculous? I don't like the increasing distance between the arg selector (the `key` to :meth:`~object.__getitem__`) and the corresponding predicate. Maybe we should move to the simpler ``_args(p0, p1, p2, key1=p3, key2=p4)`` form? Combining :func:`_all` and :func:`_apply` would let us duplicate the behaviour of the current ``_args(predicate)``, but we'd lose the ability to (easily) apply separate predicates to *ranges* of args, including *overlapping* ranges, like we get with the multidimensional slice. E.g., .. code-block:: python >>> fn = _args[0:4, 3:5](isstring, _not(isempty)) >>> fn('', '', "bad robot!") True >>> fn('', "bad robot!", '', 'jack', (42,)) True >>> fn('', "bad robot!", '', 'jack', ()) False Ultimately, what I'm looking for is a "single point of truth" for specifying, concisely, constraints on all of the arguments I care about. """ def __call__ (self, pos_predicate=None, **kwargs): """ Wrapper around :meth:`__getitem__` to shortcut the 'apply to all arguments' (`[:]`) case and 'apply to keyword argument(s)' (`['x', 'y', 'z']) cases. """ return self[:](pos_predicate, **kwargs) def __getitem__ (self, key): """ Returns a `predicate factory` which will create a `callable` which returns `True` if its ``predicate`` returns `True` for each element of the slice of its args using this function's `key` argument. I.e., ``args[key]``. **NOTE:** keys must (currently) be integers or :func:`slice` objects. """ # replace each non-slice element of `key` with an equivalent # slice, forcing the result to be a tuple (we need `args[0]` # to return `(x,)`, not `x`, so we can use it as `*args[0]` ). key = (key if isslice(key) else slice(key, key+1) if key >= 0 else slice(key, None)) def _args_factory (pos_predicate=None, **kw_predicates): """ Returns a `callable` which returns `True` if `pos_predicate` returns `True` for each positional arg in the slice ``args[key]`` (where `key` is the value originally passed to ``_args[...]``), and, for each `(kw, predicate)` pair in `kwargs`, applying the predicate to ``kwargs[kw]`` returns `True`. """ if not (pos_predicate or kw_predicates): raise ValueError( "must specify a predicate for positional args, " + "a set of predicates for keyword args, or both.") # keyword predicates only if not pos_predicate: def _args (*args, **kwargs): return all(predicate(kwargs.get(kw, None)) for (kw, predicate) in kw_predicates.items()) return _args # get a predicate to map the predicate to an iterable pos_predicate = _all(pos_predicate) # positional predicate only if not kw_predicates: def _args (*args, **kwargs): return pos_predicate(*args[key]) return _args # positional *and* keyword predicates def _args (*args, **kwargs): return (pos_predicate(*args[key]) and all(predicate(kwargs.get(kw, None)) for (kw, predicate) in kw_predicates.items())) return _args return _args_factory
_args = ArgSlicer()
[docs]def _nargs (atleast=False, atmost=False, exactly=False): """ Returns a `callable` which returns `True` if it is called with `at least`, `at most`, or `exactly` the number of positional *and* keyword arguments specified in `atleast`, `atmost`, and `exactly`, respectively. See :func:`_npos` and :func:`_nkw` for separate constraints on positional and keyword args, respectively. `atleast` and `atmost` may be combined, but `exactly` must stand alone. """ def length (*args, **kwargs): return len(args) + len(kwargs) return _fnis(length, atleast, atmost, exactly)
[docs]def _npos (atleast=False, atmost=False, exactly=False): """ Returns a `callable` which returns `True` if it is called with `at least`, `at most`, or `exactly` the number of positional arguments specified in `atleast`, `atmost`, and `exactly`, respectively. See :func:`_nkw` and :func:`_nargs`. `atleast` and `atmost` may be combined, but `exactly` must stand alone. """ def length (*args, **kwargs): return len(args) return _fnis(length, atleast, atmost, exactly)
[docs]def _nkw (atleast=False, atmost=False, exactly=False): """ Returns a `callable` which returns `True` if it is called with `at least`, `at most`, or `exactly` the number of keyword arguments specified in `atleast`, `atmost`, and `exactly`, respectively. See :func:`_npos` and :func:`_nargs`. `atleast` and `atmost` may be combined, but `exactly` must stand alone. """ def length (*args, **kwargs): return len(kwargs) return _fnis(length, atleast, atmost, exactly)
[docs]def _inkw (atleast=False, atmost=False, exactly=False): """ Returns a `callable` which returns `True` if it has `at least`, `at most`, or `exactly`, the keyword arguments specified. This constrains the *argument names*, while :func:`_nkw` constrains the *number* of arguments. Note, too, that this does *not* constrain the keyword arguments' *values*. `atleast` and `atmost` may be combined, but `exactly` must stand alone. **TODO:** add a new predicate, or extend this one, to validate keyword argument values (which means we'll also need an equivalent predicate for positional args, and one for mixed positional and keyword args.) """ if not exactly is False: if not ((atleast is False) and (atmost is False)): raise ValueError( "cannot mix 'exactly' and 'atleast' or 'atmost'") def _inkw (*args, **kwargs): return (len(kwargs) == len(exactly) and all(kw in kwargs for kw in exactly)) return _inkw if atleast is False and atmost is False: raise ValueError( "must specify 'exactly' or one or both of 'atleast' and 'atmost'") if atleast is False: atleast = () atleast = frozenset(atleast) if atmost is False: def _inkw (*args, **kwargs): keys = frozenset(kwargs.keys()) return keys >= atleast return _inkw atmost = frozenset(atmost) def _inkw (*args, **kwargs): keys = frozenset(kwargs.keys()) return atleast <= keys <= atmost return _inkw # Value predicates # ----------------
[docs]def isempty (val): """ `True` if ``val`` is empty. *Empty*, here, means zero-length, not 'false-y'. I.e., :data:`False` and ``0`` are *not* empty, even though they are `false` in a boolean context. Use :func:`~operator.truth` and :func:`~operator.not_` for 'standard' truth testing. """ return issized(val) and len(val) == 0
[docs]def _contains (*contents): """ Returns a `callable` which returns `True` if *each* member of ``contents`` is a member of its ``container`` argument. The signature of the returned callable is: .. function:: fn (container:Container) -> bool """ # :func:`all` would return `True` with an empty `contents` if len(contents) == 0: return true_ # no reason to call :func:`all` if we're only testing one value if len(contents) == 1: el = contents[0] def _contains (container): return el in container return _contains # otherwise, we need to check each element. def _contains (container): return all(el in container for el in contents) return _contains # Type predicates # ---------------
[docs]def _isa (classinfo, docstring=None): """ A wrapper around :func:`isinstance` to swap the argument ordering, so it can be used as a partial. The signature of the returned `callable` is: .. function:: fn (obj) -> bool If `docstring` is supplied, it will become the docstring of the new `callable`. If `docstring` is :data:`None`, a docstring will be created based on `classinfo`. """ def _isa (obj): return isinstance(obj, classinfo) # Make the docstring reflect what the new method does if docstring is None: name = getattr(classinfo, '__name__', None) if name: name = ":class:`%s`" % name else: if isiterable(classinfo): name = ('(' + ", ".join(getattr(t, '__name__', str(t)) for t in classinfo) + ')') else: name = str(classinfo) docstring = "`True` if `obj` is an instance of %s" % name _isa.__doc__ = docstring return _isa
doctmpl = "`True` if `obj` %s." iscallable = _isa(Callable, doctmpl % 'is `callable`') iscontainer = _isa(Container, doctmpl % 'is a `container`') ishashable = _isa(Hashable, doctmpl % 'is `hashable`') isiterable = _isa(Iterable, doctmpl % 'is `iterable`') isiterator = _isa(Iterator, doctmpl % 'is an `iterator`') ismap = _isa(Mapping, doctmpl % 'is a `mapping`') ismmap = _isa(MutableMapping, doctmpl % 'is a mutable `mapping`') ismapv = _isa(MappingView, doctmpl % 'is a `mapping view`') isitemsv = _isa(ItemsView, doctmpl % 'is an `items view`') iskeysv = _isa(KeysView, doctmpl % 'is a `keys view`') isvalsv = _isa(ValuesView, doctmpl % 'is a `values view`') isseq = _isa(Sequence, doctmpl % 'is a `sequence`') ismseq = _isa(MutableSequence, doctmpl % 'is a `mutable sequence`') isset = _isa(Set, doctmpl % 'is a `set`') ismset = _isa(MutableSet, doctmpl % 'is a `mutable set`') issized = _isa(Sized, doctmpl % 'has a :meth:`~object.__len__` method') isslice = _isa(slice, doctmpl % 'is a :func:`slice`') islist = _isa(list, doctmpl % 'is a :func:`list`') istuple = _isa(tuple, doctmpl % 'is a :func:`tuple`') isstring = _isa(basestring, doctmpl % 'is a :func:`string <basestring>`') isstr = _isa(str, doctmpl % 'is an :func:`str`') isunicode = _isa(unicode, doctmpl % 'is a :func:`unicode string <unicode>`') isbool = _isa(bool, doctmpl % 'is a :func:`bool`') isint = _isa(int, doctmpl % 'is an :func:`int`') islong = _isa(long, doctmpl % 'is a :func:`long`') isfloat = _isa(float, doctmpl % 'is a :func:`float`')
[docs]def isnsiterable (obj): """`True` if `obj` is a non-string `iterable`""" return isiterable(obj) and not isstring(obj)
[docs]def isatom (val): """ `True` if ``val`` looks 'atomic' (i.e., is a string, or any non-iterable). This is a naive test: any non-string iterable yields :data:`False`. """ return not isnsiterable(val) # Identity predicates # -------------------
[docs]def _is (it, docstring=None): """ A wrapper around :func:`~operator.is_` to set the docstring (which :func:`~functools.partial` does not). """ def _is (obj): return obj is it # Make the docstring reflect what the new method does _is.__doc__ = (docstring or ("`True` if `obj` is %s" % getattr(it, '__name__', it))) return _is
isnone = _is(None, doctmpl % '*is* :data:`None`') istrue = _is(True, doctmpl % '*is* :data:`True`') isfalse = _is(False, doctmpl % '*is* :data:`False`') # Helpers # -------
[docs]def _apply (func): """ Returns a `callable` which expands its `args` and `kwargs` into the `*args` and `**kwargs` of `func`. It's equivalent to ``partial(apply, func)``, except that :func:`apply` is deprecated. The signature of the returned callable is: .. function:: fn(args=(), kwargs={}) -> object Its principal use is to make predicates which operate on all of their arguments (i.e., `*args`) operate on *any* iterable. E.g., ``_apply(_all(isstring))(['jack', 'kate'])`` is equivalent to ``_all(isstring)('jack', 'hurley')``. This is especially useful when testing the *contents* of arguments passed to a :func:`_zip` callable. E.g., .. code-block:: python >>> int_and_strings = _zip(isint, _apply(_all(isstring))) >>> int_and_strings(42, ['jack', 'kate', 'sawyer']) True """ def _apply (args=(), kwargs={}): return func(*args, **kwargs) return _apply
__cache_return = {}
[docs]def _return (val): """ Always returns `val`. WHY?!? Because, sometimes you need a `callable` that's just a closure over ``return val``. E.g., in the 'no contents' special-case in :func:`_contains`. **NOTE:** This is one of the few memoized factories, because we don't want a proliferation of `_return(True)` and `_return(False)` helpers (of course, that's why we have :func:`true_` and :func:`false_`, but no matter). """ if ishashable(val): if val not in __cache_return: def _return (*args, **kwargs): return val __cache_return[val] = _return return __cache_return[val] def _return (*args, **kwargs): return val return _return
true_ = _return(True) false_ = _return(False)
[docs]def _nis (atleast=False, atmost=False, exactly=False): """ Returns a `callable` which returns `True` if ``n`` is ``>=`` `atleast`, ``<=`` `atmost`, or ``==`` `exactly`. See :func:`_nargs`, :func:`_nkw`, etc., for example use. The signature of the returned `callable` is: .. function:: fn (n:number) -> bool `atleast` and `atmost` may be combined, but `exactly` must stand alone. """ if (atleast < 0) or (atmost < 0) or (exactly < 0): raise ValueError("arg limits cannot be negative") if not exactly is False: if not ((atleast is False) and (atmost is False)): raise ValueError( "cannot mix 'exactly' and 'atleast' or 'atmost'") def _nis_exactly (n): return n == exactly return _nis_exactly if atleast is False and atmost is False: raise ValueError( "must specify 'exactly' or one or both of 'atleast' and 'atmost'") if atleast is False: atleast = 0 if atmost is False: atmost = float('inf') def _nis_between (n): return (atleast <= n <= atmost) return _nis_between
[docs]def _fnis (func, atleast=False, atmost=False, exactly=False): """ Returns a `callable` which returns `True` if the result of ``func(*args, **kwargs)`` is ``>=`` `atleast` ``<=`` `atmost`, or ``==`` `exactly`. See :func:`_nargs`, :func:`_nkw`, etc., for example use. `atleast` and `atmost` may be combined, but `exactly` must stand alone. """ __nis = _nis(atleast, atmost, exactly) def _fnis (*args, **kwargs): return __nis(func(*args, **kwargs)) return _fnis

This Page