metafunctions 1.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- metafunctions/__init__.py +27 -0
- metafunctions/api.py +151 -0
- metafunctions/core/__init__.py +19 -0
- metafunctions/core/base.py +294 -0
- metafunctions/core/call_state.py +97 -0
- metafunctions/core/concurrent.py +145 -0
- metafunctions/core/decorators.py +40 -0
- metafunctions/exceptions.py +25 -0
- metafunctions/map.py +36 -0
- metafunctions/operators.py +14 -0
- metafunctions/tests/__init__.py +0 -0
- metafunctions/tests/simple_nodes.py +30 -0
- metafunctions/tests/test_api.py +87 -0
- metafunctions/tests/test_at_operator.py +40 -0
- metafunctions/tests/test_call_state.py +251 -0
- metafunctions/tests/test_concurrent.py +228 -0
- metafunctions/tests/test_deferred_value.py +24 -0
- metafunctions/tests/test_function_chain.py +45 -0
- metafunctions/tests/test_function_merge.py +154 -0
- metafunctions/tests/test_imports.py +34 -0
- metafunctions/tests/test_map.py +56 -0
- metafunctions/tests/test_pipe.py +319 -0
- metafunctions/tests/test_simple_function.py +24 -0
- metafunctions/tests/test_star.py +83 -0
- metafunctions/tests/test_util.py +86 -0
- metafunctions/tests/util.py +38 -0
- metafunctions/util.py +52 -0
- metafunctions-1.1.2.dist-info/METADATA +221 -0
- metafunctions-1.1.2.dist-info/RECORD +32 -0
- metafunctions-1.1.2.dist-info/WHEEL +5 -0
- metafunctions-1.1.2.dist-info/licenses/LICENSE +21 -0
- metafunctions-1.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError
|
|
2
|
+
from importlib.metadata import version
|
|
3
|
+
|
|
4
|
+
from metafunctions.api import bind_call_state
|
|
5
|
+
from metafunctions.api import concurrent
|
|
6
|
+
from metafunctions.api import locate_error
|
|
7
|
+
from metafunctions.api import mmap
|
|
8
|
+
from metafunctions.api import node
|
|
9
|
+
from metafunctions.api import recall
|
|
10
|
+
from metafunctions.api import star
|
|
11
|
+
from metafunctions.api import store
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
__version__ = version("metafunctions")
|
|
15
|
+
except PackageNotFoundError:
|
|
16
|
+
__version__ = "unknown"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"bind_call_state",
|
|
20
|
+
"concurrent",
|
|
21
|
+
"locate_error",
|
|
22
|
+
"mmap",
|
|
23
|
+
"node",
|
|
24
|
+
"recall",
|
|
25
|
+
"star",
|
|
26
|
+
"store",
|
|
27
|
+
]
|
metafunctions/api.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for use in function pipelines.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import typing as tp
|
|
7
|
+
|
|
8
|
+
from metafunctions import operators
|
|
9
|
+
from metafunctions import util
|
|
10
|
+
from metafunctions.core import CallState
|
|
11
|
+
from metafunctions.core import FunctionMerge
|
|
12
|
+
from metafunctions.core import MetaFunction
|
|
13
|
+
from metafunctions.core import SimpleFunction
|
|
14
|
+
from metafunctions.core.concurrent import ConcurrentMerge
|
|
15
|
+
from metafunctions.map import MergeMap
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def node(_func=None, *, name=None):
|
|
19
|
+
"""Turn the decorated function into a MetaFunction.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
_func: Internal use. This will be the decorated function if node is used as a decorator
|
|
23
|
+
with no params.
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
|
|
27
|
+
@node
|
|
28
|
+
def f(x):
|
|
29
|
+
<do something cool>
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def decorator(function):
|
|
33
|
+
newfunc = SimpleFunction(function, name=name)
|
|
34
|
+
return newfunc
|
|
35
|
+
|
|
36
|
+
if not _func:
|
|
37
|
+
return decorator
|
|
38
|
+
return decorator(_func)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def bind_call_state(func):
|
|
42
|
+
@functools.wraps(func)
|
|
43
|
+
def provides_call_state(*args, **kwargs):
|
|
44
|
+
call_state = kwargs.pop("call_state")
|
|
45
|
+
return func(call_state, *args, **kwargs)
|
|
46
|
+
|
|
47
|
+
provides_call_state._receives_call_state = True
|
|
48
|
+
return provides_call_state
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def star(meta_function: MetaFunction) -> MetaFunction:
|
|
52
|
+
"""
|
|
53
|
+
star calls its Metafunction with *x instead of x.
|
|
54
|
+
"""
|
|
55
|
+
fname = str(meta_function)
|
|
56
|
+
|
|
57
|
+
# This convoluted inline `if` just decides whether we should add brackets or not.
|
|
58
|
+
@node(name=f"star{fname}" if fname.startswith("(") else f"star({fname})")
|
|
59
|
+
@functools.wraps(meta_function)
|
|
60
|
+
def wrapper(args, **kwargs):
|
|
61
|
+
return meta_function(*args, **kwargs)
|
|
62
|
+
|
|
63
|
+
return wrapper
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def store(key):
|
|
67
|
+
"""Store the received output in the meta data dictionary under the given key."""
|
|
68
|
+
|
|
69
|
+
@node(name=f"store('{key}')")
|
|
70
|
+
@bind_call_state
|
|
71
|
+
def storer(call_state, val):
|
|
72
|
+
call_state.data[key] = val
|
|
73
|
+
return val
|
|
74
|
+
|
|
75
|
+
return storer
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def recall(key, from_call_state: CallState = None):
|
|
79
|
+
"""Retrieve the given key from the meta data dictionary. Optionally, use `from_call_state` to
|
|
80
|
+
specify a different call_state than the current one.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
@node(name=f"recall('{key}')")
|
|
84
|
+
@bind_call_state
|
|
85
|
+
def recaller(call_state, *_):
|
|
86
|
+
if from_call_state:
|
|
87
|
+
return from_call_state.data[key]
|
|
88
|
+
return call_state.data[key]
|
|
89
|
+
|
|
90
|
+
return recaller
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def concurrent(function: FunctionMerge) -> ConcurrentMerge:
|
|
94
|
+
"""
|
|
95
|
+
Upgrade the specified FunctionMerge object to a ConcurrentMerge, which runs each of its
|
|
96
|
+
component functions in separate processes. See ConcurrentMerge documentation for more
|
|
97
|
+
information.
|
|
98
|
+
|
|
99
|
+
Usage:
|
|
100
|
+
|
|
101
|
+
c = concurrent(long_running_function + other_long_running_function)
|
|
102
|
+
c(input_data) # The two functions run in parallel
|
|
103
|
+
"""
|
|
104
|
+
return ConcurrentMerge(function)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def mmap(function: tp.Callable, operator: tp.Callable = operators.concat) -> MergeMap:
|
|
108
|
+
"""
|
|
109
|
+
Upgrade the specified function to a MergeMap, which calls its single function once per input,
|
|
110
|
+
as per the builtin `map` (https://docs.python.org/3.6/library/functions.html#map).
|
|
111
|
+
|
|
112
|
+
Consider the name 'mmap' to be a placeholder for now.
|
|
113
|
+
"""
|
|
114
|
+
return MergeMap(MetaFunction.make_meta(function), operator)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def locate_error(
|
|
118
|
+
meta_function: MetaFunction,
|
|
119
|
+
use_color=util.system_supports_color(), # noqa: B008
|
|
120
|
+
) -> SimpleFunction:
|
|
121
|
+
"""
|
|
122
|
+
Wrap the given MetaFunction with an error handler that adds location information to any
|
|
123
|
+
exception raised therein.
|
|
124
|
+
|
|
125
|
+
Usage:
|
|
126
|
+
cmp = locate_error(a | b | c)
|
|
127
|
+
cmp()
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def with_location(*args, call_state, **kwargs):
|
|
131
|
+
new_e = None
|
|
132
|
+
try:
|
|
133
|
+
return meta_function(*args, call_state=call_state, **kwargs)
|
|
134
|
+
except Exception as e: # noqa: BLE001
|
|
135
|
+
if hasattr(e, "location") and e.location:
|
|
136
|
+
# If the exception has location info attached
|
|
137
|
+
location = e.location
|
|
138
|
+
else:
|
|
139
|
+
location = call_state.highlight_active_function()
|
|
140
|
+
|
|
141
|
+
if use_color:
|
|
142
|
+
location = util.color_highlights(location)
|
|
143
|
+
detailed_message = (
|
|
144
|
+
f"{e!s} \n\nOccured in the following function: {location}"
|
|
145
|
+
)
|
|
146
|
+
new_e = type(e)(detailed_message).with_traceback(e.__traceback__)
|
|
147
|
+
new_e.__cause__ = e.__cause__
|
|
148
|
+
raise new_e
|
|
149
|
+
|
|
150
|
+
with_location._receives_call_state = True
|
|
151
|
+
return node(with_location, name=str(meta_function))
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .base import DeferredValue
|
|
2
|
+
from .base import FunctionChain
|
|
3
|
+
from .base import FunctionMerge
|
|
4
|
+
from .base import MetaFunction
|
|
5
|
+
from .base import SimpleFunction
|
|
6
|
+
from .call_state import CallState
|
|
7
|
+
from .concurrent import ConcurrentMerge
|
|
8
|
+
from .decorators import manage_call_state
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"CallState",
|
|
12
|
+
"ConcurrentMerge",
|
|
13
|
+
"DeferredValue",
|
|
14
|
+
"FunctionChain",
|
|
15
|
+
"FunctionMerge",
|
|
16
|
+
"MetaFunction",
|
|
17
|
+
"SimpleFunction",
|
|
18
|
+
"manage_call_state",
|
|
19
|
+
]
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import functools
|
|
3
|
+
import itertools
|
|
4
|
+
import typing as tp
|
|
5
|
+
|
|
6
|
+
from metafunctions import exceptions
|
|
7
|
+
from metafunctions import operators
|
|
8
|
+
from metafunctions.core.call_state import CallState
|
|
9
|
+
from metafunctions.core.decorators import binary_operation
|
|
10
|
+
from metafunctions.core.decorators import manage_call_state
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MetaFunction(metaclass=abc.ABCMeta):
|
|
14
|
+
# Metafunctions will pass call state to any function with this attribute set to true
|
|
15
|
+
_receives_call_state = True
|
|
16
|
+
_function_join_str = ""
|
|
17
|
+
|
|
18
|
+
@abc.abstractmethod
|
|
19
|
+
def __init__(self, *args, **kwargs):
|
|
20
|
+
"""A MetaFunction is a function that contains other functions. When executed, it calls the
|
|
21
|
+
functions it contains.
|
|
22
|
+
"""
|
|
23
|
+
self._functions = []
|
|
24
|
+
|
|
25
|
+
@abc.abstractmethod
|
|
26
|
+
def __call__(self, *args, call_state=None, **kwargs):
|
|
27
|
+
"""Call the functions contained in this MetaFunction"""
|
|
28
|
+
|
|
29
|
+
def __str__(self):
|
|
30
|
+
return "({})".format(
|
|
31
|
+
f" {self._function_join_str} ".join(str(f) for f in self.functions)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def functions(self):
|
|
36
|
+
return self._functions
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def make_meta(function):
|
|
40
|
+
"""Wrap the given function in a metafunction, unless it's already a metafunction"""
|
|
41
|
+
if not isinstance(function, MetaFunction):
|
|
42
|
+
return SimpleFunction(function)
|
|
43
|
+
return function
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def defer_value(value):
|
|
47
|
+
"""Wrap the given value in a DeferredValue object, (which returns its value when called)."""
|
|
48
|
+
return DeferredValue(value)
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def new_call_state():
|
|
52
|
+
return CallState()
|
|
53
|
+
|
|
54
|
+
### Operator overloads ###
|
|
55
|
+
@binary_operation
|
|
56
|
+
def __or__(self, other):
|
|
57
|
+
return FunctionChain.combine(self, other)
|
|
58
|
+
|
|
59
|
+
@binary_operation
|
|
60
|
+
def __ror__(self, other):
|
|
61
|
+
return FunctionChain.combine(other, self)
|
|
62
|
+
|
|
63
|
+
@binary_operation
|
|
64
|
+
def __and__(self, other):
|
|
65
|
+
return FunctionMerge.combine(operators.concat, self, other)
|
|
66
|
+
|
|
67
|
+
@binary_operation
|
|
68
|
+
def __rand__(self, other):
|
|
69
|
+
return FunctionMerge.combine(operators.concat, other, self)
|
|
70
|
+
|
|
71
|
+
@binary_operation
|
|
72
|
+
def __add__(self, other):
|
|
73
|
+
return FunctionMerge(operators.add, (self, other))
|
|
74
|
+
|
|
75
|
+
@binary_operation
|
|
76
|
+
def __radd__(self, other):
|
|
77
|
+
return FunctionMerge(operators.add, (other, self))
|
|
78
|
+
|
|
79
|
+
@binary_operation
|
|
80
|
+
def __sub__(self, other):
|
|
81
|
+
return FunctionMerge(operators.sub, (self, other))
|
|
82
|
+
|
|
83
|
+
@binary_operation
|
|
84
|
+
def __rsub__(self, other):
|
|
85
|
+
return FunctionMerge(operators.sub, (other, self))
|
|
86
|
+
|
|
87
|
+
@binary_operation
|
|
88
|
+
def __mul__(self, other):
|
|
89
|
+
return FunctionMerge(operators.mul, (self, other))
|
|
90
|
+
|
|
91
|
+
@binary_operation
|
|
92
|
+
def __rmul__(self, other):
|
|
93
|
+
return FunctionMerge(operators.mul, (other, self))
|
|
94
|
+
|
|
95
|
+
@binary_operation
|
|
96
|
+
def __truediv__(self, other):
|
|
97
|
+
return FunctionMerge(operators.truediv, (self, other))
|
|
98
|
+
|
|
99
|
+
@binary_operation
|
|
100
|
+
def __rtruediv__(self, other):
|
|
101
|
+
return FunctionMerge(operators.truediv, (other, self))
|
|
102
|
+
|
|
103
|
+
@binary_operation
|
|
104
|
+
def __matmul__(self, other):
|
|
105
|
+
from metafunctions.api import star
|
|
106
|
+
|
|
107
|
+
return FunctionChain.combine(self, star(other))
|
|
108
|
+
|
|
109
|
+
@binary_operation
|
|
110
|
+
def __rmatmul__(self, other):
|
|
111
|
+
from metafunctions.api import star
|
|
112
|
+
|
|
113
|
+
return FunctionChain.combine(other, star(self))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class FunctionChain(MetaFunction):
|
|
117
|
+
_function_join_str = "|"
|
|
118
|
+
|
|
119
|
+
def __init__(self, *functions):
|
|
120
|
+
"""A FunctionChain is a metafunction that calls its functions in sequence, passing the
|
|
121
|
+
results of the first function subsequent functions.
|
|
122
|
+
"""
|
|
123
|
+
super().__init__()
|
|
124
|
+
self._functions = functions
|
|
125
|
+
|
|
126
|
+
@manage_call_state
|
|
127
|
+
def __call__(self, *args, **kwargs):
|
|
128
|
+
f_iter = iter(self._functions)
|
|
129
|
+
result = next(f_iter)(*args, **kwargs)
|
|
130
|
+
for f in f_iter:
|
|
131
|
+
result = f(result, **kwargs)
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
def __repr__(self):
|
|
135
|
+
return f"{self.__class__.__name__}{self.functions}"
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def combine(cls, *funcs):
|
|
139
|
+
"""Merge chains; i.e., combine all FunctionChains in `funcs` into a single FunctionChain."""
|
|
140
|
+
new_funcs = []
|
|
141
|
+
for f in funcs:
|
|
142
|
+
if type(f) is cls:
|
|
143
|
+
new_funcs.extend(f.functions)
|
|
144
|
+
else:
|
|
145
|
+
new_funcs.append(f)
|
|
146
|
+
return cls(*new_funcs)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class FunctionMerge(MetaFunction):
|
|
150
|
+
_character_to_operator: tp.ClassVar = {
|
|
151
|
+
"+": operators.add,
|
|
152
|
+
"-": operators.sub,
|
|
153
|
+
"*": operators.mul,
|
|
154
|
+
"/": operators.truediv,
|
|
155
|
+
"&": operators.concat,
|
|
156
|
+
}
|
|
157
|
+
_operator_to_character: tp.ClassVar = {
|
|
158
|
+
v: k for k, v in _character_to_operator.items()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
def __init__(self, merge_func: tp.Callable, functions: tuple, function_join_str=""):
|
|
162
|
+
"""
|
|
163
|
+
A FunctionMerge merges its functions by executing all of them and passing their results to
|
|
164
|
+
`merge_func`.
|
|
165
|
+
|
|
166
|
+
Behaviour of __call__:
|
|
167
|
+
|
|
168
|
+
FunctionMerge does not pass all positional arguments to all of its functions. Rather, given
|
|
169
|
+
`f=FunctionMerge()` when f is called with `f(*args)`,
|
|
170
|
+
|
|
171
|
+
* if len(args) == 1, each component function is called with args[0]
|
|
172
|
+
* if len(args) > 1 <= len(functions), function n is called with arg n. Any remaining
|
|
173
|
+
functions after all args have been exhausted are called with no args.
|
|
174
|
+
* if len(args) < len(functions), a MetaFunction CallError is raised.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
function_join_str: If you're using a `merge_func` that is not one of the standard operator
|
|
178
|
+
functions, use this argument to provide a custom character to use in string formatting. If
|
|
179
|
+
not provided, we default to using str(merge_func).
|
|
180
|
+
"""
|
|
181
|
+
super().__init__()
|
|
182
|
+
self._merge_func = merge_func
|
|
183
|
+
self._functions = functions
|
|
184
|
+
self._function_join_str = function_join_str or self._operator_to_character.get(
|
|
185
|
+
merge_func, str(merge_func)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
@manage_call_state
|
|
189
|
+
def __call__(self, *args, **kwargs):
|
|
190
|
+
args_iter, func_iter = self._get_call_iterators(args)
|
|
191
|
+
|
|
192
|
+
results = []
|
|
193
|
+
# Note that args_iter appears first in the zip. This is because I know its len is <=
|
|
194
|
+
# len(func_iter) (I asserted so above). In zip, if the first iterator is longer than the
|
|
195
|
+
# second, the first will be advanced one extra time, because zip has already called next()
|
|
196
|
+
# on the first iterator before discovering that the second has been exhausted.
|
|
197
|
+
for arg, f in zip(args_iter, func_iter):
|
|
198
|
+
results.append(self._call_function(f, (arg,), kwargs))
|
|
199
|
+
|
|
200
|
+
# Any extra functions are called with no input
|
|
201
|
+
results.extend([self._call_function(f, (), kwargs) for f in func_iter])
|
|
202
|
+
return self._merge_func(*results)
|
|
203
|
+
|
|
204
|
+
def __repr__(self):
|
|
205
|
+
return f"{self.__class__.__name__}({self._merge_func}, {self.functions})"
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def combine(cls, merge_func: tp.Callable, *funcs, function_join_str=None):
|
|
209
|
+
"""Combine FunctionMerges. If consecutive FunctionMerges have the same merge_funcs, combine
|
|
210
|
+
them into a single FunctionMerge.
|
|
211
|
+
|
|
212
|
+
NOTE: combine does not check to make sure the merge_func can accept the new number of
|
|
213
|
+
arguments, or that combining is appropriate for the operator. (e.g., it is inappropriate to
|
|
214
|
+
combine FunctionMerges where order of operations matter. 5 / 2 / 3 != 5 / (2 / 3))
|
|
215
|
+
"""
|
|
216
|
+
new_funcs = []
|
|
217
|
+
for f in funcs:
|
|
218
|
+
if isinstance(f, cls) and f._merge_func is merge_func:
|
|
219
|
+
new_funcs.extend(f.functions)
|
|
220
|
+
else:
|
|
221
|
+
new_funcs.append(f)
|
|
222
|
+
return cls(merge_func, tuple(new_funcs), function_join_str=function_join_str)
|
|
223
|
+
|
|
224
|
+
def _get_call_iterators(self, args):
|
|
225
|
+
"""Do length checking and return (`args_iter`, `call_iter`), iterables of arguments and
|
|
226
|
+
self.functions. Call them using zip. Note that len(args) can be less than
|
|
227
|
+
len(self.functions), and remaining functions should be called with no argument.
|
|
228
|
+
"""
|
|
229
|
+
args_iter = iter(args)
|
|
230
|
+
func_iter = iter(self.functions)
|
|
231
|
+
if len(args) > len(self.functions):
|
|
232
|
+
raise exceptions.CallError(
|
|
233
|
+
f"{self} takes 1 or <= {len(self.functions)} arguments, but {len(args)} were given"
|
|
234
|
+
)
|
|
235
|
+
if len(args) == 1:
|
|
236
|
+
args_iter = itertools.repeat(next(args_iter))
|
|
237
|
+
|
|
238
|
+
return args_iter, func_iter
|
|
239
|
+
|
|
240
|
+
def _call_function(self, f, args: tuple, kwargs: dict):
|
|
241
|
+
"""This function receives one function, and the args and kwargs that should be used to call
|
|
242
|
+
that function. It returns the result of the function call. This gets its own method so that
|
|
243
|
+
subclasses can customize its behaviour.
|
|
244
|
+
"""
|
|
245
|
+
return f(*args, **kwargs)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class SimpleFunction(MetaFunction):
|
|
249
|
+
def __init__(self, function, name=None):
|
|
250
|
+
"""A MetaFunction-aware wrapper around a single function
|
|
251
|
+
The `bind` parameter causes us to pass a meta object as the first argument to our inherited function, but it is only respected if the wrapped function is not another metafunction.
|
|
252
|
+
"""
|
|
253
|
+
# An interesting side effect of wraps: it causes simplefunctions to collapse into each
|
|
254
|
+
# other. Because calling wraps on a function copies all that function's attributes to the
|
|
255
|
+
# new function, we copy _function, etc from the wrapped function. Essentially
|
|
256
|
+
# absorbing it. I'm not sure if that's good or bad.
|
|
257
|
+
functools.wraps(function)(self)
|
|
258
|
+
|
|
259
|
+
super().__init__()
|
|
260
|
+
self._function = function
|
|
261
|
+
self._name = name or getattr(function, "__name__", False) or str(function)
|
|
262
|
+
|
|
263
|
+
@manage_call_state
|
|
264
|
+
def __call__(self, *args, call_state, **kwargs):
|
|
265
|
+
if getattr(self._function, "_receives_call_state", False):
|
|
266
|
+
kwargs["call_state"] = call_state
|
|
267
|
+
return self._function(*args, **kwargs)
|
|
268
|
+
|
|
269
|
+
def __repr__(self):
|
|
270
|
+
return f"{self.__class__.__name__}({self.functions[0]!r})"
|
|
271
|
+
|
|
272
|
+
def __str__(self):
|
|
273
|
+
return self._name
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def functions(self):
|
|
277
|
+
return (self._function,)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class DeferredValue(SimpleFunction):
|
|
281
|
+
def __init__(self, value):
|
|
282
|
+
"""A simple Deferred Value. Returns `value` when called. Equivalent to lambda x: x."""
|
|
283
|
+
self._value = value
|
|
284
|
+
self._name = repr(value)
|
|
285
|
+
|
|
286
|
+
def __call__(self, *args, **kwargs):
|
|
287
|
+
return self._value
|
|
288
|
+
|
|
289
|
+
def __repr__(self):
|
|
290
|
+
return f"{self.__class__.__name__}({self._value!r})"
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def functions(self):
|
|
294
|
+
return (self,)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from collections import OrderedDict
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from collections import namedtuple
|
|
4
|
+
|
|
5
|
+
from metafunctions import util
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CallState:
|
|
9
|
+
Node = namedtuple("Node", "function insert_index")
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
"""
|
|
13
|
+
A call tree keeps track of function execution order in metafunctions. This is used to
|
|
14
|
+
accurately determine the location of the currently active function, and to identify
|
|
15
|
+
exception locations. It can be thought of as a metafunction aware call stack.
|
|
16
|
+
"""
|
|
17
|
+
# Meta entry is conceptually the root node of the call tree
|
|
18
|
+
self._meta_entry = None
|
|
19
|
+
self.active_node = None
|
|
20
|
+
self._nodes_visited = 0
|
|
21
|
+
|
|
22
|
+
# A dictionary of {child: parent} functions
|
|
23
|
+
self._parents = OrderedDict()
|
|
24
|
+
|
|
25
|
+
# A dictionary of {parent: [children]}, in call order
|
|
26
|
+
self._children = defaultdict(list)
|
|
27
|
+
self.data = {}
|
|
28
|
+
|
|
29
|
+
def push(self, f):
|
|
30
|
+
"""
|
|
31
|
+
Push a function onto the tree
|
|
32
|
+
"""
|
|
33
|
+
if self._meta_entry is None:
|
|
34
|
+
node = self.Node(f, self._nodes_visited)
|
|
35
|
+
self._meta_entry = node
|
|
36
|
+
else:
|
|
37
|
+
node = self.Node(f, self._nodes_visited)
|
|
38
|
+
self._parents[node] = self.active_node
|
|
39
|
+
self._children[self.active_node].append(node)
|
|
40
|
+
self._nodes_visited += 1
|
|
41
|
+
self.active_node = node
|
|
42
|
+
|
|
43
|
+
def pop(self):
|
|
44
|
+
"""Remove last inserted f from the call tree."""
|
|
45
|
+
try:
|
|
46
|
+
node, parent = self._parents.popitem()
|
|
47
|
+
except KeyError:
|
|
48
|
+
m = self._meta_entry
|
|
49
|
+
self._meta_entry = None
|
|
50
|
+
self.active_node = None
|
|
51
|
+
return m
|
|
52
|
+
self._children.pop(node, None)
|
|
53
|
+
self.active_node = parent
|
|
54
|
+
return node[0]
|
|
55
|
+
|
|
56
|
+
def iter_parent_nodes(self, node):
|
|
57
|
+
"""
|
|
58
|
+
Return an iterator over all parents of this node in the tree, ending with the meta_entry
|
|
59
|
+
point.
|
|
60
|
+
"""
|
|
61
|
+
# if node isn't in _parents, it must be the meta entry
|
|
62
|
+
parent = self._parents.get(node, self._meta_entry)
|
|
63
|
+
yield parent
|
|
64
|
+
if parent is not self._meta_entry:
|
|
65
|
+
yield from self.iter_parent_nodes(parent)
|
|
66
|
+
|
|
67
|
+
def highlight_active_function(self):
|
|
68
|
+
"""
|
|
69
|
+
Return a formatted string showing the location of the most recently called function in
|
|
70
|
+
call_state.
|
|
71
|
+
|
|
72
|
+
Consider this a 'you are here' when called from within a function pipeline.
|
|
73
|
+
"""
|
|
74
|
+
current_function = self.active_node.function
|
|
75
|
+
current_name = str(current_function)
|
|
76
|
+
new_name = util.highlight(current_name)
|
|
77
|
+
|
|
78
|
+
# rename active function in parent (if active function isn't in parent, active function
|
|
79
|
+
# becomes parent)
|
|
80
|
+
for parent in self.iter_parent_nodes(self.active_node):
|
|
81
|
+
parent_name = str(parent.function)
|
|
82
|
+
|
|
83
|
+
# Note we count occurences of current name in functions we've called so far. We do this
|
|
84
|
+
# because it's possible previous functions contain the name of this function.
|
|
85
|
+
name_count = sum(
|
|
86
|
+
str(n.function).count(current_name) for n in self._children[parent]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
new_name = util.replace_nth(parent_name, current_name, name_count, new_name)
|
|
90
|
+
current_function = parent.function
|
|
91
|
+
current_name = parent_name
|
|
92
|
+
|
|
93
|
+
# if new parent name hasn't changed (meaning it didn't contain the name we're
|
|
94
|
+
# highlighting), highlight the parent name
|
|
95
|
+
if new_name == parent_name:
|
|
96
|
+
new_name = util.highlight(new_name)
|
|
97
|
+
return new_name
|