computation-graph 54__tar.gz → 57__tar.gz

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.
Files changed (36) hide show
  1. {computation-graph-54 → computation_graph-57}/PKG-INFO +1 -1
  2. {computation-graph-54 → computation_graph-57}/computation_graph/base_types.py +5 -1
  3. {computation-graph-54 → computation_graph-57}/computation_graph/composers/duplication.py +2 -1
  4. {computation-graph-54 → computation_graph-57}/computation_graph/graph.py +1 -1
  5. {computation-graph-54 → computation_graph-57}/computation_graph/graph_test.py +49 -0
  6. computation_graph-57/computation_graph/run.py +577 -0
  7. {computation-graph-54 → computation_graph-57}/computation_graph.egg-info/PKG-INFO +1 -1
  8. {computation-graph-54 → computation_graph-57}/computation_graph.egg-info/top_level.txt +1 -0
  9. {computation-graph-54 → computation_graph-57}/setup.py +1 -1
  10. computation-graph-54/computation_graph/run.py +0 -427
  11. {computation-graph-54 → computation_graph-57}/LICENSE.md +0 -0
  12. {computation-graph-54 → computation_graph-57}/README.md +0 -0
  13. {computation-graph-54 → computation_graph-57}/computation_graph/__init__.py +0 -0
  14. {computation-graph-54 → computation_graph-57}/computation_graph/composers/__init__.py +0 -0
  15. {computation-graph-54 → computation_graph-57}/computation_graph/composers/composers_test.py +0 -0
  16. {computation-graph-54 → computation_graph-57}/computation_graph/composers/condition.py +0 -0
  17. {computation-graph-54 → computation_graph-57}/computation_graph/composers/condition_test.py +0 -0
  18. {computation-graph-54 → computation_graph-57}/computation_graph/composers/debug.py +0 -0
  19. {computation-graph-54 → computation_graph-57}/computation_graph/composers/lift.py +0 -0
  20. {computation-graph-54 → computation_graph-57}/computation_graph/composers/logic.py +0 -0
  21. {computation-graph-54 → computation_graph-57}/computation_graph/composers/memory.py +0 -0
  22. {computation-graph-54 → computation_graph-57}/computation_graph/composers/memory_test.py +0 -0
  23. {computation-graph-54 → computation_graph-57}/computation_graph/graph_runners.py +0 -0
  24. {computation-graph-54 → computation_graph-57}/computation_graph/legacy.py +0 -0
  25. {computation-graph-54 → computation_graph-57}/computation_graph/signature.py +0 -0
  26. {computation-graph-54 → computation_graph-57}/computation_graph/trace/__init__.py +0 -0
  27. {computation-graph-54 → computation_graph-57}/computation_graph/trace/ascii.py +0 -0
  28. {computation-graph-54 → computation_graph-57}/computation_graph/trace/graphviz.py +0 -0
  29. {computation-graph-54 → computation_graph-57}/computation_graph/trace/graphviz_test.py +0 -0
  30. {computation-graph-54 → computation_graph-57}/computation_graph/trace/mermaid.py +0 -0
  31. {computation-graph-54 → computation_graph-57}/computation_graph/trace/trace_utils.py +0 -0
  32. {computation-graph-54 → computation_graph-57}/computation_graph.egg-info/SOURCES.txt +0 -0
  33. {computation-graph-54 → computation_graph-57}/computation_graph.egg-info/dependency_links.txt +0 -0
  34. {computation-graph-54 → computation_graph-57}/computation_graph.egg-info/requires.txt +0 -0
  35. {computation-graph-54 → computation_graph-57}/pyproject.toml +0 -0
  36. {computation-graph-54 → computation_graph-57}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: computation-graph
3
- Version: 54
3
+ Version: 57
4
4
  Requires-Python: >=3
5
5
  Description-Content-Type: text/markdown
6
6
  License-File: LICENSE.md
@@ -96,9 +96,13 @@ class ComputationNode:
96
96
  func: Callable
97
97
  signature: NodeSignature
98
98
  is_terminal: bool
99
+ computed_hash: int = dataclasses.field(init=False)
100
+
101
+ def __post_init__(self):
102
+ object.__setattr__(self, "computed_hash", hash(self.func))
99
103
 
100
104
  def __hash__(self):
101
- return hash(self.func)
105
+ return self.computed_hash
102
106
 
103
107
  def __repr__(self):
104
108
  return self.name
@@ -44,6 +44,7 @@ _duplicate_node = gamla.compose_left(
44
44
  graph.make_computation_node,
45
45
  )
46
46
 
47
+ duplicate_node = _duplicate_node
47
48
 
