|
|
- import inspect
- import sys
- from functools import partial
- from operator import attrgetter
- from textwrap import dedent
- from cytoolz.utils import no_default
- from cytoolz.compatibility import PY3, PY33, PY34, filter as ifilter, map as imap, reduce, import_module
- import cytoolz._signatures as _sigs
-
- from toolz.functoolz import (InstanceProperty, instanceproperty, is_arity,
- num_required_args, has_varargs, has_keywords,
- is_valid_args, is_partial_args)
-
- cimport cython
- from cpython.dict cimport PyDict_Merge, PyDict_New
- from cpython.object cimport (PyCallable_Check, PyObject_Call, PyObject_CallObject,
- PyObject_RichCompare, Py_EQ, Py_NE)
- from cpython.ref cimport PyObject
- from cpython.sequence cimport PySequence_Concat
- from cpython.set cimport PyFrozenSet_New
- from cpython.tuple cimport PyTuple_Check, PyTuple_GET_SIZE
-
-
- __all__ = ['identity', 'thread_first', 'thread_last', 'memoize', 'compose',
- 'pipe', 'complement', 'juxt', 'do', 'curry', 'memoize', 'flip',
- 'excepts']
-
-
- cpdef object identity(object x):
- return x
-
-
- cdef object c_thread_first(object val, object forms):
- cdef object form, func
- cdef tuple args
- for form in forms:
- if PyCallable_Check(form):
- val = form(val)
- elif PyTuple_Check(form):
- func, args = form[0], (val,) + form[1:]
- val = PyObject_CallObject(func, args)
- else:
- val = None
- return val
-
-
- def thread_first(val, *forms):
- """
- Thread value through a sequence of functions/forms
-
- >>> def double(x): return 2*x
- >>> def inc(x): return x + 1
- >>> thread_first(1, inc, double)
- 4
-
- If the function expects more than one input you can specify those inputs
- in a tuple. The value is used as the first input.
-
- >>> def add(x, y): return x + y
- >>> def pow(x, y): return x**y
- >>> thread_first(1, (add, 4), (pow, 2)) # pow(add(1, 4), 2)
- 25
-
- So in general
- thread_first(x, f, (g, y, z))
- expands to
- g(f(x), y, z)
-
- See Also:
- thread_last
- """
- return c_thread_first(val, forms)
-
-
- cdef object c_thread_last(object val, object forms):
- cdef object form, func
- cdef tuple args
- for form in forms:
- if PyCallable_Check(form):
- val = form(val)
- elif PyTuple_Check(form):
- func, args = form[0], form[1:] + (val,)
- val = PyObject_CallObject(func, args)
- else:
- val = None
- return val
-
-
- def thread_last(val, *forms):
- """
- Thread value through a sequence of functions/forms
-
- >>> def double(x): return 2*x
- >>> def inc(x): return x + 1
- >>> thread_last(1, inc, double)
- 4
-
- If the function expects more than one input you can specify those inputs
- in a tuple. The value is used as the last input.
-
- >>> def add(x, y): return x + y
- >>> def pow(x, y): return x**y
- >>> thread_last(1, (add, 4), (pow, 2)) # pow(2, add(4, 1))
- 32
-
- So in general
- thread_last(x, f, (g, y, z))
- expands to
- g(y, z, f(x))
-
- >>> def iseven(x):
- ... return x % 2 == 0
- >>> list(thread_last([1, 2, 3], (map, inc), (filter, iseven)))
- [2, 4]
-
- See Also:
- thread_first
- """
- return c_thread_last(val, forms)
-
-
- cdef struct partialobject:
- PyObject _
- PyObject *fn
- PyObject *args
- PyObject *kw
- PyObject *dict
- PyObject *weakreflist
-
-
- cdef object _partial = partial(lambda: None)
-
-
- cdef object _empty_kwargs():
- if <object> (<partialobject*> _partial).kw is None:
- return None
- return PyDict_New()
-
-
- cdef class curry:
- """ curry(self, *args, **kwargs)
-
- Curry a callable function
-
- Enables partial application of arguments through calling a function with an
- incomplete set of arguments.
-
- >>> def mul(x, y):
- ... return x * y
- >>> mul = curry(mul)
-
- >>> double = mul(2)
- >>> double(10)
- 20
-
- Also supports keyword arguments
-
- >>> @curry # Can use curry as a decorator
- ... def f(x, y, a=10):
- ... return a * (x + y)
-
- >>> add = f(a=1)
- >>> add(2, 3)
- 5
-
- See Also:
- cytoolz.curried - namespace of curried functions
- https://toolz.readthedocs.io/en/latest/curry.html
- """
-
- def __cinit__(self, *args, **kwargs):
- if not args:
- raise TypeError('__init__() takes at least 2 arguments (1 given)')
- func, args = args[0], args[1:]
- if not PyCallable_Check(func):
- raise TypeError("Input must be callable")
-
- # curry- or functools.partial-like object? Unpack and merge arguments
- if (hasattr(func, 'func')
- and hasattr(func, 'args')
- and hasattr(func, 'keywords')
- and isinstance(func.args, tuple)):
- if func.keywords:
- PyDict_Merge(kwargs, func.keywords, False)
- ## Equivalent to:
- # for key, val in func.keywords.items():
- # if key not in kwargs:
- # kwargs[key] = val
- args = func.args + args
- func = func.func
-
- self.func = func
- self.args = args
- self.keywords = kwargs if kwargs else _empty_kwargs()
- self.__doc__ = getattr(func, '__doc__', None)
- self.__name__ = getattr(func, '__name__', '<curry>')
- self.__module__ = getattr(func, '__module__', None)
- self.__qualname__ = getattr(func, '__qualname__', None)
- self._sigspec = None
- self._has_unknown_args = None
-
- def __str__(self):
- return str(self.func)
-
- def __repr__(self):
- return repr(self.func)
-
- def __hash__(self):
- return hash((self.func, self.args,
- frozenset(self.keywords.items()) if self.keywords
- else None))
-
- def __richcmp__(self, other, int op):
- is_equal = (isinstance(other, curry) and self.func == other.func and
- self.args == other.args and self.keywords == other.keywords)
- if op == Py_EQ:
- return is_equal
- if op == Py_NE:
- return not is_equal
- return PyObject_RichCompare(id(self), id(other), op)
-
- def __call__(self, *args, **kwargs):
- cdef object val
-
- if PyTuple_GET_SIZE(args) == 0:
- args = self.args
- elif PyTuple_GET_SIZE(self.args) != 0:
- args = PySequence_Concat(self.args, args)
- if self.keywords is not None:
- PyDict_Merge(kwargs, self.keywords, False)
- try:
- return self.func(*args, **kwargs)
- except TypeError as val:
- if self._should_curry_internal(args, kwargs, val):
- return type(self)(self.func, *args, **kwargs)
- raise
-
- def _should_curry(self, args, kwargs, exc=None):
- if PyTuple_GET_SIZE(args) == 0:
- args = self.args
- elif PyTuple_GET_SIZE(self.args) != 0:
- args = PySequence_Concat(self.args, args)
- if self.keywords is not None:
- PyDict_Merge(kwargs, self.keywords, False)
- return self._should_curry_internal(args, kwargs)
-
- def _should_curry_internal(self, args, kwargs, exc=None):
- func = self.func
-
- # `toolz` has these three lines
- #args = self.args + args
- #if self.keywords:
- # kwargs = dict(self.keywords, **kwargs)
-
- if self._sigspec is None:
- sigspec = self._sigspec = _sigs.signature_or_spec(func)
- self._has_unknown_args = has_varargs(func, sigspec) is not False
- else:
- sigspec = self._sigspec
-
- if is_partial_args(func, args, kwargs, sigspec) is False:
- # Nothing can make the call valid
- return False
- elif self._has_unknown_args:
- # The call may be valid and raised a TypeError, but we curry
- # anyway because the function may have `*args`. This is useful
- # for decorators with signature `func(*args, **kwargs)`.
- return True
- elif not is_valid_args(func, args, kwargs, sigspec):
- # Adding more arguments may make the call valid
- return True
- else:
- # There was a genuine TypeError
- return False
-
- def bind(self, *args, **kwargs):
- return type(self)(self, *args, **kwargs)
-
- def call(self, *args, **kwargs):
- cdef object val
-
- if PyTuple_GET_SIZE(args) == 0:
- args = self.args
- elif PyTuple_GET_SIZE(self.args) != 0:
- args = PySequence_Concat(self.args, args)
- if self.keywords is not None:
- PyDict_Merge(kwargs, self.keywords, False)
- return self.func(*args, **kwargs)
-
- def __get__(self, instance, owner):
- if instance is None:
- return self
- return type(self)(self, instance)
-
- property __signature__:
- def __get__(self):
- try:
- sig = inspect.signature(self.func)
- except TypeError:
- if PY33 and (getattr(self.func, '__module__') or '').startswith('cytoolz.'):
- raise ValueError('callable %r is not supported by signature' % self.func)
- raise
-
- args = self.args or ()
- keywords = self.keywords or {}
- if is_partial_args(self.func, args, keywords, sig) is False:
- raise TypeError('curry object has incorrect arguments')
-
- params = list(sig.parameters.values())
- skip = 0
- for param in params[:len(args)]:
- if param.kind == param.VAR_POSITIONAL:
- break
- skip += 1
-
- kwonly = False
- newparams = []
- for param in params[skip:]:
- kind = param.kind
- default = param.default
- if kind == param.VAR_KEYWORD:
- pass
- elif kind == param.VAR_POSITIONAL:
- if kwonly:
- continue
- elif param.name in keywords:
- default = keywords[param.name]
- kind = param.KEYWORD_ONLY
- kwonly = True
- else:
- if kwonly:
- kind = param.KEYWORD_ONLY
- if default is param.empty:
- default = no_default
- newparams.append(param.replace(default=default, kind=kind))
-
- return sig.replace(parameters=newparams)
-
- def __reduce__(self):
- func = self.func
- modname = getattr(func, '__module__', None)
- qualname = getattr(func, '__qualname__', None)
- if qualname is None:
- qualname = getattr(func, '__name__', None)
- is_decorated = None
- if modname and qualname:
- attrs = []
- obj = import_module(modname)
- for attr in qualname.split('.'):
- if isinstance(obj, curry):
- attrs.append('func')
- obj = obj.func
- obj = getattr(obj, attr, None)
- if obj is None:
- break
- attrs.append(attr)
- if isinstance(obj, curry) and obj.func is func:
- is_decorated = obj is self
- qualname = '.'.join(attrs)
- func = '%s:%s' % (modname, qualname)
-
- state = (type(self), func, self.args, self.keywords, is_decorated)
- return (_restore_curry, state)
-
-
- cpdef object _restore_curry(cls, func, args, kwargs, is_decorated):
- if isinstance(func, str):
- modname, qualname = func.rsplit(':', 1)
- obj = import_module(modname)
- for attr in qualname.split('.'):
- obj = getattr(obj, attr)
- if is_decorated:
- return obj
- func = obj.func
- obj = cls(func, *args, **(kwargs or {}))
- return obj
-
-
- cpdef object memoize(object func, object cache=None, object key=None):
- """
- Cache a function's result for speedy future evaluation
-
- Considerations:
- Trades memory for speed.
- Only use on pure functions.
-
- >>> def add(x, y): return x + y
- >>> add = memoize(add)
-
- Or use as a decorator
-
- >>> @memoize
- ... def add(x, y):
- ... return x + y
-
- Use the ``cache`` keyword to provide a dict-like object as an initial cache
-
- >>> @memoize(cache={(1, 2): 3})
- ... def add(x, y):
- ... return x + y
-
- Note that the above works as a decorator because ``memoize`` is curried.
-
- It is also possible to provide a ``key(args, kwargs)`` function that
- calculates keys used for the cache, which receives an ``args`` tuple and
- ``kwargs`` dict as input, and must return a hashable value. However,
- the default key function should be sufficient most of the time.
-
- >>> # Use key function that ignores extraneous keyword arguments
- >>> @memoize(key=lambda args, kwargs: args)
- ... def add(x, y, verbose=False):
- ... if verbose:
- ... print('Calculating %s + %s' % (x, y))
- ... return x + y
- """
- return _memoize(func, cache, key)
-
-
- cdef class _memoize:
-
- property __doc__:
- def __get__(self):
- return self.func.__doc__
-
- property __name__:
- def __get__(self):
- return self.func.__name__
-
- property __wrapped__:
- def __get__(self):
- return self.func
-
- def __cinit__(self, func, cache, key):
- self.func = func
- if cache is None:
- self.cache = PyDict_New()
- else:
- self.cache = cache
- self.key = key
-
- try:
- self.may_have_kwargs = has_keywords(func) is not False
- # Is unary function (single arg, no variadic argument or keywords)?
- self.is_unary = is_arity(1, func)
- except TypeError:
- self.is_unary = False
- self.may_have_kwargs = True
-
- def __call__(self, *args, **kwargs):
- cdef object key
- if self.key is not None:
- key = self.key(args, kwargs)
- elif self.is_unary:
- key = args[0]
- elif self.may_have_kwargs:
- key = (args or None,
- PyFrozenSet_New(kwargs.items()) if kwargs else None)
- else:
- key = args
-
- if key in self.cache:
- return self.cache[key]
- else:
- result = PyObject_Call(self.func, args, kwargs)
- self.cache[key] = result
- return result
-
- def __get__(self, instance, owner):
- if instance is None:
- return self
- return curry(self, instance)
-
-
- cdef class Compose:
- """ Compose(self, *funcs)
-
- A composition of functions
-
- See Also:
- compose
- """
- # fix for #103, note: we cannot use __name__ at module-scope in cython
- __module__ = 'cytooz.functoolz'
-
- def __cinit__(self, *funcs):
- self.first = funcs[-1]
- self.funcs = tuple(reversed(funcs[:-1]))
-
- def __call__(self, *args, **kwargs):
- cdef object func, ret
- ret = PyObject_Call(self.first, args, kwargs)
- for func in self.funcs:
- ret = func(ret)
- return ret
-
- def __reduce__(self):
- return (Compose, (self.first,), self.funcs)
-
- def __setstate__(self, state):
- self.funcs = state
-
- property __name__:
- def __get__(self):
- try:
- return '_of_'.join(
- f.__name__ for f in reversed((self.first,) + self.funcs)
- )
- except AttributeError:
- return type(self).__name__
-
- property __doc__:
- def __get__(self):
- def composed_doc(*fs):
- """Generate a docstring for the composition of fs.
- """
- if not fs:
- # Argument name for the docstring.
- return '*args, **kwargs'
-
- return '{f}({g})'.format(f=fs[0].__name__, g=composed_doc(*fs[1:]))
-
- try:
- return (
- 'lambda *args, **kwargs: ' +
- composed_doc(*reversed((self.first,) + self.funcs))
- )
- except AttributeError:
- # One of our callables does not have a `__name__`, whatever.
- return 'A composition of functions'
-
-
- cdef object c_compose(object funcs):
- if not funcs:
- return identity
- elif len(funcs) == 1:
- return funcs[0]
- else:
- return Compose(*funcs)
-
-
- def compose(*funcs):
- """
- Compose functions to operate in series.
-
- Returns a function that applies other functions in sequence.
-
- Functions are applied from right to left so that
- ``compose(f, g, h)(x, y)`` is the same as ``f(g(h(x, y)))``.
-
- If no arguments are provided, the identity function (f(x) = x) is returned.
-
- >>> inc = lambda i: i + 1
- >>> compose(str, inc)(3)
- '4'
-
- See Also:
- pipe
- """
- return c_compose(funcs)
-
-
- cdef object c_pipe(object data, object funcs):
- cdef object func
- for func in funcs:
- data = func(data)
- return data
-
-
- def pipe(data, *funcs):
- """
- Pipe a value through a sequence of functions
-
- I.e. ``pipe(data, f, g, h)`` is equivalent to ``h(g(f(data)))``
-
- We think of the value as progressing through a pipe of several
- transformations, much like pipes in UNIX
-
- ``$ cat data | f | g | h``
-
- >>> double = lambda i: 2 * i
- >>> pipe(3, double, str)
- '6'
-
- See Also:
- compose
- thread_first
- thread_last
- """
- return c_pipe(data, funcs)
-
-
- cdef class complement:
- """ complement(func)
-
- Convert a predicate function to its logical complement.
-
- In other words, return a function that, for inputs that normally
- yield True, yields False, and vice-versa.
-
- >>> def iseven(n): return n % 2 == 0
- >>> isodd = complement(iseven)
- >>> iseven(2)
- True
- >>> isodd(2)
- False
- """
- def __cinit__(self, func):
- self.func = func
-
- def __call__(self, *args, **kwargs):
- return not PyObject_Call(self.func, args, kwargs) # use PyObject_Not?
-
- def __reduce__(self):
- return (complement, (self.func,))
-
-
- cdef class _juxt_inner:
- def __cinit__(self, funcs):
- self.funcs = tuple(funcs)
-
- def __call__(self, *args, **kwargs):
- if kwargs:
- return tuple(PyObject_Call(func, args, kwargs) for func in self.funcs)
- else:
- return tuple(PyObject_CallObject(func, args) for func in self.funcs)
-
- def __reduce__(self):
- return (_juxt_inner, (self.funcs,))
-
-
- cdef object c_juxt(object funcs):
- return _juxt_inner(funcs)
-
-
- def juxt(*funcs):
- """
- Creates a function that calls several functions with the same arguments
-
- Takes several functions and returns a function that applies its arguments
- to each of those functions then returns a tuple of the results.
-
- Name comes from juxtaposition: the fact of two things being seen or placed
- close together with contrasting effect.
-
- >>> inc = lambda x: x + 1
- >>> double = lambda x: x * 2
- >>> juxt(inc, double)(10)
- (11, 20)
- >>> juxt([inc, double])(10)
- (11, 20)
- """
- if len(funcs) == 1 and not PyCallable_Check(funcs[0]):
- funcs = funcs[0]
- return c_juxt(funcs)
-
-
- cpdef object do(object func, object x):
- """
- Runs ``func`` on ``x``, returns ``x``
-
- Because the results of ``func`` are not returned, only the side
- effects of ``func`` are relevant.
-
- Logging functions can be made by composing ``do`` with a storage function
- like ``list.append`` or ``file.write``
-
- >>> from cytoolz import compose
- >>> from cytoolz.curried import do
-
- >>> log = []
- >>> inc = lambda x: x + 1
- >>> inc = compose(inc, do(log.append))
- >>> inc(1)
- 2
- >>> inc(11)
- 12
- >>> log
- [1, 11]
- """
- func(x)
- return x
-
-
- cpdef object flip(object func, object a, object b):
- """
- Call the function call with the arguments flipped
-
- This function is curried.
-
- >>> def div(a, b):
- ... return a // b
- ...
- >>> flip(div, 2, 6)
- 3
- >>> div_by_two = flip(div, 2)
- >>> div_by_two(4)
- 2
-
- This is particularly useful for built in functions and functions defined
- in C extensions that accept positional only arguments. For example:
- isinstance, issubclass.
-
- >>> data = [1, 'a', 'b', 2, 1.5, object(), 3]
- >>> only_ints = list(filter(flip(isinstance, int), data))
- >>> only_ints
- [1, 2, 3]
- """
- return PyObject_CallObject(func, (b, a))
-
-
- _flip = flip # uncurried
-
-
- cpdef object return_none(object exc):
- """
- Returns None.
- """
- return None
-
-
- cdef class excepts:
- """
- A wrapper around a function to catch exceptions and
- dispatch to a handler.
-
- This is like a functional try/except block, in the same way that
- ifexprs are functional if/else blocks.
-
- Examples
- --------
- >>> excepting = excepts(
- ... ValueError,
- ... lambda a: [1, 2].index(a),
- ... lambda _: -1,
- ... )
- >>> excepting(1)
- 0
- >>> excepting(3)
- -1
-
- Multiple exceptions and default except clause.
- >>> excepting = excepts((IndexError, KeyError), lambda a: a[0])
- >>> excepting([])
- >>> excepting([1])
- 1
- >>> excepting({})
- >>> excepting({0: 1})
- 1
- """
-
- def __init__(self, exc, func, handler=return_none):
- self.exc = exc
- self.func = func
- self.handler = handler
-
- def __call__(self, *args, **kwargs):
- try:
- return self.func(*args, **kwargs)
- except self.exc as e:
- return self.handler(e)
-
- property __name__:
- def __get__(self):
- exc = self.exc
- try:
- if isinstance(exc, tuple):
- exc_name = '_or_'.join(map(attrgetter('__name__'), exc))
- else:
- exc_name = exc.__name__
- return '%s_excepting_%s' % (self.func.__name__, exc_name)
- except AttributeError:
- return 'excepting'
-
- property __doc__:
- def __get__(self):
- exc = self.exc
- try:
- if isinstance(exc, tuple):
- exc_name = '(%s)' % ', '.join(
- map(attrgetter('__name__'), exc),
- )
- else:
- exc_name = exc.__name__
-
- return dedent(
- """\
- A wrapper around {inst.func.__name__!r} that will except:
- {exc}
- and handle any exceptions with {inst.handler.__name__!r}.
-
- Docs for {inst.func.__name__!r}:
- {inst.func.__doc__}
-
- Docs for {inst.handler.__name__!r}:
- {inst.handler.__doc__}
- """
- ).format(
- inst=self,
- exc=exc_name,
- )
- except AttributeError:
- return type(self).__doc__
-
|