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,145 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import os
|
|
3
|
+
import pickle
|
|
4
|
+
from collections import namedtuple
|
|
5
|
+
from multiprocessing import Queue
|
|
6
|
+
from operator import itemgetter
|
|
7
|
+
|
|
8
|
+
from metafunctions import exceptions
|
|
9
|
+
from metafunctions.core.base import FunctionMerge
|
|
10
|
+
from metafunctions.core.decorators import manage_call_state
|
|
11
|
+
|
|
12
|
+
# Result tuple to be sent back from workers. Defined at module level for eas of pickling
|
|
13
|
+
_ConcurrentResult = namedtuple(
|
|
14
|
+
"_ConcurrentResult", "index result call_state_data exception location"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConcurrentMerge(FunctionMerge):
|
|
19
|
+
def __init__(self, function_merge: FunctionMerge):
|
|
20
|
+
"""A subclass of FunctionMerge that calls each of its component functions in parallel.
|
|
21
|
+
|
|
22
|
+
ConcurrentMerge takes a FunctionMerge object and upgrades it.
|
|
23
|
+
"""
|
|
24
|
+
if not isinstance(function_merge, FunctionMerge):
|
|
25
|
+
# This check is necessary because functools.wraps will copy FunctionMerge attributes to
|
|
26
|
+
# objects that are not FunctionMerges, so this init will succeed, then result in errors
|
|
27
|
+
# at call time.
|
|
28
|
+
raise exceptions.CompositionError(
|
|
29
|
+
f"{type(self)} can only upgrade FunctionMerges"
|
|
30
|
+
)
|
|
31
|
+
if not hasattr(os, "fork"):
|
|
32
|
+
raise exceptions.CompositionError(
|
|
33
|
+
f"{type(self).__name__} requires os.fork, and thus is only available on unix"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
super().__init__(
|
|
37
|
+
function_merge._merge_func,
|
|
38
|
+
function_merge._functions,
|
|
39
|
+
function_merge._function_join_str,
|
|
40
|
+
)
|
|
41
|
+
self._function_merge = function_merge
|
|
42
|
+
|
|
43
|
+
def __str__(self):
|
|
44
|
+
merge_name = str(self._function_merge)
|
|
45
|
+
return (
|
|
46
|
+
f"concurrent{merge_name}"
|
|
47
|
+
if merge_name.startswith("(")
|
|
48
|
+
else f"concurrent({merge_name})"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@manage_call_state
|
|
52
|
+
def __call__(self, *args, **kwargs):
|
|
53
|
+
"""We fork here, and execute each function in a child process before joining the results
|
|
54
|
+
with _merge_func
|
|
55
|
+
"""
|
|
56
|
+
arg_iter, func_iter = self._get_call_iterators(args)
|
|
57
|
+
enumerated_funcs = enumerate(func_iter)
|
|
58
|
+
result_q = Queue()
|
|
59
|
+
|
|
60
|
+
# spawn a child for each function
|
|
61
|
+
children = []
|
|
62
|
+
for arg, (i, f) in zip(arg_iter, enumerated_funcs):
|
|
63
|
+
child_pid = self._process_in_fork(i, f, result_q, (arg,), kwargs)
|
|
64
|
+
children.append(child_pid)
|
|
65
|
+
|
|
66
|
+
# iterate over any remaining functions for which we have no args
|
|
67
|
+
for i, f in enumerated_funcs:
|
|
68
|
+
child_pid = self._process_in_fork(i, f, result_q, (), kwargs)
|
|
69
|
+
children.append(child_pid)
|
|
70
|
+
|
|
71
|
+
# the parent waits for all children to complete
|
|
72
|
+
for pid in children:
|
|
73
|
+
os.waitpid(pid, 0)
|
|
74
|
+
|
|
75
|
+
# then retrieves the results
|
|
76
|
+
results = []
|
|
77
|
+
# iterate over the result q in sorted order
|
|
78
|
+
result_q.put(None)
|
|
79
|
+
for r in sorted(iter(result_q.get, None), key=itemgetter(0)):
|
|
80
|
+
if r.exception:
|
|
81
|
+
raise exceptions.ConcurrentException(
|
|
82
|
+
"Caught exception in child process", location=r.location
|
|
83
|
+
) from pickle.loads(r.exception)
|
|
84
|
+
kwargs["call_state"].data.update(pickle.loads(r.call_state_data))
|
|
85
|
+
results.append(pickle.loads(r.result))
|
|
86
|
+
return self._merge_func(*results)
|
|
87
|
+
|
|
88
|
+
def _get_call_iterators(self, args):
|
|
89
|
+
return self._function_merge._get_call_iterators(args)
|
|
90
|
+
|
|
91
|
+
def _call_function(self, f, args: tuple, kwargs: dict):
|
|
92
|
+
return self._function_merge._call_function(f, args, kwargs)
|
|
93
|
+
|
|
94
|
+
def _process_in_fork(self, idx, func, result_q, args, kwargs):
|
|
95
|
+
"""Call self._call_function in a child process. This function returns the ID of the child
|
|
96
|
+
in the parent process, while the child process calls _call_function, puts the results in
|
|
97
|
+
the provided queues, then exits.
|
|
98
|
+
"""
|
|
99
|
+
pid = os.fork()
|
|
100
|
+
if pid:
|
|
101
|
+
return pid
|
|
102
|
+
|
|
103
|
+
# here we are the child
|
|
104
|
+
make_result = functools.partial(
|
|
105
|
+
_ConcurrentResult,
|
|
106
|
+
result=None,
|
|
107
|
+
exception=None,
|
|
108
|
+
index=idx,
|
|
109
|
+
call_state_data=None,
|
|
110
|
+
location="",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
result = None
|
|
114
|
+
try:
|
|
115
|
+
r = self._call_function(func, args, kwargs)
|
|
116
|
+
|
|
117
|
+
# pickle here, so that we can't crash with pickle errors in the finally clause
|
|
118
|
+
pickled_r = pickle.dumps(r)
|
|
119
|
+
data = pickle.dumps(kwargs["call_state"].data)
|
|
120
|
+
result = make_result(result=pickled_r, call_state_data=data)
|
|
121
|
+
except Exception as e: # noqa: BLE001
|
|
122
|
+
try:
|
|
123
|
+
# In case func does something stupid like raising an unpicklable exception
|
|
124
|
+
pickled_exception = pickle.dumps(e)
|
|
125
|
+
except (AttributeError, pickle.PicklingError): # fmt: skip
|
|
126
|
+
# Python < 3.14 raises AttributeError for local classes; 3.14+ raises PicklingError
|
|
127
|
+
pickled_exception = pickle.dumps(
|
|
128
|
+
AttributeError(f"Unplicklable exception raised in {func}")
|
|
129
|
+
)
|
|
130
|
+
result = make_result(
|
|
131
|
+
exception=pickled_exception,
|
|
132
|
+
location=kwargs["call_state"].highlight_active_function(),
|
|
133
|
+
)
|
|
134
|
+
finally:
|
|
135
|
+
result_q.put(result)
|
|
136
|
+
# it's necessary to explicitly close the result_q and join its background thread here,
|
|
137
|
+
# because the below os._exit won't allow time for any cleanup.
|
|
138
|
+
result_q.close()
|
|
139
|
+
result_q.join_thread()
|
|
140
|
+
|
|
141
|
+
# This is the one place that the python docs say it's normal to use os._exit. Because
|
|
142
|
+
# this is executed in a child process, calling sys.exit can have unintended
|
|
143
|
+
# consequences. e.g., anything above this that catches the resulting SystemExit can
|
|
144
|
+
# cause the child process to stay alive. the unittest framework does this.
|
|
145
|
+
os._exit(0)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Internal decorators that are applied to MetaFunction methods, not functions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from functools import wraps
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def binary_operation(method):
|
|
10
|
+
"""Internal decorator to apply common type checking for binary operations"""
|
|
11
|
+
|
|
12
|
+
@wraps(method)
|
|
13
|
+
def new_operation(self, other):
|
|
14
|
+
if isinstance(other, Callable):
|
|
15
|
+
new_other = self.make_meta(other)
|
|
16
|
+
else:
|
|
17
|
+
new_other = self.defer_value(other)
|
|
18
|
+
return method(self, new_other)
|
|
19
|
+
|
|
20
|
+
return new_operation
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def manage_call_state(call_method):
|
|
24
|
+
"""Decorates the call method to insure call_state is present in kwargs, or create a new one.
|
|
25
|
+
If the call state isn't active, we assume we are the meta entry point.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
@wraps(call_method)
|
|
29
|
+
def with_call_state(self, *args, **kwargs):
|
|
30
|
+
try:
|
|
31
|
+
call_state = kwargs["call_state"]
|
|
32
|
+
except KeyError:
|
|
33
|
+
call_state = self.new_call_state()
|
|
34
|
+
kwargs["call_state"] = call_state
|
|
35
|
+
call_state.push(self)
|
|
36
|
+
r = call_method(self, *args, **kwargs)
|
|
37
|
+
call_state.pop()
|
|
38
|
+
return r
|
|
39
|
+
|
|
40
|
+
return with_call_state
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class MetaFunctionError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CompositionError(MetaFunctionError, TypeError):
|
|
6
|
+
"An exception that occureds when MetaFunctions are composed incorrectly"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CallError(MetaFunctionError, TypeError):
|
|
10
|
+
"An exception that occures when a MetaFunction is called incorrectly"
|
|
11
|
+
|
|
12
|
+
def __init__(self, *args, location: str = ""):
|
|
13
|
+
self.location = location
|
|
14
|
+
super().__init__(*args)
|
|
15
|
+
|
|
16
|
+
def __repr__(self):
|
|
17
|
+
return "{}({}, {})".format(
|
|
18
|
+
self.__class__.__name__,
|
|
19
|
+
self.args,
|
|
20
|
+
f"location='{self.location}'" if self.location else "",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConcurrentException(CallError):
|
|
25
|
+
"Concurrent specific call errors (e.g., things that aren't picklable)"
|
metafunctions/map.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import typing as tp
|
|
3
|
+
|
|
4
|
+
from metafunctions.core.concurrent import FunctionMerge
|
|
5
|
+
from metafunctions.operators import concat
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MergeMap(FunctionMerge):
|
|
9
|
+
def __init__(self, function: tp.Callable, merge_function: tp.Callable = concat):
|
|
10
|
+
"""
|
|
11
|
+
MergeMap is a FunctionMerge with only one function. When called, it behaves like the
|
|
12
|
+
builtin `map` function and calls its function once per item in the iterable(s) it receives.
|
|
13
|
+
"""
|
|
14
|
+
super().__init__(merge_function, (function,))
|
|
15
|
+
|
|
16
|
+
def _get_call_iterators(self, args):
|
|
17
|
+
"""
|
|
18
|
+
Each element in args is an iterable.
|
|
19
|
+
"""
|
|
20
|
+
args_iter = zip(*args)
|
|
21
|
+
|
|
22
|
+
# Note that EVERY element in the func iter will be called, so we need to make sure the
|
|
23
|
+
# length of our iterator is the same as the shortest iterable we received.
|
|
24
|
+
shortest_arg = min(args, key=len)
|
|
25
|
+
func_iter = itertools.repeat(self.functions[0], len(shortest_arg))
|
|
26
|
+
return args_iter, func_iter
|
|
27
|
+
|
|
28
|
+
def _call_function(self, f, args: tuple, kwargs: dict):
|
|
29
|
+
"""In MergeMap, args will be a single element tuple containing the args for this function."""
|
|
30
|
+
return f(*args[0], **kwargs)
|
|
31
|
+
|
|
32
|
+
def __str__(self):
|
|
33
|
+
return f"mmap({self.functions[0]!s})"
|
|
34
|
+
|
|
35
|
+
def __repr__(self):
|
|
36
|
+
return f"{self.__class__.__name__}({self.functions[0]}, merge_function={self._merge_func})"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Extra operators used by MetaFunctions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# These are imported for use elsewhere
|
|
6
|
+
from operator import add # noqa: F401
|
|
7
|
+
from operator import mul # noqa: F401
|
|
8
|
+
from operator import sub # noqa: F401
|
|
9
|
+
from operator import truediv # noqa: F401
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def concat(*args):
|
|
13
|
+
"concat(1, 2, 3) -> (1, 2, 3)"
|
|
14
|
+
return args
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Some simple nodes to use in tests.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from metafunctions.api import node
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@node
|
|
9
|
+
def a(x):
|
|
10
|
+
return x + "a"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@node()
|
|
14
|
+
def b(x):
|
|
15
|
+
return x + "b"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@node()
|
|
19
|
+
def c(x):
|
|
20
|
+
return x + "c"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@node
|
|
24
|
+
def d(x):
|
|
25
|
+
return x + "d"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@node
|
|
29
|
+
def e(x):
|
|
30
|
+
return x + "e"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from metafunctions.api import bind_call_state
|
|
2
|
+
from metafunctions.api import node
|
|
3
|
+
from metafunctions.api import recall
|
|
4
|
+
from metafunctions.api import store
|
|
5
|
+
from metafunctions.core import CallState
|
|
6
|
+
from metafunctions.core import SimpleFunction
|
|
7
|
+
from metafunctions.tests.simple_nodes import *
|
|
8
|
+
from metafunctions.tests.util import BaseTestCase
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestUnit(BaseTestCase):
|
|
12
|
+
def test_bind_call_state(self):
|
|
13
|
+
"""
|
|
14
|
+
If decorated with bind_call_state, the function receives the call state dictionary as its
|
|
15
|
+
first argument.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@node
|
|
19
|
+
@bind_call_state
|
|
20
|
+
def a_(call_state, x):
|
|
21
|
+
self.assertIsInstance(call_state, CallState)
|
|
22
|
+
call_state.data["a"] = "b"
|
|
23
|
+
return x + "a"
|
|
24
|
+
|
|
25
|
+
@node
|
|
26
|
+
@bind_call_state
|
|
27
|
+
def f(call_state, x):
|
|
28
|
+
return x + call_state.data.get("a", "f")
|
|
29
|
+
|
|
30
|
+
self.assertEqual(a("_"), "_a")
|
|
31
|
+
self.assertEqual(f("_"), "_f")
|
|
32
|
+
|
|
33
|
+
cmp = a_ | f
|
|
34
|
+
self.assertEqual(cmp("_"), "_ab")
|
|
35
|
+
cmp = f | a_ | a_ | f + f
|
|
36
|
+
self.assertEqual(cmp("_"), "_faab_faab")
|
|
37
|
+
|
|
38
|
+
def test_node_bracketless(self):
|
|
39
|
+
"""
|
|
40
|
+
I'm allowing the node decorator to be applied without calling because this is how both
|
|
41
|
+
celery and function_pipes work.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
@node
|
|
45
|
+
def a(x):
|
|
46
|
+
return x + "a"
|
|
47
|
+
|
|
48
|
+
@node()
|
|
49
|
+
def b(x):
|
|
50
|
+
return x + "b"
|
|
51
|
+
|
|
52
|
+
self.assertIsInstance(a, SimpleFunction)
|
|
53
|
+
self.assertIsInstance(b, SimpleFunction)
|
|
54
|
+
self.assertEqual((b | a)("_"), "_ba")
|
|
55
|
+
|
|
56
|
+
def test_store(self):
|
|
57
|
+
state = CallState()
|
|
58
|
+
abc = a | b | store("output") | c
|
|
59
|
+
big = (
|
|
60
|
+
a
|
|
61
|
+
| b
|
|
62
|
+
| c + store("ab") + store("ab2")
|
|
63
|
+
| store("abc")
|
|
64
|
+
| recall("ab") + recall("ab2")
|
|
65
|
+
| c + recall("abc")
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self.assertEqual(abc("_", call_state=state), "_abc")
|
|
69
|
+
self.assertEqual(state.data["output"], "_ab")
|
|
70
|
+
self.assertEqual(big("_"), "_ab_abc_abc_ab_ab")
|
|
71
|
+
|
|
72
|
+
def test_recall(self):
|
|
73
|
+
state = a.new_call_state()
|
|
74
|
+
state.data["k"] = "secret"
|
|
75
|
+
|
|
76
|
+
cmp = a + b | store("k") | c + recall("k")
|
|
77
|
+
self.assertEqual(cmp("_"), "_a_bc_a_b")
|
|
78
|
+
|
|
79
|
+
cmp = a + b | store("k") | c + recall("k") | recall("k", from_call_state=state)
|
|
80
|
+
self.assertEqual(cmp("_"), "secret")
|
|
81
|
+
|
|
82
|
+
def test_str_store(self):
|
|
83
|
+
# this should be possible
|
|
84
|
+
self.assertEqual(str(store("key")), "store('key')")
|
|
85
|
+
|
|
86
|
+
def test_str_recall(self):
|
|
87
|
+
self.assertEqual(str(recall("key")), "recall('key')")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from metafunctions.api import node
|
|
2
|
+
from metafunctions.tests.simple_nodes import *
|
|
3
|
+
from metafunctions.tests.util import BaseTestCase
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestUnit(BaseTestCase):
|
|
7
|
+
def test_broadcast(self):
|
|
8
|
+
@node
|
|
9
|
+
def f(*args):
|
|
10
|
+
return args
|
|
11
|
+
|
|
12
|
+
cmp = (a | b) @ f
|
|
13
|
+
self.assertEqual(cmp("_"), ("_", "a", "b"))
|
|
14
|
+
|
|
15
|
+
rcmp = (1, 2, 3) @ f
|
|
16
|
+
self.assertEqual(rcmp(), (1, 2, 3))
|
|
17
|
+
|
|
18
|
+
def test_str_repr(self):
|
|
19
|
+
cmp = a @ b
|
|
20
|
+
self.assertEqual(str(cmp), "(a | star(b))")
|
|
21
|
+
self.assertEqual(repr(cmp), f"FunctionChain{(a, cmp.functions[1])}")
|
|
22
|
+
|
|
23
|
+
cmp = a | a @ b * 5 / 7 | b & b @ a
|
|
24
|
+
self.assertEqual(
|
|
25
|
+
str(cmp), "(a | (((a | star(b)) * 5) / 7) | (b & (b | star(a))))"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
cmp = (1, 2, 3) @ a
|
|
29
|
+
self.assertEqual(str(cmp), "((1, 2, 3) | star(a))")
|
|
30
|
+
|
|
31
|
+
cmp = a @ (b & c)
|
|
32
|
+
self.assertEqual(str(cmp), "(a | star(b & c))")
|
|
33
|
+
self.assertEqual(
|
|
34
|
+
repr(cmp),
|
|
35
|
+
f"FunctionChain({a!r}, SimpleFunction({cmp._functions[1]._function}))",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def test_upgrade_merge(self):
|
|
39
|
+
aabbcc = (a & b & c) @ (a & b & c)
|
|
40
|
+
self.assertEqual(aabbcc("_"), ("_aa", "_bb", "_cc"))
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
from metafunctions.api import bind_call_state
|
|
2
|
+
from metafunctions.api import locate_error
|
|
3
|
+
from metafunctions.api import mmap
|
|
4
|
+
from metafunctions.api import node
|
|
5
|
+
from metafunctions.core import CallState
|
|
6
|
+
from metafunctions.tests.simple_nodes import *
|
|
7
|
+
from metafunctions.tests.util import BaseTestCase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestUnit(BaseTestCase):
|
|
11
|
+
def test_bind_call_state(self):
|
|
12
|
+
# Bound functions receive call state
|
|
13
|
+
@node
|
|
14
|
+
@bind_call_state
|
|
15
|
+
def f(call_state, x):
|
|
16
|
+
self.assertIsInstance(call_state, CallState)
|
|
17
|
+
call_state.data["h"] = "b"
|
|
18
|
+
return x + "f"
|
|
19
|
+
|
|
20
|
+
@node
|
|
21
|
+
def g(x):
|
|
22
|
+
return x + "g"
|
|
23
|
+
|
|
24
|
+
@node
|
|
25
|
+
@bind_call_state
|
|
26
|
+
def h(call_state, x):
|
|
27
|
+
self.assertIsInstance(call_state, CallState)
|
|
28
|
+
return x + call_state.data["h"]
|
|
29
|
+
|
|
30
|
+
fg = f | g | h
|
|
31
|
+
self.assertEqual(fg("_"), "_fgb")
|
|
32
|
+
|
|
33
|
+
def test_provide_call_state(self):
|
|
34
|
+
# A call state you provide is passed to all functions
|
|
35
|
+
@node
|
|
36
|
+
@bind_call_state
|
|
37
|
+
def f(call_state, x):
|
|
38
|
+
self.assertIs(call_state, c)
|
|
39
|
+
call_state.data["h"] = "d"
|
|
40
|
+
return x + "f"
|
|
41
|
+
|
|
42
|
+
@node
|
|
43
|
+
def g(x):
|
|
44
|
+
return x + "g"
|
|
45
|
+
|
|
46
|
+
@node
|
|
47
|
+
@bind_call_state
|
|
48
|
+
def h(call_state, x):
|
|
49
|
+
self.assertIs(call_state, c)
|
|
50
|
+
return x + call_state.data["h"]
|
|
51
|
+
|
|
52
|
+
fg = f | g | h
|
|
53
|
+
|
|
54
|
+
c = CallState()
|
|
55
|
+
self.assertEqual(fg("_", call_state=c), "_fgd")
|
|
56
|
+
|
|
57
|
+
def test_add_remove(self):
|
|
58
|
+
# imagine these are functions
|
|
59
|
+
a, b, c, d = ["a", "b", "c", "d"]
|
|
60
|
+
Node = CallState.Node
|
|
61
|
+
|
|
62
|
+
tree = CallState()
|
|
63
|
+
tree.push(a)
|
|
64
|
+
tree.push(a)
|
|
65
|
+
self.assertEqual(tree.pop(), a)
|
|
66
|
+
self.assertDictEqual(tree._parents, {})
|
|
67
|
+
|
|
68
|
+
tree.push(b)
|
|
69
|
+
tree.push(a)
|
|
70
|
+
self.assertDictEqual(
|
|
71
|
+
tree._parents, {Node(b, 2): Node(a, 0), Node(a, 3): Node(b, 2)}
|
|
72
|
+
)
|
|
73
|
+
self.assertDictEqual(
|
|
74
|
+
tree._children,
|
|
75
|
+
{
|
|
76
|
+
Node(a, 0): [Node(a, 1), Node(b, 2)],
|
|
77
|
+
Node(b, 2): [Node(a, 3)],
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
tree.push(b)
|
|
82
|
+
self.assertDictEqual(
|
|
83
|
+
tree._children,
|
|
84
|
+
{
|
|
85
|
+
Node(a, 0): [Node(a, 1), Node(b, 2)],
|
|
86
|
+
Node(b, 2): [Node(a, 3)],
|
|
87
|
+
Node(a, 3): [Node(b, 4)],
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
self.assertDictEqual(
|
|
91
|
+
tree._parents,
|
|
92
|
+
{
|
|
93
|
+
Node(b, 4): Node(a, 3),
|
|
94
|
+
Node(a, 3): Node(b, 2),
|
|
95
|
+
Node(b, 2): Node(a, 0),
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
self.assertEqual(tree.pop(), b)
|
|
100
|
+
self.assertEqual(tree.pop(), a)
|
|
101
|
+
for f in a, b, c, d:
|
|
102
|
+
tree.push(f)
|
|
103
|
+
tree.pop()
|
|
104
|
+
self.assertDictEqual(
|
|
105
|
+
tree._children,
|
|
106
|
+
{
|
|
107
|
+
Node(a, 0): [Node(a, 1), Node(b, 2)],
|
|
108
|
+
Node(b, 2): [
|
|
109
|
+
Node(a, 3),
|
|
110
|
+
Node(a, 5),
|
|
111
|
+
Node(b, 6),
|
|
112
|
+
Node(c, 7),
|
|
113
|
+
Node(d, 8),
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
self.assertDictEqual(
|
|
118
|
+
tree._parents,
|
|
119
|
+
{
|
|
120
|
+
Node(b, 2): Node(a, 0),
|
|
121
|
+
},
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def test_iter_parent_nodes(self):
|
|
125
|
+
@node
|
|
126
|
+
def l(*args):
|
|
127
|
+
return []
|
|
128
|
+
|
|
129
|
+
@node
|
|
130
|
+
@bind_call_state
|
|
131
|
+
def return_parent_nodes(cs, *args):
|
|
132
|
+
return list(cs.iter_parent_nodes(cs.active_node))
|
|
133
|
+
|
|
134
|
+
def nodes2str(nodes):
|
|
135
|
+
return [(str(p.function), p.insert_index) for p in nodes]
|
|
136
|
+
|
|
137
|
+
def get_parents(f):
|
|
138
|
+
cs = CallState()
|
|
139
|
+
return nodes2str(f("", call_state=cs))
|
|
140
|
+
|
|
141
|
+
self.assertEqual(return_parent_nodes(), [(return_parent_nodes, 0)])
|
|
142
|
+
|
|
143
|
+
# create some crazy compositions to get parents out of
|
|
144
|
+
simple_chain = a | b | c | return_parent_nodes
|
|
145
|
+
self.assertListEqual(get_parents(simple_chain), [(str(simple_chain), 0)])
|
|
146
|
+
|
|
147
|
+
merges = l + (l + (l + return_parent_nodes))
|
|
148
|
+
self.assertListEqual(
|
|
149
|
+
get_parents(merges),
|
|
150
|
+
[
|
|
151
|
+
("(l + return_parent_nodes)", 4),
|
|
152
|
+
("(l + (l + return_parent_nodes))", 2),
|
|
153
|
+
("(l + (l + (l + return_parent_nodes)))", 0),
|
|
154
|
+
],
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def test_highlight_active_function(self):
|
|
158
|
+
fmt_index = 7
|
|
159
|
+
|
|
160
|
+
@node
|
|
161
|
+
def ff(x):
|
|
162
|
+
return x + "F"
|
|
163
|
+
|
|
164
|
+
@node
|
|
165
|
+
@bind_call_state
|
|
166
|
+
def f(call_state, x):
|
|
167
|
+
if call_state._nodes_visited == fmt_index:
|
|
168
|
+
location_string = call_state.highlight_active_function()
|
|
169
|
+
self.assertEqual(
|
|
170
|
+
location_string, "(a | b | ff | f | f | ->f<- | f | f)"
|
|
171
|
+
)
|
|
172
|
+
self.assertEqual(x, "_abFff")
|
|
173
|
+
return x + "f"
|
|
174
|
+
|
|
175
|
+
pipe = a | b | ff | f | f | f | f | f
|
|
176
|
+
pipe("_")
|
|
177
|
+
|
|
178
|
+
state = CallState()
|
|
179
|
+
af = a + f
|
|
180
|
+
af("_", call_state=state)
|
|
181
|
+
with self.assertRaises(AttributeError):
|
|
182
|
+
state.highlight_active_function()
|
|
183
|
+
|
|
184
|
+
def test_hightlight_active_function_no_parents(self):
|
|
185
|
+
@node
|
|
186
|
+
@bind_call_state
|
|
187
|
+
def f(cs):
|
|
188
|
+
return cs.highlight_active_function()
|
|
189
|
+
|
|
190
|
+
self.assertEqual(f(), "->f<-")
|
|
191
|
+
|
|
192
|
+
def test_highlight_active_function_multichar(self):
|
|
193
|
+
# Don't fail on long named functions. This is a regression test
|
|
194
|
+
@node
|
|
195
|
+
def fail(x):
|
|
196
|
+
if not x:
|
|
197
|
+
raise ZeroDivisionError("division by zero")
|
|
198
|
+
return x - 1
|
|
199
|
+
|
|
200
|
+
cmp = fail | fail + a
|
|
201
|
+
color = locate_error(cmp, use_color=True)
|
|
202
|
+
no_color = locate_error(cmp, use_color=False)
|
|
203
|
+
with self.assertRaises(ZeroDivisionError) as e:
|
|
204
|
+
color(1)
|
|
205
|
+
self.assertTrue(
|
|
206
|
+
e.exception.args[0].endswith("(fail | (\x1b[31m->fail<-\x1b[0m + a))")
|
|
207
|
+
)
|
|
208
|
+
with self.assertRaises(ZeroDivisionError) as e:
|
|
209
|
+
no_color(1)
|
|
210
|
+
self.assertTrue(e.exception.args[0].endswith("(fail | (->fail<- + a))"))
|
|
211
|
+
|
|
212
|
+
def test_highlight_with_map(self):
|
|
213
|
+
@node
|
|
214
|
+
def no_fail(x):
|
|
215
|
+
return x
|
|
216
|
+
|
|
217
|
+
@node
|
|
218
|
+
def fail(*args):
|
|
219
|
+
raise ZeroDivisionError("division by zero")
|
|
220
|
+
|
|
221
|
+
cmp = a + b | (c & no_fail & fail)
|
|
222
|
+
mapper = locate_error(("aaaaa", "BBBBB") | mmap(cmp), use_color=False)
|
|
223
|
+
with self.assertRaises(ZeroDivisionError) as e:
|
|
224
|
+
mapper()
|
|
225
|
+
self.assertEqual(
|
|
226
|
+
str(e.exception),
|
|
227
|
+
"division by zero \n\nOccured in the following function: "
|
|
228
|
+
"(('aaaaa', 'BBBBB') | mmap(((a + b) | (c & no_fail & ->fail<-))))",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def test_tricky_highlight(self):
|
|
232
|
+
def wrapper(mf, new_name=None):
|
|
233
|
+
@node(name=new_name or str(mf))
|
|
234
|
+
@bind_call_state
|
|
235
|
+
def meta_wrapper(cs, *args, **kwargs):
|
|
236
|
+
return mf(*args, call_state=cs, **kwargs)
|
|
237
|
+
|
|
238
|
+
return meta_wrapper
|
|
239
|
+
|
|
240
|
+
@node
|
|
241
|
+
@bind_call_state
|
|
242
|
+
def locator_string(cs, *args):
|
|
243
|
+
return cs.highlight_active_function()
|
|
244
|
+
|
|
245
|
+
cmp = a | b + c | locator_string
|
|
246
|
+
|
|
247
|
+
tim = wrapper(cmp, new_name="tim")
|
|
248
|
+
atim = a | tim
|
|
249
|
+
self.assertEqual(str(tim), "tim")
|
|
250
|
+
self.assertEqual(str(atim), "(a | tim)")
|
|
251
|
+
self.assertEqual(atim("_"), "(a | ->tim<-)")
|