-
Notifications
You must be signed in to change notification settings - Fork 3
/
normal.py
178 lines (142 loc) · 5.51 KB
/
normal.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# SPDX-License-Identifier: 0BSD
# Copyright 2019 Alexander Kozhevnikov <mentalisttraceur@gmail.com>
"""The classic ``compose``, with all the Pythonic features."""
__all__ = ('compose', 'acompose', 'sacompose')
__version__ = '1.5.0'
from inspect import isawaitable as _isawaitable
from reprlib import recursive_repr as _recursive_repr
def _name(obj):
return type(obj).__name__
class compose:
"""Function composition: compose(f, g)(...) is equivalent to f(g(...))."""
def __init__(self, *functions):
"""Initialize the composed function.
Arguments:
*functions: Functions (or other callables) to compose.
Raises:
TypeError:
If no arguments are given.
If any argument is not callable.
"""
if not functions:
raise TypeError(_name(self) + '() needs at least one argument')
_functions = []
for function in reversed(functions):
if not callable(function):
raise TypeError(_name(self) + '() arguments must be callable')
if isinstance(function, compose):
_functions.extend(function.functions)
else:
_functions.append(function)
self.__wrapped__ = _functions[0]
self._wrappers = tuple(_functions[1:])
def __call__(self, /, *args, **kwargs):
"""Call the composed function."""
result = self.__wrapped__(*args, **kwargs)
for function in self._wrappers:
result = function(result)
return result
def __get__(self, obj, objtype=None):
"""Get the composed function as a bound method."""
wrapped = self.__wrapped__
try:
bind = type(wrapped).__get__
except AttributeError:
return self
bound_wrapped = bind(wrapped, obj, objtype)
if bound_wrapped is wrapped:
return self
bound_self = type(self)(bound_wrapped)
bound_self._wrappers = self._wrappers
return bound_self
@_recursive_repr('<...>')
def __repr__(self):
"""Represent the composed function as an unambiguous string."""
arguments = ', '.join(map(repr, reversed(self.functions)))
return _name(self) + '(' + arguments + ')'
@property
def functions(self):
"""Read-only tuple of the composed callables, in order of execution."""
return (self.__wrapped__,) + tuple(self._wrappers)
class acompose:
"""Asynchronous function composition.
await acompose(f, g)(...) is equivalent to one of
f(g(...))
await f(g(...))
f(await g(...))
await f(await g(...))
based on whether f and g return awaitable values.
"""
def __init__(self, *functions):
"""Initialize the composed function.
Arguments:
*functions: Functions (or other callables) to compose.
Raises:
TypeError:
If no arguments are given.
If any argument is not callable.
"""
if not functions:
raise TypeError(_name(self) + '() needs at least one argument')
_functions = []
for function in reversed(functions):
if not callable(function):
raise TypeError(_name(self) + '() arguments must be callable')
if isinstance(function, (acompose, sacompose)):
_functions.extend(function.functions)
else:
_functions.append(function)
self.__wrapped__ = _functions[0]
self._wrappers = tuple(_functions[1:])
async def __call__(self, /, *args, **kwargs):
"""Call the composed function."""
result = self.__wrapped__(*args, **kwargs)
if _isawaitable(result):
result = await result
for function in self._wrappers:
result = function(result)
if _isawaitable(result):
result = await result
return result
__repr__ = compose.__repr__
__get__ = compose.__get__
functions = compose.functions
class sacompose:
"""Sometimes-asynchronous function composition.
sacompose(f, g)(...) is equivalent to:
acompose(f, g)(...) if either f or g returns an awaitable object.
compose(f, g)(...) if neither f nor g returns an awaitable object.
"""
__init__ = acompose.__init__
def __call__(self, /, *args, **kwargs):
"""Call the composed function.
The return value will either be a normal value if all composed
callables return normal values, or an awaitable that needs an
`await` if at least one composed callable returns an awaitable.
"""
result = self.__wrapped__(*args, **kwargs)
if _isawaitable(result):
return _finish(self._wrappers, result)
wrappers = iter(self._wrappers)
for function in wrappers:
result = function(result)
if _isawaitable(result):
return _finish(wrappers, result)
return result
__repr__ = compose.__repr__
__get__ = compose.__get__
functions = compose.functions
async def _finish(remaining_functions, first_awaitable_result):
result = await first_awaitable_result
for function in remaining_functions:
result = function(result)
if _isawaitable(result):
result = await result
return result
# Portability to some minimal Python implementations:
try:
compose.__name__
except AttributeError:
compose.__name__ = 'compose'
acompose.__name__ = 'acompose'
sacompose.__name__ = 'sacompose'