48
49
  _node_to_duplicated_node = gamla.compose_left(
49
50
  gamla.remove(base_types.node_is_terminal),
@@ -104,4 +105,4 @@ def safe_replace_sources(
104
105
  g = graph.replace_source(source, get_node_replacement(source), g)
105
106
  return g
106
107
 
107
- return gamla.reduce(lambda updated_cg, e: updated_cg + update_edge(e), (), cg)
108
+ return base_types.merge_graphs(*(update_edge(e) for e in cg))
@@ -162,7 +162,7 @@ traverse_forward: Callable[
162
162
  ),
163
163
  gamla.groupby_many_reduce(
164
164
  opt_gamla.compose_left(gamla.head, gamla.wrap_tuple),
165
- lambda destinations, e: (*(destinations if destinations else ()), e[1]),
165
+ lambda destinations, e: {*(destinations if destinations else ()), e[1]},
166
166
  ),
167
167
  gamla.dict_to_getter_with_default(()),
168
168
  gamla.before(make_computation_node),
@@ -63,6 +63,55 @@ def test_simple():
63
63
  )
64
64
 
65
65
 
66
+ async def test_async_run_as_soon_as_possible(capsys):
67
+ # This test is to make sure that the async node runs as soon as possible.
68
+ # nodes in different topological layers should run concurrently if not dependent on each other.
69
+ async def concurrent1(x):
70
+ print("start concurrent1") # noqa
71
+ await asyncio.sleep(0.1)
72
+ print("end concurrent1") # noqa
73
+ return "concurrent1"
74
+
75
+ async def concurrent2(x):
76
+ print("start concurrent2") # noqa
77
+ await asyncio.sleep(0.1)
78
+ print("end concurrent2") # noqa
79
+ return "concurrent2"
80
+
81
+ def sink(x, y):
82
+ return f"x={x}, y={y}"
83
+
84
+ g = base_types.merge_graphs(
85
+ composers.compose_unary(concurrent1, lambda: "x"),
86
+ composers.compose_unary(concurrent2, lambda y: y, lambda: "y"),
87
+ composers.compose_left(concurrent1, sink, key="x"),
88
+ composers.compose_left(concurrent2, sink, key="y"),
89
+ )
90
+
91
+ await graph_runners.nullary(g, sink)
92
+
93
+ # Capture the output
94
+ captured = capsys.readouterr()
95
+
96
+ # Assertions
97
+ assert "start concurrent1" in captured.out
98
+ assert "start concurrent2" in captured.out
99
+ assert "end concurrent1" in captured.out
100
+ assert "end concurrent2" in captured.out
101
+ assert captured.out.index("start concurrent1") < captured.out.index(
102
+ "end concurrent1"
103
+ )
104
+ assert captured.out.index("start concurrent2") < captured.out.index(
105
+ "end concurrent2"
106
+ )
107
+ assert captured.out.index("start concurrent1") < captured.out.index(
108
+ "end concurrent2"
109
+ )
110
+ assert captured.out.index("start concurrent2") < captured.out.index(
111
+ "end concurrent1"
112
+ )
113
+
114
+
66
115
  async def test_simple_async():
67
116
  assert (
68
117
  await graph_runners.unary(
@@ -0,0 +1,577 @@
1
+ import asyncio
2
+ import dataclasses
3
+ import functools
4
+ import inspect
5
+ import itertools
6
+ import logging
7
+ import os
8
+ import time
9
+ import typing
10
+ from typing import (
11
+ Any,
12
+ Awaitable,
13
+ Callable,
14
+ Dict,
15
+ FrozenSet,
16
+ Iterable,
17
+ Mapping,
18
+ Optional,
19
+ Set,
20
+ Tuple,
21
+ Type,
22
+ )
23
+
24
+ import gamla
25
+ import immutables
26
+ import termcolor
27
+ import toposort
28
+ import typeguard
29
+ from gamla.optimized import sync as opt_gamla
30
+
31
+ from computation_graph import base_types, composers, graph, signature
32
+
33
+ CG_NO_RESULT = "CG_NO_RESULT"
34
+
35
+
36
+ class _DepNotFoundError(Exception):
37
+ pass
38
+
39
+
40
+ _NodeToResults = Dict[base_types.ComputationNode, base_types.Result]
41
+ _ComputationInput = Tuple[Tuple[base_types.Result, ...], Dict[str, base_types.Result]]
42
+ _SingleNodeSideEffect = Callable[[base_types.ComputationNode, Any], None]
43
+ _ComputationInputSpec = Tuple[
44
+ Tuple[base_types.ComputationNode, ...], Dict[str, base_types.ComputationNode]
45
+ ]
46
+ _NodeExecutor = Callable[
47
+ [
48
+ Mapping[
49
+ base_types.ComputationNode,
50
+ base_types.Result | Awaitable[base_types.Result],
51
+ ],
52
+ base_types.ComputationNode,
53
+ ],
54
+ base_types.Result,
55
+ ]
56
+
57
+
58
+ def _transpose_graph(
59
+ graph: Dict[base_types.ComputationNode, Set[base_types.ComputationNode]]
60
+ ) -> Dict[base_types.ComputationNode, Set[base_types.ComputationNode]]:
61
+ return opt_gamla.pipe(
62
+ graph, dict.keys, opt_gamla.groupby_many(graph.get), opt_gamla.valmap(set)
63
+ )
64
+
65
+
66
+ _toposort_nodes: Callable[
67
+ [base_types.GraphType], Tuple[FrozenSet[base_types.ComputationNode], ...]
68
+ ] = opt_gamla.compose_left(
69
+ opt_gamla.groupby_many(base_types.edge_sources),
70
+ opt_gamla.valmap(
71
+ opt_gamla.compose_left(opt_gamla.map(base_types.edge_destination), set)
72
+ ),
73
+ _transpose_graph,
74
+ lambda g: toposort.toposort_flatten(g, sort=False),
75
+ )
76
+
77
+
78
+ def _type_check(node: base_types.ComputationNode, result):
79
+ return_typing = typing.get_type_hints(node.func).get("return", None)
80
+ if return_typing:
81
+ try:
82
+ typeguard.check_type(str(node), result, return_typing)
83
+ except TypeError as e:
84
+ logging.error([node.func.__code__, e])
85
+
86
+
87
+ def _profile(node, time_started: float):
88
+ elapsed = time.perf_counter() - time_started
89
+ if elapsed <= 0.1:
90
+ return
91
+ logging.warning(
92
+ termcolor.colored(
93
+ f"function took {elapsed:.6f} seconds: {base_types.pretty_print_function_name(node.func)}",
94
+ color="red",
95
+ )
96
+ )
97
+
98
+
99
+ _group_by_is_async_result = opt_gamla.groupby(lambda k_v: inspect.isawaitable(k_v[1]))
100
+
101
+
102
+ _is_graph_async = opt_gamla.compose_left(
103
+ opt_gamla.mapcat(lambda edge: (edge.source, *edge.args)),
104
+ opt_gamla.remove(gamla.equals(None)),
105
+ opt_gamla.map(base_types.node_implementation),
106
+ gamla.anymap(asyncio.iscoroutinefunction),
107
+ )
108
+
109
+
110
+ def _future_edge_to_regular_edge_with_placeholder(
111
+ source_to_placeholder: dict[base_types.ComputationNode, base_types.ComputationNode]
112
+ ) -> Callable[[base_types.ComputationEdge], base_types.ComputationEdge]:
113
+ def replace_source(edge):
114
+ assert edge.source, "only supports singular edges for now"
115
+
116
+ return dataclasses.replace(
117
+ edge, is_future=False, source=source_to_placeholder[edge.source]
118
+ )
119
+
120
+ return replace_source
121
+
122
+
123
+ _map_future_source_to_placeholder = opt_gamla.compose_left(
124
+ opt_gamla.map(
125
+ opt_gamla.pair_right(
126
+ opt_gamla.compose_left(
127
+ gamla.attrgetter("name"), graph.make_source_with_name
128
+ )
129
+ )
130
+ ),
131
+ gamla.frozendict,
132
+ )
133
+ _graph_to_future_sources = opt_gamla.compose_left(
134
+ opt_gamla.filter(base_types.edge_is_future),
135
+ # We assume that future edges cannot be with multiple sources
136
+ opt_gamla.map(base_types.edge_source),
137
+ frozenset,
138
+ )
139
+
140
+
141
+ """Replace multiple edges pointing to a terminal with one edge that has multiple args"""
142
+
143
+
144
+ def _merge_edges_pointing_to_terminals(g: base_types.GraphType) -> base_types.GraphType:
145
+ return gamla.compose_left(
146
+ gamla.groupby(gamla.attrgetter("destination")),
147
+ gamla.itemmap(
148
+ gamla.star(
149
+ lambda dest, edges_for_dest: (
150
+ dest,
151
+ base_types.merge_graphs(
152
+ composers.make_or(
153
+ opt_gamla.maptuple(base_types.edge_source)(edges_for_dest),
154
+ merge_fn=(aggregate := lambda args: args),
155
+ ),
156
+ composers.compose_left_unary(aggregate, dest),
157
+ )
158
+ if dest.is_terminal
159
+ else edges_for_dest,
160
+ )
161
+ )
162
+ ),
163
+ dict.values,
164
+ gamla.concat,
165
+ tuple,
166
+ )(g)
167
+
168
+
169
+ def _to_callable_with_side_effect_for_single_and_multiple(
170
+ single_node_side_effect: _SingleNodeSideEffect,
171
+ all_nodes_side_effect: Callable,
172
+ edges: base_types.GraphType,
173
+ handled_exceptions: Tuple[Type[Exception], ...],
174
+ ) -> Callable[[_NodeToResults, _NodeToResults], _NodeToResults]:
175
+ edges = _merge_edges_pointing_to_terminals(edges)
176
+ single_node_side_effect = (
177
+ (lambda node, result: result)
178
+ if os.getenv(base_types.COMPUTATION_GRAPH_DEBUG_ENV_KEY) is None
179
+ else single_node_side_effect
180
+ )
181
+
182
+ future_sources = _graph_to_future_sources(edges)
183
+ future_source_to_placeholder = _map_future_source_to_placeholder(future_sources)
184
+ edges = gamla.pipe(
185
+ edges,
186
+ gamla.unique,
187
+ opt_gamla.map(
188
+ opt_gamla.when(
189
+ base_types.edge_is_future,
190
+ _future_edge_to_regular_edge_with_placeholder(
191
+ future_source_to_placeholder
192
+ ),
193
+ )
194
+ ),
195
+ tuple,
196
+ gamla.side_effect(_assert_composition_is_valid),
197
+ gamla.side_effect(base_types.assert_no_unwanted_ambiguity),
198
+ )
199
+ is_async = _is_graph_async(edges)
200
+ placeholder_to_future_source = opt_gamla.pipe(
201
+ future_source_to_placeholder, gamla.itemmap(lambda k_v: (k_v[1], k_v[0]))
202
+ )
203
+ get_node_executor = _make_get_node_executor(
204
+ edges, handled_exceptions, single_node_side_effect
205
+ )
206
+
207
+ topological_layers = opt_gamla.pipe(
208
+ edges,
209
+ _toposort_nodes,
210
+ gamla.remove(gamla.contains(placeholder_to_future_source)),
211
+ opt_gamla.maptuple(opt_gamla.pair_right(get_node_executor)),
212
+ )
213
+
214
+ translate_source_to_placeholder = opt_gamla.compose_left(
215
+ opt_gamla.keyfilter(gamla.contains(future_sources)),
216
+ opt_gamla.keymap(future_source_to_placeholder.__getitem__),
217
+ )
218
+ all_node_side_effects_on_edges = gamla.side_effect(all_nodes_side_effect(edges))
219
+
220
+ if is_async:
221
+
222
+ async def final_runner(sources_to_values):
223
+ d = gamla.pipe(
224
+ topological_layers,
225
+ _run_graph(
226
+ translate_source_to_placeholder(sources_to_values),
227
+ handled_exceptions,
228
+ ),
229
+ dict,
230
+ )
231
+ results_by_is_async = _group_by_is_async_result(d.items())
232
+ async_results = tuple(zip(*results_by_is_async.get(True, ())))
233
+ sync_results = dict(results_by_is_async.get(False, ()))
234
+ return all_node_side_effects_on_edges(
235
+ sync_results
236
+ | (
237
+ {
238
+ k: v
239
+ for (k, v) in zip(
240
+ async_results[0],
241
+ await asyncio.gather(
242
+ *async_results[1], return_exceptions=True
243
+ ),
244
+ )
245
+ if not isinstance(v, Exception)
246
+ }
247
+ if async_results
248
+ else {}
249
+ )
250
+ )
251
+
252
+ else:
253
+
254
+ def final_runner(sources_to_values):
255
+ return gamla.pipe(
256
+ topological_layers,
257
+ _run_graph(
258
+ translate_source_to_placeholder(sources_to_values),
259
+ handled_exceptions,
260
+ ),
261
+ dict,
262
+ all_node_side_effects_on_edges,
263
+ )
264
+
265
+ return (_async_graph_reducer if is_async else _graph_reducer)(final_runner)
266
+
267
+
268
+ _get_args_nodes: Callable[
269
+ [Tuple[base_types.ComputationEdge, ...]], Tuple[base_types.ComputationNode, ...]
270
+ ] = gamla.compose_left(
271
+ opt_gamla.filter(base_types.edge_args), gamla.head, base_types.edge_args
272
+ )
273
+ _get_kwargs_nodes = opt_gamla.compose_left(
274
+ opt_gamla.filter(
275
+ gamla.compose_left(base_types.edge_key, gamla.not_equals("*args"))
276
+ ),
277
+ gamla.map(gamla.juxt(base_types.edge_key, base_types.edge_source)),
278
+ gamla.frozendict,
279
+ )
280
+
281
+
282
+ def _node_incoming_edges_to_input_spec(
283
+ node_incoming_edges: Tuple[base_types.ComputationEdge],
284
+ ) -> _ComputationInputSpec:
285
+ if not len(node_incoming_edges):
286
+ return (), {}
287
+ first_incoming_edge = gamla.head(node_incoming_edges)
288
+ node = base_types.edge_destination(first_incoming_edge)
289
+ if node.signature.is_kwargs:
290
+ return (base_types.edge_source(first_incoming_edge),), {}
291
+ return (
292
+ _get_args_nodes(node_incoming_edges) if node.signature.is_args else (),
293
+ _get_kwargs_nodes(node_incoming_edges),
294
+ )
295
+
296
+
297
+ def _to_awaitable(v) -> Awaitable:
298
+ if inspect.isawaitable(v):
299
+ return v
300
+ f = asyncio.get_event_loop().create_future()
301
+ f.set_result(v)
302
+ return f
303
+
304
+
305
+ def _make_get_node_executor(
306
+ edges, handled_exceptions, single_node_side_effect: _SingleNodeSideEffect
307
+ ):
308
+ node_to_incoming_edges = functools.cache(graph.get_incoming_edges_for_node(edges))
309
+ node_to_computation_input_spec_options: Callable[
310
+ [base_types.ComputationNode], Tuple[_ComputationInputSpec]
311
+ ] = functools.cache(
312
+ gamla.compose_left(
313
+ node_to_incoming_edges,
314
+ opt_gamla.groupby(base_types.edge_key),
315
+ opt_gamla.valmap(gamla.sort_by(base_types.edge_priority)),
316
+ dict.values,
317
+ opt_gamla.star(itertools.product),
318
+ opt_gamla.maptuple(_node_incoming_edges_to_input_spec),
319
+ )
320
+ )
321
+
322
+ async def gather(
323
+ args: Tuple[Awaitable, ...], kwargs: Mapping[str, Awaitable]
324
+ ) -> tuple[tuple[Any, ...], dict[str, Any]]:
325
+ if args or kwargs:
326
+ try:
327
+ gathered = await asyncio.gather(*args, *kwargs.values())
328
+ return gathered[: len(args)], dict(
329
+ zip(kwargs.keys(), gathered[len(args) :])
330
+ )
331
+ except (
332
+ _DepNotFoundError,
333
+ base_types.SkipComputationError,
334
+ *handled_exceptions,
335
+ ):
336
+ # We delete the references to the upstream tasks to avoid circular reference (task->exception->traceback->task) and improve memory performance
337
+ del args, kwargs
338
+ raise _DepNotFoundError() from None
339
+ return (), {}
340
+
341
+ def node_to_input_sync(
342
+ accumulated_results: Mapping[base_types.ComputationNode, base_types.Result],
343
+ input_options: Iterable[_ComputationInputSpec],
344
+ ) -> Optional[_ComputationInput]:
345
+ for input_spec in input_options:
346
+ args, kwargs = input_spec
347
+ try:
348
+ return tuple(accumulated_results[arg] for arg in args), {
349
+ k: accumulated_results[v] for k, v in kwargs.items()
350
+ }
351
+ except KeyError:
352
+ ...
353
+ return None
354
+
355
+ async def node_to_input_async(
356
+ accumulated_results: Mapping[
357
+ base_types.ComputationNode, base_types.Result | Awaitable[base_types.Result]
358
+ ],
359
+ input_options: Iterable[_ComputationInputSpec],
360
+ ) -> Optional[_ComputationInput]:
361
+ for input_spec in input_options:
362
+ args_spec, kwargs_spec = input_spec
363
+ if all(
364
+ accumulated_results.get(arg, CG_NO_RESULT) is not CG_NO_RESULT
365
+ for arg in args_spec
366
+ ) and all(
367
+ accumulated_results.get(kwarg, CG_NO_RESULT) is not CG_NO_RESULT
368
+ for kwarg in kwargs_spec.values()
369
+ ):
370
+ try:
371
+ return await gather(
372
+ tuple(_to_awaitable(accumulated_results[a]) for a in args_spec),
373
+ {
374
+ k: _to_awaitable(accumulated_results[v])
375
+ for k, v in kwargs_spec.items()
376
+ },
377
+ )
378
+ except (
379
+ _DepNotFoundError,
380
+ base_types.SkipComputationError,
381
+ *handled_exceptions,
382
+ ):
383
+ ...
384
+ return None
385
+
386
+ @opt_gamla.after(asyncio.create_task)
387
+ async def await_deps_and_apply(
388
+ accumulated_results: Mapping[
389
+ base_types.ComputationNode, base_types.Result | Awaitable[base_types.Result]
390
+ ],
391
+ node: base_types.ComputationNode,
392
+ ) -> base_types.Result:
393
+ args_kwargs = await node_to_input_async(
394
+ accumulated_results, node_to_computation_input_spec_options(node)
395
+ )
396
+ # We delete the references to the upstream tasks to avoid circular reference (task->exception->traceback->task) and improve memory performance
397
+ del accumulated_results
398
+ if args_kwargs is None:
399
+ raise _DepNotFoundError()
400
+
401
+ args, kwargs = args_kwargs
402
+ before = time.perf_counter()
403
+ result = node.func(*args, **kwargs)
404
+ single_node_side_effect(node, result)
405
+ if inspect.isawaitable(result):
406
+ raise Exception(
407
+ f"{node} returned an awaitable result but is not an async function"
408
+ )
409
+ _profile(node, before)
410
+ return result
411
+
412
+ @opt_gamla.after(asyncio.create_task)
413
+ async def await_deps_and_await(
414
+ accumulated_results: Mapping[
415
+ base_types.ComputationNode, base_types.Result | Awaitable[base_types.Result]
416
+ ],
417
+ node: base_types.ComputationNode,
418
+ ) -> base_types.Result:
419
+ args_kwargs = await node_to_input_async(
420
+ accumulated_results, node_to_computation_input_spec_options(node)
421
+ )
422
+ # We delete the references to the upstream tasks to avoid circular reference (task->exception->traceback->task) and improve memory performance
423
+ del accumulated_results
424
+ if args_kwargs is None:
425
+ raise _DepNotFoundError()
426
+
427
+ args, kwargs = args_kwargs
428
+ before = time.perf_counter()
429
+ result = await node.func(*args, **kwargs)
430
+ single_node_side_effect(node, result)
431
+ _profile(node, before)
432
+ return result
433
+
434
+ @opt_gamla.after(asyncio.create_task)
435
+ async def get_deps_and_await(
436
+ accumulated_results: Mapping[base_types.ComputationNode, base_types.Result],
437
+ node: base_types.ComputationNode,
438
+ ) -> base_types.Result:
439
+ args_kwargs = node_to_input_sync(
440
+ accumulated_results, node_to_computation_input_spec_options(node)
441
+ )
442
+ # We delete the references to the upstream tasks to avoid circular reference (task->exception->traceback->task) and improve memory performance
443
+ del accumulated_results
444
+ if args_kwargs is None:
445
+ raise _DepNotFoundError()
446
+
447
+ args, kwargs = args_kwargs
448
+ before = time.perf_counter()
449
+ result = await node.func(*args, **kwargs)
450
+ single_node_side_effect(node, result)
451
+ _profile(node, before)
452
+ return result
453
+
454
+ def get_deps_and_apply(
455
+ accumulated_results: Mapping[base_types.ComputationNode, base_types.Result],
456
+ node: base_types.ComputationNode,
457
+ ) -> base_types.Result:
458
+ args_kwargs = node_to_input_sync(
459
+ accumulated_results, node_to_computation_input_spec_options(node)
460
+ )
461
+ # We delete the references to the upstream tasks to avoid circular reference (task->exception->traceback->task) and improve memory performance
462
+ del accumulated_results
463
+ if args_kwargs is None:
464
+ raise _DepNotFoundError()
465
+
466
+ args, kwargs = args_kwargs
467
+ before = time.perf_counter()
468
+ result = node.func(*args, **kwargs)
469
+ single_node_side_effect(node, result)
470
+ if inspect.isawaitable(result):
471
+ raise Exception(
472
+ f"{node} returned an awaitable result but is not an async function"
473
+ )
474
+ _profile(node, before)
475
+ return result
476
+
477
+ all_nodes = graph.get_all_nodes(edges)
478
+ async_nodes = {n for n in all_nodes if asyncio.iscoroutinefunction(n.func)}
479
+ sync = all_nodes - async_nodes
480
+ tf = graph.traverse_forward(edges)
481
+ downstream_from_async = set(gamla.graph_traverse_many(async_nodes, tf))
482
+
483
+ async_and_downstream = async_nodes & downstream_from_async
484
+ async_not_downstream = async_nodes - downstream_from_async
485
+ sync_and_downstream = sync & downstream_from_async
486
+ sync_not_downstream = sync - downstream_from_async
487
+
488
+ def get_executor(node: base_types.ComputationNode) -> _NodeExecutor:
489
+ if node in async_and_downstream:
490
+ return await_deps_and_await
491
+ if node in async_not_downstream:
492
+ return get_deps_and_await
493
+ if node in sync_and_downstream:
494
+ return await_deps_and_apply
495
+ if node in sync_not_downstream:
496
+ # This is fully sync so it only uses sync results from the mapping, its typing says the whole mapping is sync.
497
+ return get_deps_and_apply # type: ignore
498
+ raise Exception("no executor found")
499
+
500
+ return get_executor
501
+
502
+
503
+ def _run_graph(inputs: dict, handled_exceptions):
504
+ def run_graph(
505
+ nodes: tuple[tuple[base_types.ComputationNode, _NodeExecutor]]
506
+ ) -> _NodeToResults:
507
+ accumulated_results = inputs.copy()
508
+ for node_executor in nodes:
509
+ try:
510
+ accumulated_results[node_executor[0]] = node_executor[1](
511
+ accumulated_results, node_executor[0]
512
+ )
513
+ except (
514
+ _DepNotFoundError,
515
+ base_types.SkipComputationError,
516
+ *handled_exceptions,
517
+ ):
518
+ pass
519
+ return accumulated_results
520
+
521
+ return run_graph
522
+
523
+
524
+ def _graph_reducer(graph_callable):
525
+ def reducer(prev: _NodeToResults, sources: _NodeToResults) -> _NodeToResults:
526
+ return {**prev, **(graph_callable({**prev, **sources}))}
527
+
528
+ return reducer
529
+
530
+
531
+ def _async_graph_reducer(graph_callable):
532
+ async def reducer(prev: _NodeToResults, sources: _NodeToResults) -> _NodeToResults:
533
+ return {**prev, **(await graph_callable({**prev, **sources}))}
534
+
535
+ return reducer
536
+
537
+
538
+ to_callable_with_side_effect = gamla.curry(
539
+ _to_callable_with_side_effect_for_single_and_multiple
540
+ )(_type_check)
541
+
542
+ # Use the second line if you want to see the winning path in the computation graph (a little slower).
543
+ to_callable = to_callable_with_side_effect(gamla.just(gamla.just(None)))
544
+ # to_callable = to_callable_with_side_effect(graphviz.computation_trace('utterance_computation.dot'))
545
+
546
+
547
+ def _node_is_properly_composed(
548
+ node_to_incoming_edges: base_types.GraphType,
549
+ ) -> Callable[[base_types.ComputationNode], bool]:
550
+ return gamla.compose_left(
551
+ graph.unbound_signature(node_to_incoming_edges),
552
+ signature.parameters,
553
+ gamla.len_equals(0),
554
+ )
555
+
556
+
557
+ def _assert_composition_is_valid(g: base_types.GraphType):
558
+ return opt_gamla.pipe(
559
+ g,
560
+ graph.get_all_nodes,
561
+ opt_gamla.remove(
562
+ _node_is_properly_composed(graph.get_incoming_edges_for_node(g))
563
+ ),
564
+ opt_gamla.map(gamla.wrap_str("{0} at {0.func.__code__}")),
565
+ tuple,
566
+ gamla.assert_that_with_message(
567
+ gamla.wrap_str("Bad composition for: {}"), gamla.len_equals(0)
568
+ ),
569
+ )
570
+
571
+
572
+ def to_callable_strict(
573
+ g: base_types.GraphType,
574
+ ) -> Callable[[_NodeToResults, _NodeToResults], _NodeToResults]:
575
+ return gamla.compose(
576
+ gamla.star(to_callable(g, frozenset())), gamla.map(immutables.Map), gamla.pack
577
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: computation-graph
3
- Version: 54
3
+ Version: 57
4
4
  Requires-Python: >=3
5
5
  Description-Content-Type: text/markdown
6
6
  License-File: LICENSE.md
@@ -7,7 +7,7 @@ with open("README.md", "r") as fh:
7
7
  setuptools.setup(
8
8
  name="computation-graph",
9
9
  python_requires=">=3",
10
- version="54",
10
+ version="57",
11
11
  long_description=_LONG_DESCRIPTION,
12
12
  long_description_content_type="text/markdown",
13
13
  packages=setuptools.find_namespace_packages(),
@@ -1,427 +0,0 @@
1
- import asyncio
2
- import dataclasses
3
- import functools
4
- import itertools
5
- import logging
6
- import os
7
- import time
8
- import typing
9
- from typing import Any, Callable, Dict, FrozenSet, Set, Tuple, Type
10
-
11
- import gamla
12
- import immutables
13
- import termcolor
14
- import toposort
15
- import typeguard
16
- from gamla.optimized import async_functions as opt_async_gamla
17
- from gamla.optimized import sync as opt_gamla
18
-
19
- from computation_graph import base_types, composers, graph, signature
20
- from computation_graph.composers import debug
21
-
22
-
23
- class _DepNotFoundError(Exception):
24
- pass
25
-
26
-
27
- _NodeToResults = Dict[base_types.ComputationNode, base_types.Result]
28
- _ComputationInput = Tuple[Tuple[base_types.Result, ...], Dict[str, base_types.Result]]
29
- _SingleNodeSideEffect = Callable[[base_types.ComputationNode, Any], None]
30
- _NodeToComputationInput = Callable[[base_types.ComputationNode], _ComputationInput]
31
- _ComputationInputSpec = Tuple[
32
- Tuple[base_types.ComputationNode, ...], Dict[str, base_types.ComputationNode]
33
- ]
34
-
35
-
36
- def _transpose_graph(
37
- graph: Dict[base_types.ComputationNode, Set[base_types.ComputationNode]]
38
- ) -> Dict[base_types.ComputationNode, Set[base_types.ComputationNode]]:
39
- return opt_gamla.pipe(
40
- graph, dict.keys, opt_gamla.groupby_many(graph.get), opt_gamla.valmap(set)
41
- )
42
-
43
-
44
- _toposort_nodes: Callable[
45
- [base_types.GraphType], Tuple[FrozenSet[base_types.ComputationNode], ...]
46
- ] = opt_gamla.compose_left(
47
- opt_gamla.groupby_many(base_types.edge_sources),
48
- opt_gamla.valmap(
49
- opt_gamla.compose_left(opt_gamla.map(base_types.edge_destination), set)
50
- ),
51
- _transpose_graph,
52
- toposort.toposort,
53
- )
54
-
55
-
56
- def _type_check(node: base_types.ComputationNode, result):
57
- return_typing = typing.get_type_hints(node.func).get("return", None)
58
- if return_typing:
59
- try:
60
- typeguard.check_type(str(node), result, return_typing)
61
- except TypeError as e:
62
- logging.error([node.func.__code__, e])
63
-
64
-
65
- def _profile(node, time_started: float):
66
- elapsed = time.perf_counter() - time_started
67
- if elapsed <= 0.1:
68
- return
69
- logging.warning(
70
- termcolor.colored(
71
- f"function took {elapsed:.6f} seconds: {base_types.pretty_print_function_name(node.func)}",
72
- color="red",
73
- )
74
- )
75
-
76
-
77
- def _make_get_node_input_and_apply(
78
- is_async: bool, side_effect: _SingleNodeSideEffect
79
- ) -> Callable[..., base_types.Result]:
80
- if is_async:
81
-
82
- @opt_async_gamla.star
83
- async def run_node(
84
- node_to_input: _NodeToComputationInput, node: base_types.ComputationNode
85
- ) -> base_types.Result:
86
- args, kwargs = node_to_input(node)
87
- before = time.perf_counter()
88
- result = await gamla.to_awaitable(node.func(*args, **kwargs))
89
- side_effect(node, result)
90
- _profile(node, before)
91
- return node, result
92
-
93
- else:
94
-
95
- @opt_gamla.star
96
- def run_node(node_to_input: _NodeToComputationInput, node: base_types.ComputationNode) -> base_types.Result: # type: ignore
97
- args, kwargs = node_to_input(node)
98
- before = time.perf_counter()
99
- result = node.func(*args, **kwargs)
100
- side_effect(node, result)
101
- _profile(node, before)
102
- return node, result
103
-
104
- return run_node
105
-
106
-
107
- _is_graph_async = opt_gamla.compose_left(
108
- opt_gamla.mapcat(lambda edge: (edge.source, *edge.args)),
109
- opt_gamla.remove(gamla.equals(None)),
110
- opt_gamla.map(base_types.node_implementation),
111
- gamla.anymap(asyncio.iscoroutinefunction),
112
- )
113
-
114
-
115
- def _future_edge_to_regular_edge_with_placeholder(
116
- source_to_placeholder: dict[base_types.ComputationNode, base_types.ComputationNode]
117
- ) -> Callable[[base_types.ComputationEdge], base_types.ComputationEdge]:
118
- def replace_source(edge):
119
- assert edge.source, "only supports singular edges for now"
120
-
121
- return dataclasses.replace(
122
- edge, is_future=False, source=source_to_placeholder[edge.source]
123
- )
124
-
125
- return replace_source
126
-
127
-
128
- _map_future_source_to_placeholder = opt_gamla.compose_left(
129
- opt_gamla.map(
130
- opt_gamla.pair_right(
131
- opt_gamla.compose_left(
132
- gamla.attrgetter("name"), graph.make_source_with_name
133
- )
134
- )
135
- ),
136
- gamla.frozendict,
137
- )
138
- _graph_to_future_sources = opt_gamla.compose_left(
139
- opt_gamla.filter(base_types.edge_is_future),
140
- # We assume that future edges cannot be with multiple sources
141
- opt_gamla.map(base_types.edge_source),
142
- frozenset,
143
- )
144
-
145
-
146
- """Replace multiple edges pointing to a terminal with one edge that has multiple args"""
147
-
148
-
149
- def _merge_edges_pointing_to_terminals(g: base_types.GraphType) -> base_types.GraphType:
150
- return gamla.compose_left(
151
- gamla.groupby(gamla.attrgetter("destination")),
152
- gamla.itemmap(
153
- gamla.star(
154
- lambda dest, edges_for_dest: (
155
- dest,
156
- base_types.merge_graphs(
157
- composers.make_or(
158
- opt_gamla.maptuple(base_types.edge_source)(edges_for_dest),
159
- merge_fn=(aggregate := lambda args: args),
160
- ),
161
- composers.compose_left_unary(aggregate, dest),
162
- )
163
- if dest.is_terminal
164
- else edges_for_dest,
165
- )
166
- )
167
- ),
168
- dict.values,
169
- gamla.concat,
170
- tuple,
171
- )(g)
172
-
173
-
174
- def _to_callable_with_side_effect_for_single_and_multiple(
175
- single_node_side_effect: _SingleNodeSideEffect,
176
- all_nodes_side_effect: Callable,
177
- edges: base_types.GraphType,
178
- handled_exceptions: Tuple[Type[Exception], ...],
179
- ) -> Callable[[_NodeToResults, _NodeToResults], _NodeToResults]:
180
- edges = _merge_edges_pointing_to_terminals(edges)
181
- single_node_side_effect = (
182
- (lambda node, result: result)
183
- if os.getenv(base_types.COMPUTATION_GRAPH_DEBUG_ENV_KEY) is None
184
- else single_node_side_effect
185
- )
186
-
187
- future_sources = _graph_to_future_sources(edges)
188
- future_source_to_placeholder = _map_future_source_to_placeholder(future_sources)
189
- edges = gamla.pipe(
190
- edges,
191
- gamla.unique,
192
- opt_gamla.map(
193
- opt_gamla.when(
194
- base_types.edge_is_future,
195
- _future_edge_to_regular_edge_with_placeholder(
196
- future_source_to_placeholder
197
- ),
198
- )
199
- ),
200
- tuple,
201
- gamla.side_effect(_assert_composition_is_valid),
202
- gamla.side_effect(base_types.assert_no_unwanted_ambiguity),
203
- )
204
- is_async = _is_graph_async(edges)
205
- placeholder_to_future_source = opt_gamla.pipe(
206
- future_source_to_placeholder, gamla.itemmap(lambda k_v: (k_v[1], k_v[0]))
207
- )
208
- topological_layers = opt_gamla.pipe(
209
- edges,
210
- _toposort_nodes,
211
- gamla.map_filter_empty(
212
- gamla.compose_left(
213
- gamla.remove(gamla.contains(placeholder_to_future_source)), frozenset
214
- )
215
- ),
216
- tuple,
217
- )
218
- translate_source_to_placeholder = opt_gamla.compose_left(
219
- opt_gamla.keyfilter(gamla.contains(future_sources)),
220
- opt_gamla.keymap(future_source_to_placeholder.__getitem__),
221
- )
222
- all_node_side_effects_on_edges = all_nodes_side_effect(edges)
223
- reduce_layers = _make_reduce_layers(
224
- edges, handled_exceptions, is_async, single_node_side_effect
225
- )
226
-
227
- if is_async:
228
-
229
- async def final_runner(sources_to_values):
230
- return await gamla.pipe(
231
- topological_layers,
232
- reduce_layers(
233
- immutables.Map(translate_source_to_placeholder(sources_to_values))
234
- ),
235
- dict,
236
- gamla.side_effect(all_node_side_effects_on_edges),
237
- )
238
-
239
- else:
240
-
241
- def final_runner(sources_to_values):
242
- return gamla.pipe(
243
- topological_layers,
244
- reduce_layers(
245
- immutables.Map(translate_source_to_placeholder(sources_to_values))
246
- ),
247
- dict,
248
- gamla.side_effect(all_node_side_effects_on_edges),
249
- )
250
-
251
- return (_async_graph_reducer if is_async else _graph_reducer)(final_runner)
252
-
253
-
254
- _get_args_nodes: Callable[
255
- [Tuple[base_types.ComputationEdge, ...]], Tuple[base_types.ComputationNode, ...]
256
- ] = gamla.compose_left(
257
- opt_gamla.filter(base_types.edge_args), gamla.head, base_types.edge_args
258
- )
259
- _get_kwargs_nodes = opt_gamla.compose_left(
260
- opt_gamla.filter(
261
- gamla.compose_left(base_types.edge_key, gamla.not_equals("*args"))
262
- ),
263
- gamla.map(gamla.juxt(base_types.edge_key, base_types.edge_source)),
264
- gamla.frozendict,
265
- )
266
-
267
-
268
- def _node_incoming_edges_to_input_spec(
269
- node_incoming_edges: Tuple[base_types.ComputationEdge],
270
- ) -> _ComputationInputSpec:
271
- if not len(node_incoming_edges):
272
- return (), {}
273
- first_incoming_edge = gamla.head(node_incoming_edges)
274
- node = base_types.edge_destination(first_incoming_edge)
275
- if node.signature.is_kwargs:
276
- return (base_types.edge_source(first_incoming_edge),), {}
277
- return (
278
- _get_args_nodes(node_incoming_edges) if node.signature.is_args else (),
279
- _get_kwargs_nodes(node_incoming_edges),
280
- )
281
-
282
-
283
- def _edges_to_accumulated_results_to_node_to_first_possible_input(
284
- edges,
285
- ) -> Callable[[_NodeToResults], _NodeToComputationInput]:
286
- node_to_incoming_edges = graph.get_incoming_edges_for_node(edges)
287
- node_to_computation_input_spec_options: Callable[
288
- [base_types.ComputationNode], Tuple[_ComputationInputSpec]
289
- ] = functools.cache(
290
- gamla.compose_left(
291
- node_to_incoming_edges,
292
- opt_gamla.groupby(base_types.edge_key),
293
- opt_gamla.valmap(gamla.sort_by(base_types.edge_priority)),
294
- dict.values,
295
- opt_gamla.star(itertools.product),
296
- opt_gamla.maptuple(_node_incoming_edges_to_input_spec),
297
- )
298
- )
299
- # TODO(eli): Add sync.translate_exception/except that avoids inspect.iscoroutinefunction so this can be inlined (ENG-5212)
300
- head_or_dep_not_found = gamla.translate_exception(
301
- gamla.head, StopIteration, _DepNotFoundError
302
- )
303
-
304
- def make_node_to_first_possible_input(
305
- accumulated_results: _NodeToResults,
306
- ) -> _NodeToComputationInput:
307
- return opt_gamla.compose_left(
308
- node_to_computation_input_spec_options,
309
- opt_gamla.map(
310
- gamla.excepts(
311
- KeyError,
312
- lambda _: None,
313
- opt_gamla.packstack(
314
- opt_gamla.maptuple(accumulated_results.__getitem__),
315
- opt_gamla.valmap(accumulated_results.__getitem__),
316
- ),
317
- )
318
- ),
319
- opt_gamla.remove(gamla.equals(None)),
320
- head_or_dep_not_found,
321
- tuple,
322
- )
323
-
324
- return make_node_to_first_possible_input
325
-
326
-
327
- def _make_reduce_layers(
328
- edges: base_types.GraphType,
329
- handled_exceptions: Tuple[Type[Exception], ...],
330
- is_async: bool,
331
- single_node_side_effect: _SingleNodeSideEffect,
332
- ) -> Callable[[immutables.Map], Callable[[Tuple[FrozenSet, ...]], immutables.Map]]:
333
- get_node_input_and_apply = _make_get_node_input_and_apply(
334
- is_async, single_node_side_effect
335
- )
336
- accumulated_results_to_node_to_input = (
337
- _edges_to_accumulated_results_to_node_to_first_possible_input(edges)
338
- )
339
- single_layer_reducer = debug.name_callable(
340
- gamla.compose_left(
341
- gamla.pack,
342
- gamla.juxt(
343
- gamla.head,
344
- gamla.compose_left(
345
- opt_gamla.packstack(
346
- accumulated_results_to_node_to_input, gamla.identity
347
- ),
348
- gamla.explode(1),
349
- gamla.map_filter_empty(
350
- gamla.first(
351
- get_node_input_and_apply,
352
- gamla.just(None),
353
- exception_type=(
354
- *handled_exceptions,
355
- base_types.SkipComputationError,
356
- _DepNotFoundError,
357
- ),
358
- )
359
- ),
360
- tuple,
361
- ),
362
- ),
363
- opt_gamla.star(
364
- lambda prev_results, current_results: prev_results.update(
365
- current_results
366
- )
367
- ),
368
- ),
369
- "single_layer_reducer",
370
- )
371
- return gamla.curry(gamla.reduce_curried)(single_layer_reducer)
372
-
373
-
374
- def _graph_reducer(graph_callable):
375
- def reducer(prev: _NodeToResults, sources: _NodeToResults) -> _NodeToResults:
376
- return {**prev, **(graph_callable({**prev, **sources}))}
377
-
378
- return reducer
379
-
380
-
381
- def _async_graph_reducer(graph_callable):
382
- async def reducer(prev: _NodeToResults, sources: _NodeToResults) -> _NodeToResults:
383
- return {**prev, **(await graph_callable({**prev, **sources}))}
384
-
385
- return reducer
386
-
387
-
388
- to_callable_with_side_effect = gamla.curry(
389
- _to_callable_with_side_effect_for_single_and_multiple
390
- )(_type_check)
391
-
392
- # Use the second line if you want to see the winning path in the computation graph (a little slower).
393
- to_callable = to_callable_with_side_effect(gamla.just(gamla.just(None)))
394
- # to_callable = to_callable_with_side_effect(graphviz.computation_trace('utterance_computation.dot'))
395
-
396
-
397
- def _node_is_properly_composed(
398
- node_to_incoming_edges: base_types.GraphType,
399
- ) -> Callable[[base_types.ComputationNode], bool]:
400
- return gamla.compose_left(
401
- graph.unbound_signature(node_to_incoming_edges),
402
- signature.parameters,
403
- gamla.len_equals(0),
404
- )
405
-
406
-
407
- def _assert_composition_is_valid(g: base_types.GraphType):
408
- return opt_gamla.pipe(
409
- g,
410
- graph.get_all_nodes,
411
- opt_gamla.remove(
412
- _node_is_properly_composed(graph.get_incoming_edges_for_node(g))
413
- ),
414
- opt_gamla.map(gamla.wrap_str("{0} at {0.func.__code__}")),
415
- tuple,
416
- gamla.assert_that_with_message(
417
- gamla.wrap_str("Bad composition for: {}"), gamla.len_equals(0)
418
- ),
419
- )
420
-
421
-
422
- def to_callable_strict(
423
- g: base_types.GraphType,
424
- ) -> Callable[[_NodeToResults, _NodeToResults], _NodeToResults]:
425
- return gamla.compose(
426
- gamla.star(to_callable(g, frozenset())), gamla.map(immutables.Map), gamla.pack
427
- )
File without changes
File without changes