-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
346 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
# -*- coding: utf-8 -*- | ||
import inspect | ||
import functools | ||
from .wraps import wraps | ||
from .assertions import isfunc, iscallable | ||
|
||
|
||
def curry(arity_or_fn=None, ignore_kwargs=False, evaluator=None, *args, **kw): | ||
""" | ||
Creates a function that accepts one or more arguments of a function and | ||
either invokes func returning its result if at least arity number of | ||
arguments have been provided, or returns a function that accepts the | ||
remaining function arguments until the function arity is satisfied. | ||
This function is overloaded: you can pass a function or coroutine function | ||
as first argument or an `int` indicating the explicit function arity. | ||
Function arity can be inferred via function signature or explicitly | ||
passed via `arity_or_fn` param. | ||
You can optionally ignore keyword based arguments as well passsing the | ||
`ignore_kwargs` param with `True` value. | ||
This function can be used as decorator. | ||
Arguments: | ||
arity_or_fn (int|function|coroutinefunction): function arity to curry | ||
or function to curry. | ||
ignore_kwargs (bool): ignore keyword arguments as arity to satisfy | ||
during curry. | ||
evaluator (function): use a custom arity evaluator function. | ||
*args (mixed): mixed variadic arguments for partial function | ||
application. | ||
*kwargs (mixed): keyword variadic arguments for partial function | ||
application. | ||
Raises: | ||
TypeError: if function is not a function or a coroutine function. | ||
Returns: | ||
function or coroutinefunction: function will be returned until all the | ||
function arity is satisfied, where a coroutine function will be | ||
returned instead. | ||
Usage:: | ||
# Function signature inferred function arity | ||
@paco.curry | ||
async def task(x, y, z=0): | ||
return x * y + z | ||
await task(4)(4)(z=8) | ||
# => 24 | ||
# User defined function arity | ||
@paco.curry(4) | ||
async def task(x, y, *args, **kw): | ||
return x * y + args[0] * args[1] | ||
await task(4)(4)(8)(8) | ||
# => 80 | ||
# Ignore keyword arguments from arity | ||
@paco.curry(ignore_kwargs=True) | ||
async def task(x, y, z=0): | ||
return x * y | ||
await task(4)(4) | ||
# => 16 | ||
""" | ||
def isvalidarg(x): | ||
return all([ | ||
x.kind != x.VAR_KEYWORD, | ||
x.kind != x.VAR_POSITIONAL, | ||
any([ | ||
not ignore_kwargs, | ||
ignore_kwargs and x.default == x.empty | ||
]) | ||
]) | ||
|
||
def params(fn): | ||
return inspect.signature(fn).parameters.values() | ||
|
||
def infer_arity(fn): | ||
return len([x for x in params(fn) if isvalidarg(x)]) | ||
|
||
def merge_args(acc, args, kw): | ||
_args, _kw = acc | ||
_args = _args + args | ||
_kw = _kw or {} | ||
_kw.update(kw) | ||
return _args, _kw | ||
|
||
def currier(arity, acc, fn, *args, **kw): | ||
""" | ||
Function either continues curring of the arguments | ||
or executes function if desired arguments have being collected. | ||
If function curried is variadic then execution without arguments | ||
will finish curring and trigger the function | ||
""" | ||
# Merge call arguments with accumulated ones | ||
_args, _kw = merge_args(acc, args, kw) | ||
|
||
# Get current function call accumulated arity | ||
current_arity = len(args) | ||
|
||
# Count keyword params as arity to satisfy, if required | ||
if not ignore_kwargs: | ||
current_arity += len(kw) | ||
|
||
# Decrease function arity to satisfy | ||
arity -= current_arity | ||
|
||
# Use user-defined custom arity evaluator strategy, if present | ||
currify = evaluator and evaluator(acc, fn) | ||
|
||
# If arity is not satisfied, return recursive partial function | ||
if currify is not False and arity > 0: | ||
return functools.partial(currier, arity, (_args, _kw), fn) | ||
|
||
# If arity is satisfied, instanciate coroutine and return it | ||
return fn(*_args, **_kw) | ||
|
||
def wrapper(fn, *args, **kw): | ||
if not iscallable(fn): | ||
raise TypeError('first argument must a coroutine function, a ' | ||
'function or a method.') | ||
|
||
# Infer function arity, if required | ||
arity = (arity_or_fn if isinstance(arity_or_fn, int) | ||
else infer_arity(fn)) | ||
|
||
# Wraps function as coroutine function, if needed. | ||
fn = wraps(fn) if isfunc(fn) else fn | ||
|
||
# Otherwise return recursive currier function | ||
return currier(arity, (args, kw), fn, *args, **kw) if arity > 0 else fn | ||
|
||
# Return currier function or decorator wrapper | ||
return (wrapper(arity_or_fn, *args, **kw) | ||
if iscallable(arity_or_fn) | ||
else wrapper) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
# -*- coding: utf-8 -*- | ||
import asyncio | ||
from paco import curry | ||
from .helpers import run_in_loop | ||
|
||
|
||
def task(x, y, baz=None, *args, **kw): | ||
return x + y, baz, kw | ||
|
||
|
||
@asyncio.coroutine | ||
def coro(x, y, baz=None, *args, **kw): | ||
return task(x, y, baz=baz, *args, **kw) | ||
|
||
|
||
def test_curry_function_arity(): | ||
num, val, kw = run_in_loop(curry(task)(2)(4)(baz='foo')) | ||
assert num == 6 | ||
assert val == 'foo' | ||
assert kw == {} | ||
|
||
num, val, kw = run_in_loop(curry(task)(2, 4)(baz='foo')) | ||
assert num == 6 | ||
assert val == 'foo' | ||
assert kw == {} | ||
|
||
num, val, kw = run_in_loop(curry(task)(2, 4, baz='foo')) | ||
assert num == 6 | ||
assert val == 'foo' | ||
assert kw == {} | ||
|
||
num, val, kw = run_in_loop(curry(task)(2, 4, baz='foo', fee=True)) | ||
assert num == 6 | ||
assert val == 'foo' | ||
assert kw == {'fee': True} | ||
|
||
|
||
def test_curry_single_arity(): | ||
assert run_in_loop(curry(lambda x: x)(True)) | ||
|
||
|
||
def test_curry_zero_arity(): | ||
assert run_in_loop(curry(lambda: True)) | ||
|
||
|
||
def test_curry_custom_arity(): | ||
currier = curry(4) | ||
num, val, kw = run_in_loop(currier(task)(2)(4)(baz='foo')(tee=True)) | ||
assert num == 6 | ||
assert val == 'foo' | ||
assert kw == {'tee': True} | ||
|
||
|
||
def test_curry_ignore_kwargs(): | ||
currier = curry(ignore_kwargs=True) | ||
num, val, kw = run_in_loop(currier(task)(2)(4)) | ||
assert num == 6 | ||
assert val is None | ||
assert kw == {} | ||
|
||
currier = curry(ignore_kwargs=True) | ||
num, val, kw = run_in_loop(currier(task)(2)(4, baz='foo', tee=True)) | ||
assert num == 6 | ||
assert val is 'foo' | ||
assert kw == {'tee': True} | ||
|
||
|
||
def test_curry_extra_arguments(): | ||
currier = curry(4) | ||
num, val, kw = run_in_loop(currier(task)(2)(4)(baz='foo')(tee=True)) | ||
assert num == 6 | ||
assert val == 'foo' | ||
assert kw == {'tee': True} | ||
|
||
currier = curry(4) | ||
num, val, kw = run_in_loop(currier(task)(2)(4)(baz='foo')(tee=True)) | ||
assert num == 6 | ||
assert val == 'foo' | ||
assert kw == {'tee': True} | ||
|
||
|
||
def test_curry_evaluator_function(): | ||
def evaluator(acc, fn): | ||
return len(acc[0]) < 3 | ||
|
||
def task(x, y): | ||
return x * y | ||
|
||
currier = curry(evaluator=evaluator) | ||
assert run_in_loop(currier(task)(4, 4)) == 16 | ||
|
||
|
||
def test_curry_decorator(): | ||
@curry | ||
def task(x, y, z): | ||
return x + y + z | ||
|
||
assert run_in_loop(task(2)(4)(8)) == 14 | ||
|
||
@curry(4) | ||
def task(x, y, *args): | ||
return x + y + args[0] + args[1] | ||
|
||
assert run_in_loop(task(2)(4)(8)(10)) == 24 | ||
|
||
@curry(4) | ||
@asyncio.coroutine | ||
def task(x, y, *args): | ||
return x + y + args[0] + args[1] | ||
|
||
assert run_in_loop(task(2)(4)(8)(10)) == 24 | ||
|
||
|
||
def test_curry_coroutine(): | ||
num, val, kw = run_in_loop(curry(coro)(2)(4)(baz='foo', tee=True)) | ||
assert num == 6 | ||
assert val == 'foo' | ||
assert kw == {'tee': True} |
Oops, something went wrong.