You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1076 lines
31 KiB
1076 lines
31 KiB
from functools import reduce, partial
|
|
import inspect
|
|
import operator
|
|
from operator import attrgetter
|
|
from textwrap import dedent
|
|
|
|
from .compatibility import PY3, PY33, PY34, PYPY, import_module
|
|
from .utils import no_default
|
|
|
|
|
|
__all__ = ('identity', 'thread_first', 'thread_last', 'memoize', 'compose',
|
|
'pipe', 'complement', 'juxt', 'do', 'curry', 'flip', 'excepts')
|
|
|
|
|
|
def identity(x):
|
|
""" Identity function. Return x
|
|
|
|
>>> identity(3)
|
|
3
|
|
"""
|
|
return x
|
|
|
|
|
|
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
|
|
"""
|
|
def evalform_front(val, form):
|
|
if callable(form):
|
|
return form(val)
|
|
if isinstance(form, tuple):
|
|
func, args = form[0], form[1:]
|
|
args = (val,) + args
|
|
return func(*args)
|
|
return reduce(evalform_front, forms, 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
|
|
"""
|
|
def evalform_back(val, form):
|
|
if callable(form):
|
|
return form(val)
|
|
if isinstance(form, tuple):
|
|
func, args = form[0], form[1:]
|
|
args = args + (val,)
|
|
return func(*args)
|
|
return reduce(evalform_back, forms, val)
|
|
|
|
|
|
def instanceproperty(fget=None, fset=None, fdel=None, doc=None, classval=None):
|
|
""" Like @property, but returns ``classval`` when used as a class attribute
|
|
|
|
>>> class MyClass(object):
|
|
... '''The class docstring'''
|
|
... @instanceproperty(classval=__doc__)
|
|
... def __doc__(self):
|
|
... return 'An object docstring'
|
|
... @instanceproperty
|
|
... def val(self):
|
|
... return 42
|
|
...
|
|
>>> MyClass.__doc__
|
|
'The class docstring'
|
|
>>> MyClass.val is None
|
|
True
|
|
>>> obj = MyClass()
|
|
>>> obj.__doc__
|
|
'An object docstring'
|
|
>>> obj.val
|
|
42
|
|
"""
|
|
if fget is None:
|
|
return partial(instanceproperty, fset=fset, fdel=fdel, doc=doc,
|
|
classval=classval)
|
|
return InstanceProperty(fget=fget, fset=fset, fdel=fdel, doc=doc,
|
|
classval=classval)
|
|
|
|
|
|
class InstanceProperty(property):
|
|
""" Like @property, but returns ``classval`` when used as a class attribute
|
|
|
|
Should not be used directly. Use ``instanceproperty`` instead.
|
|
"""
|
|
def __init__(self, fget=None, fset=None, fdel=None, doc=None,
|
|
classval=None):
|
|
self.classval = classval
|
|
property.__init__(self, fget=fget, fset=fset, fdel=fdel, doc=doc)
|
|
|
|
def __get__(self, obj, type=None):
|
|
if obj is None:
|
|
return self.classval
|
|
return property.__get__(self, obj, type)
|
|
|
|
def __reduce__(self):
|
|
state = (self.fget, self.fset, self.fdel, self.__doc__, self.classval)
|
|
return InstanceProperty, state
|
|
|
|
|
|
class curry(object):
|
|
""" 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:
|
|
toolz.curried - namespace of curried functions
|
|
https://toolz.readthedocs.io/en/latest/curry.html
|
|
"""
|
|
def __init__(self, *args, **kwargs):
|
|
if not args:
|
|
raise TypeError('__init__() takes at least 2 arguments (1 given)')
|
|
func, args = args[0], args[1:]
|
|
if not callable(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)
|
|
):
|
|
_kwargs = {}
|
|
if func.keywords:
|
|
_kwargs.update(func.keywords)
|
|
_kwargs.update(kwargs)
|
|
kwargs = _kwargs
|
|
args = func.args + args
|
|
func = func.func
|
|
|
|
if kwargs:
|
|
self._partial = partial(func, *args, **kwargs)
|
|
else:
|
|
self._partial = partial(func, *args)
|
|
|
|
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
|
|
|
|
@instanceproperty
|
|
def func(self):
|
|
return self._partial.func
|
|
|
|
if PY3: # pragma: py2 no cover
|
|
@instanceproperty
|
|
def __signature__(self):
|
|
sig = inspect.signature(self.func)
|
|
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)
|
|
|
|
@instanceproperty
|
|
def args(self):
|
|
return self._partial.args
|
|
|
|
@instanceproperty
|
|
def keywords(self):
|
|
return self._partial.keywords
|
|
|
|
@instanceproperty
|
|
def func_name(self):
|
|
return self.__name__
|
|
|
|
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 __eq__(self, other):
|
|
return (isinstance(other, curry) and self.func == other.func and
|
|
self.args == other.args and self.keywords == other.keywords)
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
try:
|
|
return self._partial(*args, **kwargs)
|
|
except TypeError as exc:
|
|
if self._should_curry(args, kwargs, exc):
|
|
return self.bind(*args, **kwargs)
|
|
raise
|
|
|
|
def _should_curry(self, args, kwargs, exc=None):
|
|
func = self.func
|
|
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):
|
|
return self._partial(*args, **kwargs)
|
|
|
|
def __get__(self, instance, owner):
|
|
if instance is None:
|
|
return self
|
|
return curry(self, instance)
|
|
|
|
def __reduce__(self):
|
|
func = self.func
|
|
modname = getattr(func, '__module__', None)
|
|
qualname = getattr(func, '__qualname__', None)
|
|
if qualname is None: # pragma: py3 no cover
|
|
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): # pragma: py2 no cover
|
|
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)
|
|
|
|
# functools.partial objects can't be pickled
|
|
userdict = tuple((k, v) for k, v in self.__dict__.items()
|
|
if k not in ('_partial', '_sigspec'))
|
|
state = (type(self), func, self.args, self.keywords, userdict,
|
|
is_decorated)
|
|
return (_restore_curry, state)
|
|
|
|
|
|
def _restore_curry(cls, func, args, kwargs, userdict, 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 {}))
|
|
obj.__dict__.update(userdict)
|
|
return obj
|
|
|
|
|
|
@curry
|
|
def memoize(func, cache=None, 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
|
|
"""
|
|
if cache is None:
|
|
cache = {}
|
|
|
|
try:
|
|
may_have_kwargs = has_keywords(func) is not False
|
|
# Is unary function (single arg, no variadic argument or keywords)?
|
|
is_unary = is_arity(1, func)
|
|
except TypeError: # pragma: no cover
|
|
may_have_kwargs = True
|
|
is_unary = False
|
|
|
|
if key is None:
|
|
if is_unary:
|
|
def key(args, kwargs):
|
|
return args[0]
|
|
elif may_have_kwargs:
|
|
def key(args, kwargs):
|
|
return (
|
|
args or None,
|
|
frozenset(kwargs.items()) if kwargs else None,
|
|
)
|
|
else:
|
|
def key(args, kwargs):
|
|
return args
|
|
|
|
def memof(*args, **kwargs):
|
|
k = key(args, kwargs)
|
|
try:
|
|
return cache[k]
|
|
except TypeError:
|
|
raise TypeError("Arguments to memoized function must be hashable")
|
|
except KeyError:
|
|
cache[k] = result = func(*args, **kwargs)
|
|
return result
|
|
|
|
try:
|
|
memof.__name__ = func.__name__
|
|
except AttributeError:
|
|
pass
|
|
memof.__doc__ = func.__doc__
|
|
memof.__wrapped__ = func
|
|
return memof
|
|
|
|
|
|
class Compose(object):
|
|
""" A composition of functions
|
|
|
|
See Also:
|
|
compose
|
|
"""
|
|
__slots__ = 'first', 'funcs'
|
|
|
|
def __init__(self, funcs):
|
|
funcs = tuple(reversed(funcs))
|
|
self.first = funcs[0]
|
|
self.funcs = funcs[1:]
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
ret = self.first(*args, **kwargs)
|
|
for f in self.funcs:
|
|
ret = f(ret)
|
|
return ret
|
|
|
|
def __getstate__(self):
|
|
return self.first, self.funcs
|
|
|
|
def __setstate__(self, state):
|
|
self.first, self.funcs = state
|
|
|
|
@instanceproperty(classval=__doc__)
|
|
def __doc__(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'
|
|
|
|
@property
|
|
def __name__(self):
|
|
try:
|
|
return '_of_'.join(
|
|
f.__name__ for f in reversed((self.first,) + self.funcs)
|
|
)
|
|
except AttributeError:
|
|
return type(self).__name__
|
|
|
|
|
|
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
|
|
"""
|
|
if not funcs:
|
|
return identity
|
|
if len(funcs) == 1:
|
|
return funcs[0]
|
|
else:
|
|
return Compose(funcs)
|
|
|
|
|
|
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
|
|
"""
|
|
for func in funcs:
|
|
data = func(data)
|
|
return data
|
|
|
|
|
|
def 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
|
|
"""
|
|
return compose(operator.not_, func)
|
|
|
|
|
|
class juxt(object):
|
|
""" 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)
|
|
"""
|
|
__slots__ = ['funcs']
|
|
|
|
def __init__(self, *funcs):
|
|
if len(funcs) == 1 and not callable(funcs[0]):
|
|
funcs = funcs[0]
|
|
self.funcs = tuple(funcs)
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
return tuple(func(*args, **kwargs) for func in self.funcs)
|
|
|
|
def __getstate__(self):
|
|
return self.funcs
|
|
|
|
def __setstate__(self, state):
|
|
self.funcs = state
|
|
|
|
|
|
def do(func, 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 toolz import compose
|
|
>>> from toolz.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
|
|
|
|
|
|
@curry
|
|
def flip(func, a, 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 func(b, a)
|
|
|
|
|
|
def return_none(exc):
|
|
""" Returns None.
|
|
"""
|
|
return None
|
|
|
|
|
|
class excepts(object):
|
|
"""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)
|
|
|
|
@instanceproperty(classval=__doc__)
|
|
def __doc__(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__
|
|
|
|
@property
|
|
def __name__(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'
|
|
|
|
|
|
if PY3: # pragma: py2 no cover
|
|
def _check_sigspec(sigspec, func, builtin_func, *builtin_args):
|
|
if sigspec is None:
|
|
try:
|
|
sigspec = inspect.signature(func)
|
|
except (ValueError, TypeError) as e:
|
|
sigspec = e
|
|
if isinstance(sigspec, ValueError):
|
|
return None, builtin_func(*builtin_args)
|
|
elif not isinstance(sigspec, inspect.Signature):
|
|
if (
|
|
func in _sigs.signatures
|
|
and ((
|
|
hasattr(func, '__signature__')
|
|
and hasattr(func.__signature__, '__get__')
|
|
) or (
|
|
PY33
|
|
and hasattr(func, '__wrapped__')
|
|
and hasattr(func.__wrapped__, '__get__')
|
|
and not callable(func.__wrapped__)
|
|
))
|
|
): # pragma: no cover (not covered in Python 3.4)
|
|
val = builtin_func(*builtin_args)
|
|
return None, val
|
|
return None, False
|
|
return sigspec, None
|
|
|
|
else: # pragma: py3 no cover
|
|
def _check_sigspec(sigspec, func, builtin_func, *builtin_args):
|
|
if sigspec is None:
|
|
try:
|
|
sigspec = inspect.getargspec(func)
|
|
except TypeError as e:
|
|
sigspec = e
|
|
if isinstance(sigspec, TypeError):
|
|
if not callable(func):
|
|
return None, False
|
|
return None, builtin_func(*builtin_args)
|
|
return sigspec, None
|
|
|
|
|
|
if PY34 or PYPY: # pragma: no cover
|
|
_check_sigspec_orig = _check_sigspec
|
|
|
|
def _check_sigspec(sigspec, func, builtin_func, *builtin_args):
|
|
# Python 3.4 and PyPy may lie, so use our registry for builtins instead
|
|
if func in _sigs.signatures:
|
|
val = builtin_func(*builtin_args)
|
|
return None, val
|
|
return _check_sigspec_orig(sigspec, func, builtin_func, *builtin_args)
|
|
|
|
_check_sigspec.__doc__ = """ \
|
|
Private function to aid in introspection compatibly across Python versions.
|
|
|
|
If a callable doesn't have a signature (Python 3) or an argspec (Python 2),
|
|
the signature registry in toolz._signatures is used.
|
|
"""
|
|
|
|
if PY3: # pragma: py2 no cover
|
|
def num_required_args(func, sigspec=None):
|
|
sigspec, rv = _check_sigspec(sigspec, func, _sigs._num_required_args,
|
|
func)
|
|
if sigspec is None:
|
|
return rv
|
|
return sum(1 for p in sigspec.parameters.values()
|
|
if p.default is p.empty
|
|
and p.kind in (p.POSITIONAL_OR_KEYWORD, p.POSITIONAL_ONLY))
|
|
|
|
def has_varargs(func, sigspec=None):
|
|
sigspec, rv = _check_sigspec(sigspec, func, _sigs._has_varargs, func)
|
|
if sigspec is None:
|
|
return rv
|
|
return any(p.kind == p.VAR_POSITIONAL
|
|
for p in sigspec.parameters.values())
|
|
|
|
def has_keywords(func, sigspec=None):
|
|
sigspec, rv = _check_sigspec(sigspec, func, _sigs._has_keywords, func)
|
|
if sigspec is None:
|
|
return rv
|
|
return any(p.default is not p.empty
|
|
or p.kind in (p.KEYWORD_ONLY, p.VAR_KEYWORD)
|
|
for p in sigspec.parameters.values())
|
|
|
|
def is_valid_args(func, args, kwargs, sigspec=None):
|
|
sigspec, rv = _check_sigspec(sigspec, func, _sigs._is_valid_args,
|
|
func, args, kwargs)
|
|
if sigspec is None:
|
|
return rv
|
|
try:
|
|
sigspec.bind(*args, **kwargs)
|
|
except TypeError:
|
|
return False
|
|
return True
|
|
|
|
def is_partial_args(func, args, kwargs, sigspec=None):
|
|
sigspec, rv = _check_sigspec(sigspec, func, _sigs._is_partial_args,
|
|
func, args, kwargs)
|
|
if sigspec is None:
|
|
return rv
|
|
try:
|
|
sigspec.bind_partial(*args, **kwargs)
|
|
except TypeError:
|
|
return False
|
|
return True
|
|
|
|
else: # pragma: py3 no cover
|
|
def num_required_args(func, sigspec=None):
|
|
sigspec, rv = _check_sigspec(sigspec, func, _sigs._num_required_args,
|
|
func)
|
|
if sigspec is None:
|
|
return rv
|
|
num_defaults = len(sigspec.defaults) if sigspec.defaults else 0
|
|
return len(sigspec.args) - num_defaults
|
|
|
|
def has_varargs(func, sigspec=None):
|
|
sigspec, rv = _check_sigspec(sigspec, func, _sigs._has_varargs, func)
|
|
if sigspec is None:
|
|
return rv
|
|
return sigspec.varargs is not None
|
|
|
|
def has_keywords(func, sigspec=None):
|
|
sigspec, rv = _check_sigspec(sigspec, func, _sigs._has_keywords, func)
|
|
if sigspec is None:
|
|
return rv
|
|
return sigspec.defaults is not None or sigspec.keywords is not None
|
|
|
|
def is_valid_args(func, args, kwargs, sigspec=None):
|
|
sigspec, rv = _check_sigspec(sigspec, func, _sigs._is_valid_args,
|
|
func, args, kwargs)
|
|
if sigspec is None:
|
|
return rv
|
|
spec = sigspec
|
|
defaults = spec.defaults or ()
|
|
num_pos = len(spec.args) - len(defaults)
|
|
missing_pos = spec.args[len(args):num_pos]
|
|
if any(arg not in kwargs for arg in missing_pos):
|
|
return False
|
|
|
|
if spec.varargs is None:
|
|
num_extra_pos = max(0, len(args) - num_pos)
|
|
else:
|
|
num_extra_pos = 0
|
|
|
|
kwargs = dict(kwargs)
|
|
|
|
# Add missing keyword arguments (unless already included in `args`)
|
|
missing_kwargs = spec.args[num_pos + num_extra_pos:]
|
|
kwargs.update(zip(missing_kwargs, defaults[num_extra_pos:]))
|
|
|
|
# Convert call to use positional arguments
|
|
args = args + tuple(kwargs.pop(key) for key in spec.args[len(args):])
|
|
|
|
if (
|
|
not spec.keywords and kwargs
|
|
or not spec.varargs and len(args) > len(spec.args)
|
|
or set(spec.args[:len(args)]) & set(kwargs)
|
|
):
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
def is_partial_args(func, args, kwargs, sigspec=None):
|
|
sigspec, rv = _check_sigspec(sigspec, func, _sigs._is_partial_args,
|
|
func, args, kwargs)
|
|
if sigspec is None:
|
|
return rv
|
|
spec = sigspec
|
|
defaults = spec.defaults or ()
|
|
num_pos = len(spec.args) - len(defaults)
|
|
if spec.varargs is None:
|
|
num_extra_pos = max(0, len(args) - num_pos)
|
|
else:
|
|
num_extra_pos = 0
|
|
|
|
kwargs = dict(kwargs)
|
|
|
|
# Add missing keyword arguments (unless already included in `args`)
|
|
missing_kwargs = spec.args[num_pos + num_extra_pos:]
|
|
kwargs.update(zip(missing_kwargs, defaults[num_extra_pos:]))
|
|
|
|
# Add missing position arguments as keywords (may already be in kwargs)
|
|
missing_args = spec.args[len(args):num_pos + num_extra_pos]
|
|
kwargs.update((x, None) for x in missing_args)
|
|
|
|
# Convert call to use positional arguments
|
|
args = args + tuple(kwargs.pop(key) for key in spec.args[len(args):])
|
|
|
|
if (
|
|
not spec.keywords and kwargs
|
|
or not spec.varargs and len(args) > len(spec.args)
|
|
or set(spec.args[:len(args)]) & set(kwargs)
|
|
):
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
|
|
def is_arity(n, func, sigspec=None):
|
|
""" Does a function have only n positional arguments?
|
|
|
|
This function relies on introspection and does not call the function.
|
|
Returns None if validity can't be determined.
|
|
|
|
>>> def f(x):
|
|
... return x
|
|
>>> is_arity(1, f)
|
|
True
|
|
>>> def g(x, y=1):
|
|
... return x + y
|
|
>>> is_arity(1, g)
|
|
False
|
|
"""
|
|
sigspec, rv = _check_sigspec(sigspec, func, _sigs._is_arity, n, func)
|
|
if sigspec is None:
|
|
return rv
|
|
num = num_required_args(func, sigspec)
|
|
if num is not None:
|
|
num = num == n
|
|
if not num:
|
|
return False
|
|
varargs = has_varargs(func, sigspec)
|
|
if varargs:
|
|
return False
|
|
keywords = has_keywords(func, sigspec)
|
|
if keywords:
|
|
return False
|
|
if num is None or varargs is None or keywords is None: # pragma: no cover
|
|
return None
|
|
return True
|
|
|
|
|
|
num_required_args.__doc__ = """ \
|
|
Number of required positional arguments
|
|
|
|
This function relies on introspection and does not call the function.
|
|
Returns None if validity can't be determined.
|
|
|
|
>>> def f(x, y, z=3):
|
|
... return x + y + z
|
|
>>> num_required_args(f)
|
|
2
|
|
>>> def g(*args, **kwargs):
|
|
... pass
|
|
>>> num_required_args(g)
|
|
0
|
|
"""
|
|
|
|
has_varargs.__doc__ = """ \
|
|
Does a function have variadic positional arguments?
|
|
|
|
This function relies on introspection and does not call the function.
|
|
Returns None if validity can't be determined.
|
|
|
|
>>> def f(*args):
|
|
... return args
|
|
>>> has_varargs(f)
|
|
True
|
|
>>> def g(**kwargs):
|
|
... return kwargs
|
|
>>> has_varargs(g)
|
|
False
|
|
"""
|
|
|
|
has_keywords.__doc__ = """ \
|
|
Does a function have keyword arguments?
|
|
|
|
This function relies on introspection and does not call the function.
|
|
Returns None if validity can't be determined.
|
|
|
|
>>> def f(x, y=0):
|
|
... return x + y
|
|
|
|
>>> has_keywords(f)
|
|
True
|
|
"""
|
|
|
|
is_valid_args.__doc__ = """ \
|
|
Is ``func(*args, **kwargs)`` a valid function call?
|
|
|
|
This function relies on introspection and does not call the function.
|
|
Returns None if validity can't be determined.
|
|
|
|
>>> def add(x, y):
|
|
... return x + y
|
|
|
|
>>> is_valid_args(add, (1,), {})
|
|
False
|
|
>>> is_valid_args(add, (1, 2), {})
|
|
True
|
|
>>> is_valid_args(map, (), {})
|
|
False
|
|
|
|
**Implementation notes**
|
|
Python 2 relies on ``inspect.getargspec``, which only works for
|
|
user-defined functions. Python 3 uses ``inspect.signature``, which
|
|
works for many more types of callables.
|
|
|
|
Many builtins in the standard library are also supported.
|
|
"""
|
|
|
|
is_partial_args.__doc__ = """ \
|
|
Can partial(func, *args, **kwargs)(*args2, **kwargs2) be a valid call?
|
|
|
|
Returns True *only* if the call is valid or if it is possible for the
|
|
call to become valid by adding more positional or keyword arguments.
|
|
|
|
This function relies on introspection and does not call the function.
|
|
Returns None if validity can't be determined.
|
|
|
|
>>> def add(x, y):
|
|
... return x + y
|
|
|
|
>>> is_partial_args(add, (1,), {})
|
|
True
|
|
>>> is_partial_args(add, (1, 2), {})
|
|
True
|
|
>>> is_partial_args(add, (1, 2, 3), {})
|
|
False
|
|
>>> is_partial_args(map, (), {})
|
|
True
|
|
|
|
**Implementation notes**
|
|
Python 2 relies on ``inspect.getargspec``, which only works for
|
|
user-defined functions. Python 3 uses ``inspect.signature``, which
|
|
works for many more types of callables.
|
|
|
|
Many builtins in the standard library are also supported.
|
|
"""
|
|
|
|
from . import _signatures as _sigs
|