Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add composition as an operator via a new composable class #531

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions toolz/curried/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
apply,
comp,
complement,
composable,
compose,
compose_left,
concat,
Expand Down
61 changes: 55 additions & 6 deletions toolz/functoolz.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
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

PYPY = hasattr(sys, 'pypy_version_info') and sys.version_info[0] > 2


__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')

Expand Down Expand Up @@ -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:
Expand Down
35 changes: 34 additions & 1 deletion toolz/tests/test_functoolz.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down