Skip to content

Commit c9d69b9

Browse files
authored
Merge pull request #398 from ariebovenberg/compose-improvements
Improvements to "compose"
2 parents 1b23ada + 7764358 commit c9d69b9

File tree

2 files changed

+130
-1
lines changed

2 files changed

+130
-1
lines changed

toolz/functoolz.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from operator import attrgetter
55
from importlib import import_module
66
from textwrap import dedent
7+
from types import MethodType
78

89
from .compatibility import PY3, PY34, PYPY
910
from .utils import no_default
@@ -521,6 +522,45 @@ def __name__(self):
521522
except AttributeError:
522523
return type(self).__name__
523524

525+
def __repr__(self):
526+
return '{.__class__.__name__}{!r}'.format(
527+
self, tuple(reversed((self.first, ) + self.funcs)))
528+
529+
def __eq__(self, other):
530+
if isinstance(other, Compose):
531+
return other.first == self.first and other.funcs == self.funcs
532+
return NotImplemented
533+
534+
def __ne__(self, other):
535+
equality = self.__eq__(other)
536+
return NotImplemented if equality is NotImplemented else not equality
537+
538+
def __hash__(self):
539+
return hash(self.first) ^ hash(self.funcs)
540+
541+
# Mimic the descriptor behavior of python functions.
542+
# i.e. let Compose be called as a method when bound to a class.
543+
if PY3: # pragma: py2 no cover
544+
# adapted from
545+
# docs.python.org/3/howto/descriptor.html#functions-and-methods
546+
def __get__(self, obj, objtype=None):
547+
return self if obj is None else MethodType(self, obj)
548+
else: # pragma: py3 no cover
549+
# adapted from
550+
# docs.python.org/2/howto/descriptor.html#functions-and-methods
551+
def __get__(self, obj, objtype=None):
552+
return self if obj is None else MethodType(self, obj, objtype)
553+
554+
# introspection with Signature is only possible from py3.3+
555+
if PY3: # pragma: py2 no cover
556+
@instanceproperty
557+
def __signature__(self):
558+
base = inspect.signature(self.first)
559+
last = inspect.signature(self.funcs[-1])
560+
return base.replace(return_annotation=last.return_annotation)
561+
562+
__wrapped__ = instanceproperty(attrgetter('first'))
563+
524564

525565
def compose(*funcs):
526566
""" Compose functions to operate in series.

toolz/tests/test_functoolz.py

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import inspect
12
import platform
23

34
from toolz.functoolz import (thread_first, thread_last, memoize, curry,
45
compose, compose_left, pipe, complement, do, juxt,
56
flip, excepts, apply)
7+
from toolz.compatibility import PY3
68
from operator import add, mul, itemgetter
79
from toolz.utils import raises
810
from functools import partial
@@ -24,6 +26,26 @@ def double(x):
2426
return 2 * x
2527

2628

29+
class AlwaysEquals(object):
30+
"""useful to test correct __eq__ implementation of other objects"""
31+
32+
def __eq__(self, other):
33+
return True
34+
35+
def __ne__(self, other):
36+
return False
37+
38+
39+
class NeverEquals(object):
40+
"""useful to test correct __eq__ implementation of other objects"""
41+
42+
def __eq__(self, other):
43+
return False
44+
45+
def __ne__(self, other):
46+
return True
47+
48+
2749
def test_apply():
2850
assert apply(double, 5) == 10
2951
assert tuple(map(apply, [double, inc, double], [10, 500, 8000])) == (20, 501, 16000)
@@ -573,6 +595,74 @@ def g(a):
573595
assert composed.__name__ == 'Compose'
574596
assert composed.__doc__ == 'A composition of functions'
575597

598+
assert repr(composed) == 'Compose({!r}, {!r})'.format(f, h)
599+
600+
assert composed == compose(f, h)
601+
assert composed == AlwaysEquals()
602+
assert not composed == compose(h, f)
603+
assert not composed == object()
604+
assert not composed == NeverEquals()
605+
606+
assert composed != compose(h, f)
607+
assert composed != NeverEquals()
608+
assert composed != object()
609+
assert not composed != compose(f, h)
610+
assert not composed != AlwaysEquals()
611+
612+
assert hash(composed) == hash(compose(f, h))
613+
assert hash(composed) != hash(compose(h, f))
614+
615+
bindable = compose(str, lambda x: x*2, lambda x, y=0: int(x) + y)
616+
617+
class MyClass:
618+
619+
def __int__(self):
620+
return 8
621+
622+
my_method = bindable
623+
my_static_method = staticmethod(bindable)
624+
625+
assert MyClass.my_method(3) == '6'
626+
assert MyClass.my_method(3, y=2) == '10'
627+
assert MyClass.my_static_method(2) == '4'
628+
assert MyClass().my_method() == '16'
629+
assert MyClass().my_method(y=3) == '22'
630+
assert MyClass().my_static_method(0) == '0'
631+
assert MyClass().my_static_method(0, 1) == '2'
632+
633+
assert compose(f, h).__wrapped__ is h
634+
assert compose(f, h).__class__.__wrapped__ is None
635+
636+
# __signature__ is python3 only
637+
if PY3:
638+
639+
def myfunc(a, b, c, *d, **e):
640+
return 4
641+
642+
def otherfunc(f):
643+
return 'result: {}'.format(f)
644+
645+
# set annotations compatibly with python2 syntax
646+
myfunc.__annotations__ = {
647+
'a': int,
648+
'b': str,
649+
'c': float,
650+
'd': int,
651+
'e': bool,
652+
'return': int,
653+
}
654+
otherfunc.__annotations__ = {'f': int, 'return': str}
655+
656+
composed = compose(otherfunc, myfunc)
657+
sig = inspect.signature(composed)
658+
assert sig.parameters == inspect.signature(myfunc).parameters
659+
assert sig.return_annotation == str
660+
661+
class MyClass:
662+
method = composed
663+
664+
assert len(inspect.signature(MyClass().method).parameters) == 4
665+
576666

577667
def generate_compose_left_test_cases():
578668
"""
@@ -708,4 +798,3 @@ def raise_(a):
708798
excepting = excepts(object(), object(), object())
709799
assert excepting.__name__ == 'excepting'
710800
assert excepting.__doc__ == excepts.__doc__
711-

0 commit comments

Comments
 (0)