From 2511e4959be5e176ac251a413e29989582661b9f Mon Sep 17 00:00:00 2001 From: Ruan Comelli Date: Fri, 8 Apr 2022 21:35:40 -0300 Subject: [PATCH 1/5] Add compose as an operator --- toolz/functoolz.py | 54 +++++++++++++++++++++++++++++++++-- toolz/tests/test_functoolz.py | 23 ++++++++++++++- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/toolz/functoolz.py b/toolz/functoolz.py index 067a3d49..d7b229d6 100644 --- a/toolz/functoolz.py +++ b/toolz/functoolz.py @@ -1,11 +1,11 @@ -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 typing import Callable, Generic, TypeVar from .utils import no_default @@ -18,6 +18,9 @@ PYPY = hasattr(sys, 'pypy_version_info') +T = TypeVar('T') +S = TypeVar('S') + def identity(x): """ Identity function. Return x @@ -472,6 +475,51 @@ def memof(*args, **kwargs): return memof +class composable(Generic[T]): + """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 + """ + + # TODO: when `typing_extensions` becomes a dependency for this toolz or we decide + # to support Python 3.10+ only, type annotations can be much improved here. + # + # First, we can make `composable` inherit from `Generic[P, T]`, where + # `P = (typing/typing_extensions).ParamSpec('P')`. + # + # Second, the annotation for `call` should be replaced with `Callable[P, T]` + # + # Third, the definition for `__call__` can be written as + # `def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:` + # + # Finally, `__or__` must return `composable[P, S]`. + def __init__(self, call: Callable[..., T]) -> None: + self.call = call + + def __call__(self, *args, **kwargs) -> T: + return self.call(*args, **kwargs) + + def __or__(self, other: Callable[[T], S]) -> 'composable[S]': + return composable(compose(other, self)) + + class Compose(object): """ A composition of functions diff --git a/toolz/tests/test_functoolz.py b/toolz/tests/test_functoolz.py index 555cf48d..f2a0815a 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 @@ -571,6 +571,27 @@ def test_compose(): assert compose(*compose_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 decorator + @composable + def dec(i): + return i - 1 + + composition = dec | str + assert composition(3) == "2" + + def test_compose_metadata(): # Define two functions with different names From 51f174f6c959b6746feb6cf8556a12a018362989 Mon Sep 17 00:00:00 2001 From: Ruan Comelli Date: Fri, 8 Apr 2022 22:05:54 -0300 Subject: [PATCH 2/5] Remove type annotations from `composable` --- toolz/functoolz.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/toolz/functoolz.py b/toolz/functoolz.py index d7b229d6..8f838aef 100644 --- a/toolz/functoolz.py +++ b/toolz/functoolz.py @@ -5,7 +5,6 @@ from operator import attrgetter, not_ from textwrap import dedent from types import MethodType -from typing import Callable, Generic, TypeVar from .utils import no_default @@ -18,9 +17,6 @@ PYPY = hasattr(sys, 'pypy_version_info') -T = TypeVar('T') -S = TypeVar('S') - def identity(x): """ Identity function. Return x @@ -475,7 +471,7 @@ def memof(*args, **kwargs): return memof -class composable(Generic[T]): +class composable: """A composable function using the pipe operator ``|``. Can be used as a decorator: @@ -497,26 +493,29 @@ class composable(Generic[T]): See Also: compose """ + __slots__ = ('func',) - # TODO: when `typing_extensions` becomes a dependency for this toolz or we decide - # to support Python 3.10+ only, type annotations can be much improved here. + # 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. # # First, we can make `composable` inherit from `Generic[P, T]`, where # `P = (typing/typing_extensions).ParamSpec('P')`. # - # Second, the annotation for `call` should be replaced with `Callable[P, T]` + # Second, the annotation for `call` should be written as + # `Callable[P, T]` # # Third, the definition for `__call__` can be written as # `def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:` # # Finally, `__or__` must return `composable[P, S]`. - def __init__(self, call: Callable[..., T]) -> None: - self.call = call + def __init__(self, func): + self.func = func - def __call__(self, *args, **kwargs) -> T: - return self.call(*args, **kwargs) + def __call__(self, *args, **kwargs): + return self.func(*args, **kwargs) - def __or__(self, other: Callable[[T], S]) -> 'composable[S]': + def __or__(self, other): return composable(compose(other, self)) From b37480b614cffe4814f370dca655bdf01c3330f0 Mon Sep 17 00:00:00 2001 From: Ruan Comelli Date: Fri, 8 Apr 2022 22:18:43 -0300 Subject: [PATCH 3/5] Make `compose` return `Composable` functions --- toolz/functoolz.py | 49 +++++++++++++++++---------------- toolz/tests/test_functoolz.py | 51 ++++++++++++++++++++--------------- 2 files changed, 54 insertions(+), 46 deletions(-) diff --git a/toolz/functoolz.py b/toolz/functoolz.py index 8f838aef..d0b58232 100644 --- a/toolz/functoolz.py +++ b/toolz/functoolz.py @@ -471,8 +471,26 @@ def memof(*args, **kwargs): return memof -class composable: - """A composable function using the pipe operator ``|``. +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) + + +class composable(_Composable): + """ A composable function using the pipe operator ``|``. Can be used as a decorator: @@ -493,33 +511,14 @@ class composable: See Also: compose """ - __slots__ = ('func',) - - # 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. - # - # First, we can make `composable` inherit from `Generic[P, T]`, where - # `P = (typing/typing_extensions).ParamSpec('P')`. - # - # Second, the annotation for `call` should be written as - # `Callable[P, T]` - # - # Third, the definition for `__call__` can be written as - # `def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:` - # - # Finally, `__or__` must return `composable[P, S]`. - def __init__(self, func): - self.func = func + def __init__(self, __func): + self.__func = __func def __call__(self, *args, **kwargs): - return self.func(*args, **kwargs) - - def __or__(self, other): - return composable(compose(other, self)) + return self.__func(*args, **kwargs) -class Compose(object): +class Compose(_Composable): """ A composition of functions See Also: diff --git a/toolz/tests/test_functoolz.py b/toolz/tests/test_functoolz.py index f2a0815a..16cea4ca 100644 --- a/toolz/tests/test_functoolz.py +++ b/toolz/tests/test_functoolz.py @@ -571,27 +571,6 @@ def test_compose(): assert compose(*compose_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 decorator - @composable - def dec(i): - return i - 1 - - composition = dec | str - assert composition(3) == "2" - - def test_compose_metadata(): # Define two functions with different names @@ -700,6 +679,36 @@ 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 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 From 21ff3d99b9159a7c89243952bc26864d369b46e3 Mon Sep 17 00:00:00 2001 From: Ruan Comelli Date: Fri, 8 Apr 2022 22:22:34 -0300 Subject: [PATCH 4/5] Fix testing and export `composable` in `__all__` --- toolz/curried/__init__.py | 1 + toolz/functoolz.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) 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 d0b58232..a28a61e2 100644 --- a/toolz/functoolz.py +++ b/toolz/functoolz.py @@ -12,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') From b30d4a6970a24854ad64e8ced57fca35c2c2be9b Mon Sep 17 00:00:00 2001 From: Ruan Comelli Date: Sun, 10 Apr 2022 16:45:08 -0300 Subject: [PATCH 5/5] Add support for right composition to `composable` --- toolz/functoolz.py | 3 +++ toolz/tests/test_functoolz.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/toolz/functoolz.py b/toolz/functoolz.py index a28a61e2..f2d0e3ac 100644 --- a/toolz/functoolz.py +++ b/toolz/functoolz.py @@ -488,6 +488,9 @@ class _Composable: 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 ``|``. diff --git a/toolz/tests/test_functoolz.py b/toolz/tests/test_functoolz.py index 16cea4ca..da03c0cc 100644 --- a/toolz/tests/test_functoolz.py +++ b/toolz/tests/test_functoolz.py @@ -691,6 +691,9 @@ def test_composable(): # 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):