diff --git a/toolz/curried/__init__.py b/toolz/curried/__init__.py index fab6255f..80dadac6 100644 --- a/toolz/curried/__init__.py +++ b/toolz/curried/__init__.py @@ -30,6 +30,7 @@ comp, complement, compose, + compose_left, concat, concatv, count, diff --git a/toolz/functoolz.py b/toolz/functoolz.py index 9091127d..2ed180dc 100644 --- a/toolz/functoolz.py +++ b/toolz/functoolz.py @@ -10,8 +10,8 @@ __all__ = ('identity', 'apply', 'thread_first', 'thread_last', 'memoize', - 'compose', 'pipe', 'complement', 'juxt', 'do', 'curry', 'flip', - 'excepts') + 'compose', 'compose_left', 'pipe', 'complement', 'juxt', 'do', + 'curry', 'flip', 'excepts') def identity(x): @@ -537,6 +537,7 @@ def compose(*funcs): '4' See Also: + compose_left pipe """ if not funcs: @@ -547,6 +548,27 @@ def compose(*funcs): return Compose(funcs) +def compose_left(*funcs): + """ Compose functions to operate in series. + + Returns a function that applies other functions in sequence. + + Functions are applied from left to right so that + ``compose_left(f, g, h)(x, y)`` is the same as ``h(g(f(x, y)))``. + + If no arguments are provided, the identity function (f(x) = x) is returned. + + >>> inc = lambda i: i + 1 + >>> compose_left(inc, str)(3) + '4' + + See Also: + compose + pipe + """ + return compose(*reversed(funcs)) + + def pipe(data, *funcs): """ Pipe a value through a sequence of functions diff --git a/toolz/tests/test_functoolz.py b/toolz/tests/test_functoolz.py index 148c42ff..342327f5 100644 --- a/toolz/tests/test_functoolz.py +++ b/toolz/tests/test_functoolz.py @@ -1,7 +1,8 @@ import platform from toolz.functoolz import (thread_first, thread_last, memoize, curry, - compose, pipe, complement, do, juxt, flip, excepts, apply) + compose, compose_left, pipe, complement, do, juxt, + flip, excepts, apply) from operator import add, mul, itemgetter from toolz.utils import raises from functools import partial @@ -505,17 +506,54 @@ def _should_curry(self, args, kwargs, exc=None): """ -def test_compose(): - assert compose()(0) == 0 - assert compose(inc)(0) == 1 - assert compose(double, inc)(0) == 2 - assert compose(str, iseven, inc, double)(3) == "False" - assert compose(str, add)(1, 2) == '3' +def generate_compose_test_cases(): + """ + Generate test cases for parametrized tests of the compose function. + """ - def f(a, b, c=10): + def add_then_multiply(a, b, c=10): return (a + b) * c - assert compose(str, inc, f)(1, 2, c=3) == '10' + return ( + ( + (), # arguments to compose() + (0,), {}, # positional and keyword args to the Composed object + 0 # expected result + ), + ( + (inc,), + (0,), {}, + 1 + ), + ( + (double, inc), + (0,), {}, + 2 + ), + ( + (str, iseven, inc, double), + (3,), {}, + "False" + ), + ( + (str, add), + (1, 2), {}, + '3' + ), + ( + (str, inc, add_then_multiply), + (1, 2), {"c": 3}, + '10' + ), + ) + + +def test_compose(): + for (compose_args, args, kw, expected) in generate_compose_test_cases(): + assert compose(*compose_args)(*args, **kw) == expected + + +def test_compose_metadata(): # Define two functions with different names def f(a): @@ -536,6 +574,25 @@ def g(a): assert composed.__doc__ == 'A composition of functions' +def generate_compose_left_test_cases(): + """ + Generate test cases for parametrized tests of the compose function. + + These are based on, and equivalent to, those produced by + enerate_compose_test_cases(). + """ + return tuple( + (tuple(reversed(compose_args)), args, kwargs, expected) + for (compose_args, args, kwargs, expected) + in generate_compose_test_cases() + ) + + +def test_compose_left(): + for (compose_left_args, args, kw, expected) in generate_compose_left_test_cases(): + assert compose_left(*compose_left_args)(*args, **kw) == expected + + def test_pipe(): assert pipe(1, inc) == 2 assert pipe(1, inc, inc) == 3