diff --git a/toolz/curried/__init__.py b/toolz/curried/__init__.py index 356eddbd..23e84982 100644 --- a/toolz/curried/__init__.py +++ b/toolz/curried/__init__.py @@ -29,6 +29,7 @@ apply, comp, complement, + composable, compose, compose_left, concat, diff --git a/toolz/functoolz.py b/toolz/functoolz.py index 067a3d49..f2d0e3ac 100644 --- a/toolz/functoolz.py +++ b/toolz/functoolz.py @@ -1,11 +1,10 @@ -from functools import reduce, partial import inspect import sys -from operator import attrgetter, not_ +from functools import partial, reduce from importlib import import_module +from operator import attrgetter, not_ from textwrap import dedent from types import MethodType -import sys from .utils import no_default @@ -13,8 +12,8 @@ __all__ = ('identity', 'apply', 'thread_first', 'thread_last', 'memoize', - 'compose', 'compose_left', 'pipe', 'complement', 'juxt', 'do', - 'curry', 'flip', 'excepts') + 'composable', 'compose', 'compose_left', 'pipe', 'complement', + 'juxt', 'do', 'curry', 'flip', 'excepts') PYPY = hasattr(sys, 'pypy_version_info') @@ -472,7 +471,57 @@ def memof(*args, **kwargs): return memof -class Compose(object): +class _Composable: + """ Base class for functions supporting composition via the pipe operator. + + See Also: + composable + compose + """ + # TODO: when `typing_extensions` becomes a dependency for this toolz or we + # decide to support Python 3.10+ only, we can write complete type + # annotations here. + # + # We can make `_Composable` inherit from `Generic[P, T]`, where + # `P = (typing/typing_extensions).ParamSpec('P')`. In this case, + # `__or__` must accept a `Callable[[T], S]` and return `composable[P, S]`. + def __or__(self, other): + return compose(other, self) + + def __ror__(self, other): + return compose(self, other) + + +class composable(_Composable): + """ A composable function using the pipe operator ``|``. + + Can be used as a decorator: + + >>> @composable + ... def inc(i): + ... return i + 1 + >>> composed = inc | str + >>> composed(3) + '4' + + Or inline: + + >>> inc = composable(lambda i: i + 1) + >>> composed = inc | str + >>> composed(3) + '4' + + See Also: + compose + """ + def __init__(self, __func): + self.__func = __func + + def __call__(self, *args, **kwargs): + return self.__func(*args, **kwargs) + + +class Compose(_Composable): """ A composition of functions See Also: diff --git a/toolz/tests/test_functoolz.py b/toolz/tests/test_functoolz.py index 555cf48d..da03c0cc 100644 --- a/toolz/tests/test_functoolz.py +++ b/toolz/tests/test_functoolz.py @@ -1,6 +1,6 @@ import inspect import toolz -from toolz.functoolz import (thread_first, thread_last, memoize, curry, +from toolz.functoolz import (thread_first, thread_last, memoize, curry, composable, compose, compose_left, pipe, complement, do, juxt, flip, excepts, apply) from operator import add, mul, itemgetter @@ -679,6 +679,39 @@ def test_compose_left(): assert compose_left(*compose_left_args)(*args, **kw) == expected +def test_composable(): + composable_inc = composable(inc) + + # check direct call + assert composable_inc(0) == 1 + + # check composition via pipe operator + assert (composable_inc | double)(0) == 2 + + # check multiple composition via pipe operator + assert (composable(double) | inc | iseven | str)(3) == "False" + + # check right composition + assert (double | composable_inc)(2) == 5 + + # check decorator + @composable + def dec(i): + return i - 1 + + composition = dec | str + assert composition(3) == "2" + + +def test_composable_compose(): + # check that functions created via `compose` are composable via `|` + composed = compose(double, inc) + assert composed(3) == 8 + + composed2 = composed | str + assert composed2(3) == "8" + + def test_pipe(): assert pipe(1, inc) == 2 assert pipe(1, inc, inc) == 3