computation-graph 66__tar.gz → 68__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 (39) hide show
  1. {computation_graph-66 → computation_graph-68}/PKG-INFO +1 -1
  2. {computation_graph-66 → computation_graph-68}/computation_graph/base_types.py +42 -20
  3. {computation_graph-66 → computation_graph-68}/computation_graph/composers/__init__.py +177 -87
  4. {computation_graph-66 → computation_graph-68}/computation_graph/composers/composers_test.py +24 -2
  5. computation_graph-68/computation_graph/composers/duplication.py +154 -0
  6. {computation_graph-66 → computation_graph-68}/computation_graph/graph.py +148 -41
  7. {computation_graph-66 → computation_graph-68}/computation_graph/graph_runners.py +17 -19
  8. {computation_graph-66 → computation_graph-68}/computation_graph/graph_test.py +230 -126
  9. computation_graph-68/computation_graph/infer_sink.py +48 -0
  10. computation_graph-68/computation_graph/infer_sink_test.py +403 -0
  11. {computation_graph-66 → computation_graph-68}/computation_graph/legacy.py +6 -2
  12. {computation_graph-66 → computation_graph-68}/computation_graph/run.py +30 -18
  13. {computation_graph-66 → computation_graph-68}/computation_graph/trace/graphviz.py +5 -3
  14. {computation_graph-66 → computation_graph-68}/computation_graph.egg-info/PKG-INFO +1 -1
  15. {computation_graph-66 → computation_graph-68}/computation_graph.egg-info/SOURCES.txt +2 -0
  16. {computation_graph-66 → computation_graph-68}/setup.py +1 -1
  17. computation_graph-66/computation_graph/composers/duplication.py +0 -108
  18. {computation_graph-66 → computation_graph-68}/LICENSE.md +0 -0
  19. {computation_graph-66 → computation_graph-68}/README.md +0 -0
  20. {computation_graph-66 → computation_graph-68}/computation_graph/__init__.py +0 -0
  21. {computation_graph-66 → computation_graph-68}/computation_graph/composers/condition.py +0 -0
  22. {computation_graph-66 → computation_graph-68}/computation_graph/composers/condition_test.py +0 -0
  23. {computation_graph-66 → computation_graph-68}/computation_graph/composers/debug.py +0 -0
  24. {computation_graph-66 → computation_graph-68}/computation_graph/composers/lift.py +0 -0
  25. {computation_graph-66 → computation_graph-68}/computation_graph/composers/lift_test.py +0 -0
  26. {computation_graph-66 → computation_graph-68}/computation_graph/composers/logic.py +0 -0
  27. {computation_graph-66 → computation_graph-68}/computation_graph/composers/memory.py +0 -0
  28. {computation_graph-66 → computation_graph-68}/computation_graph/composers/memory_test.py +0 -0
  29. {computation_graph-66 → computation_graph-68}/computation_graph/signature.py +0 -0
  30. {computation_graph-66 → computation_graph-68}/computation_graph/trace/__init__.py +0 -0
  31. {computation_graph-66 → computation_graph-68}/computation_graph/trace/ascii.py +0 -0
  32. {computation_graph-66 → computation_graph-68}/computation_graph/trace/graphviz_test.py +0 -0
  33. {computation_graph-66 → computation_graph-68}/computation_graph/trace/mermaid.py +0 -0
  34. {computation_graph-66 → computation_graph-68}/computation_graph/trace/trace_utils.py +0 -0
  35. {computation_graph-66 → computation_graph-68}/computation_graph.egg-info/dependency_links.txt +0 -0
  36. {computation_graph-66 → computation_graph-68}/computation_graph.egg-info/requires.txt +0 -0
  37. {computation_graph-66 → computation_graph-68}/computation_graph.egg-info/top_level.txt +0 -0
  38. {computation_graph-66 → computation_graph-68}/pyproject.toml +0 -0
  39. {computation_graph-66 → computation_graph-68}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: computation-graph
3
- Version: 66
3
+ Version: 68
4
4
  Requires-Python: >=3
5
5
  Description-Content-Type: text/markdown
6
6
  License-File: LICENSE.md
@@ -63,9 +63,17 @@ class ComputationEdge:
63
63
  assert bool(self.args) != bool(
64
64
  self.source
65
65
  ), f"Edge must have a source or args, not both: {self}"
66
+ assert bool(self.args) or isinstance(
67
+ self.source, ComputationNode
68
+ ), f"source must be a ComputationNode {self.source}"
69
+ assert isinstance(
70
+ self.destination, ComputationNode
71
+ ), f"destination must be a ComputationNode {self.destination}"
72
+ assert all(
73
+ isinstance(x, ComputationNode) for x in self.args
74
+ ), f"all args must be ComputationNodes {self.args}"
66
75
  if (
67
76
  not self.args
68
- # TODO(uri): doesn't support `functools.partial`, suggested to drop support for it entirely.
69
77
  and not isinstance(self.source.func, functools.partial)
70
78
  and not isinstance(self.destination.func, functools.partial)
71
79
  ):
@@ -76,7 +84,9 @@ class ComputationEdge:
76
84
 
77
85
  def __repr__(self):
78
86
  source_str = (
79
- "".join(map(str, self.args)) if self.source is None else str(self.source)
87
+ f"[{''.join(map(str, self.args))}]"
88
+ if self.source is None
89
+ else str(self.source)
80
90
  )
81
91
  line = "...." if self.is_future else "----"
82
92
  return source_str + line + self.key + line + ">" + str(self.destination)
@@ -105,7 +115,16 @@ class ComputationNode:
105
115
  return self.computed_hash
106
116
 
107
117
  def __repr__(self):
108
- return self.name
118
+ return f"<CompuationNode {self.name.replace('<', '').replace('>', '')}({','.join(self.signature.kwargs)}) >"
119
+
120
+
121
+ @dataclasses.dataclass(frozen=True)
122
+ class GraphType:
123
+ edges: FrozenSet[ComputationEdge]
124
+ sink: ComputationNode
125
+
126
+ def __post_init__(self):
127
+ assert isinstance(self.edges, frozenset), "edges must be a frozenset"
109
128
 
110
129
 
111
130
  node_implementation = gamla.attrgetter("func")
@@ -125,13 +144,15 @@ def edge_sources(edge: ComputationEdge) -> Tuple[ComputationNode, ...]:
125
144
 
126
145
  is_computation_graph = gamla.alljuxt(
127
146
  # Note that this cannot be set to `GraphType` (due to `is_instance` limitation).
128
- gamla.is_instance((tuple, set, frozenset)),
129
- gamla.len_greater(0),
130
- gamla.allmap(gamla.is_instance(ComputationEdge)),
147
+ gamla.is_instance(GraphType),
148
+ gamla.compose_left(gamla.attrgetter("edges"), gamla.len_greater(0)),
149
+ gamla.compose_left(
150
+ gamla.attrgetter("edges"), gamla.allmap(gamla.is_instance(ComputationEdge))
151
+ ),
131
152
  )
132
153
 
133
154
 
134
- ambiguity_groups = gamla.compose(
155
+ _ambiguity_groups_on_edges = gamla.compose(
135
156
  frozenset,
136
157
  gamla.filter(gamla.len_greater(1)),
137
158
  dict.values,
@@ -139,9 +160,9 @@ ambiguity_groups = gamla.compose(
139
160
  gamla.groupby(gamla.juxt(edge_destination, edge_key, edge_priority)),
140
161
  )
141
162
 
142
- assert_no_unwanted_ambiguity = gamla.side_effect(
163
+ assert_no_unwanted_ambiguity_on_edges = gamla.side_effect(
143
164
  gamla.compose_left(
144
- ambiguity_groups,
165
+ _ambiguity_groups_on_edges,
145
166
  gamla.assert_that_with_message(
146
167
  gamla.wrap_str(
147
168
  "There are multiple edges with the same destination, key and priority in the computation graph!: {}"
@@ -153,23 +174,24 @@ assert_no_unwanted_ambiguity = gamla.side_effect(
153
174
  )
154
175
 
155
176
 
156
- @gamla.side_effect
157
- def _assert_no_unwanted_ambiguity_when_debug_set(graph):
158
- if os.getenv(COMPUTATION_GRAPH_DEBUG_ENV_KEY) is not None:
159
- assert_no_unwanted_ambiguity(graph)
177
+ def is_debug_mode() -> bool:
178
+ return os.getenv(COMPUTATION_GRAPH_DEBUG_ENV_KEY) is not None
160
179
 
161
180
 
162
- def merge_graphs(*graphs):
163
- s = frozenset({})
164
- s = s.union(*graphs)
181
+ @gamla.side_effect
182
+ def assert_no_unwanted_ambiguity_when_debug_set(graph: GraphType):
183
+ if is_debug_mode():
184
+ assert_no_unwanted_ambiguity_on_edges(graph.edges)
185
+
165
186
 
166
- return _assert_no_unwanted_ambiguity_when_debug_set(s)
187
+ # TODO(nitzo): Is this used ? Check if we can re-enable the assertion when in debug / move to merge.
188
+ def merge_edges(*edges: FrozenSet[ComputationEdge]) -> FrozenSet[ComputationEdge]:
189
+ result: FrozenSet[ComputationEdge] = frozenset()
190
+ return result.union(*edges)
167
191
 
168
192
 
169
- # We use a tuple to generate a unique id for each node based on the order of edges.
170
- GraphType = FrozenSet[ComputationEdge]
171
193
  GraphOrCallable = Union[Callable, GraphType]
172
194
  CallableOrNode = Union[Callable, ComputationNode]
173
195
  CallableOrNodeOrGraph = Union[CallableOrNode, GraphType]
174
196
  NodeOrGraph = Union[ComputationNode, GraphType]
175
- EMPTY_GRAPH: GraphType = frozenset()
197
+ EMPTY_GRAPH: GraphType = GraphType(edges=frozenset(), sink=None) # type: ignore[arg-type]
@@ -1,18 +1,20 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any, Callable, Dict, Iterable, Optional, Sequence
3
+ from typing import Any, Callable, Dict, Iterable, Optional, Sequence, Tuple
4
4
 
5
5
  import gamla
6
6
 
7
7
  from computation_graph import base_types, graph, signature
8
+ from computation_graph.base_types import GraphType
9
+ from computation_graph.graph import make_computation_node
8
10
 
9
11
 
10
12
  class _ComputationError:
11
13
  pass
12
14
 
13
15
 
14
- _callable_or_graph_type_to_node_or_graph_type = gamla.unless(
15
- gamla.is_instance((tuple, set, frozenset)), graph.make_computation_node
16
+ _to_computation_node_if_callable = gamla.unless(
17
+ gamla.is_instance(base_types.GraphType), graph.make_computation_node
16
18
  )
17
19
 
18
20
 
@@ -47,37 +49,52 @@ def make_and(
47
49
  return args
48
50
 
49
51
  merge_node = graph.make_computation_node(args_to_tuple)
52
+ # `merge_fn` may be a callable/node or a whole graph; `make_compose` handles
53
+ # both, connecting `merge_node`'s output into `merge_fn` under key "args".
54
+ merge_fn_graph = make_compose(merge_fn, merge_node, key="args")
50
55
 
51
56
  return gamla.sync.pipe(
52
57
  funcs,
53
- gamla.sync.map(_callable_or_graph_type_to_node_or_graph_type),
58
+ gamla.sync.map(_to_computation_node_if_callable),
54
59
  tuple,
55
60
  gamla.sync.juxtcat(
56
- gamla.sync.map(_get_edges_from_node_or_graph),
61
+ gamla.sync.filter(gamla.is_instance(base_types.GraphType)),
57
62
  gamla.sync.compose_left(
58
- gamla.sync.map(_infer_sink),
63
+ gamla.sync.map(
64
+ lambda node_or_graph: (
65
+ node_or_graph.sink
66
+ if isinstance(node_or_graph, base_types.GraphType)
67
+ else node_or_graph
68
+ )
69
+ ),
59
70
  tuple,
60
- lambda nodes: (
61
- (
62
- base_types.ComputationEdge(
63
- is_future=False,
64
- priority=0,
65
- source=None,
66
- args=nodes,
67
- destination=merge_node,
68
- key="*args",
69
- ),
71
+ lambda nodes: base_types.GraphType(
72
+ frozenset(
73
+ [
74
+ base_types.ComputationEdge(
75
+ is_future=False,
76
+ priority=0,
77
+ source=None,
78
+ args=nodes,
79
+ destination=merge_node,
80
+ key="*args",
81
+ )
82
+ ]
70
83
  ),
71
- make_compose(merge_fn, merge_node, key="args"),
84
+ merge_node,
72
85
  ),
86
+ gamla.wrap_tuple,
73
87
  ),
74
88
  ),
75
- gamla.sync.star(base_types.merge_graphs),
89
+ tuple,
90
+ lambda graphs: graph.merge_graphs(
91
+ *graphs, merge_fn_graph, sink_node_or_graph=merge_fn_graph
92
+ ),
76
93
  )
77
94
 
78
95
 
79
96
  def make_or(
80
- funcs: Sequence[base_types.CallableOrNodeOrGraph],
97
+ nodes_or_graphs: Sequence[base_types.CallableOrNodeOrGraph],
81
98
  merge_fn: base_types.CallableOrNodeOrGraph,
82
99
  ) -> base_types.GraphType:
83
100
  """Aggregate funcs' output into `merge_fn`.
@@ -91,63 +108,44 @@ def make_or(
91
108
  )
92
109
 
93
110
  filter_node = graph.make_computation_node(filter_computation_errors)
111
+ # `merge_fn` may be a callable/node or a whole graph; `make_compose` handles
112
+ # both, connecting `filter_node`'s output into `merge_fn` under key "args".
113
+ merge_fn_graph = make_compose(merge_fn, filter_node, key="args")
94
114
 
95
115
  return gamla.sync.pipe(
96
- funcs,
116
+ nodes_or_graphs,
97
117
  gamla.sync.map(make_optional(default_value=_ComputationError())),
98
118
  tuple,
99
- gamla.sync.pair_right(
119
+ gamla.juxtcat(
120
+ gamla.identity,
100
121
  gamla.sync.compose_left(
101
- gamla.sync.map(_infer_sink),
122
+ gamla.sync.map(gamla.attrgetter("sink")),
102
123
  tuple,
103
- lambda sinks: (
104
- (
105
- base_types.ComputationEdge(
106
- is_future=False,
107
- priority=0,
108
- source=None,
109
- args=sinks,
110
- destination=filter_node,
111
- key="*args",
112
- ),
124
+ lambda sinks: GraphType(
125
+ frozenset(
126
+ [
127
+ base_types.ComputationEdge(
128
+ is_future=False,
129
+ priority=0,
130
+ source=None,
131
+ args=sinks,
132
+ destination=filter_node,
133
+ key="*args",
134
+ )
135
+ ]
113
136
  ),
114
- make_compose(merge_fn, filter_node, key="args"),
137
+ filter_node,
115
138
  ),
116
- )
139
+ gamla.wrap_tuple,
140
+ ),
141
+ ),
142
+ tuple,
143
+ lambda graphs: graph.merge_graphs(
144
+ *graphs, merge_fn_graph, sink_node_or_graph=merge_fn_graph
117
145
  ),
118
- gamla.concat,
119
- gamla.sync.star(base_types.merge_graphs),
120
146
  )
121
147
 
122
148
 
123
- _destinations = gamla.compose(set, gamla.map(base_types.edge_destination))
124
-
125
-
126
- def _infer_sink(graph_or_node: base_types.NodeOrGraph) -> base_types.ComputationNode:
127
- if isinstance(graph_or_node, base_types.ComputationNode):
128
- return graph_or_node
129
- graph_without_future_edges = graph.remove_future_edges(graph_or_node)
130
- if graph_without_future_edges:
131
- try:
132
- return graph.sink_excluding_terminals(graph_without_future_edges)
133
- except AssertionError:
134
- # If we reached here we can try again without sources of future edges.
135
- sources_of_future_edges = gamla.sync.pipe(
136
- graph_or_node,
137
- gamla.sync.filter(base_types.edge_is_future),
138
- gamla.sync.map(base_types.edge_source),
139
- frozenset,
140
- )
141
- result = (
142
- graph.get_leaves(graph_without_future_edges) - sources_of_future_edges
143
- )
144
- assert len(result) == 1
145
- return gamla.head(result)
146
-
147
- assert len(_destinations(graph_or_node)) == 1, graph_or_node
148
- return base_types.edge_destination(gamla.head(graph_or_node))
149
-
150
-
151
149
  def make_first(*graphs: base_types.CallableOrNodeOrGraph) -> base_types.GraphType:
152
150
  """Returns a graph that when run, returns the first value that doesn't raise an exception.
153
151
  >>> def raise_some_exception():
@@ -156,22 +154,34 @@ def make_first(*graphs: base_types.CallableOrNodeOrGraph) -> base_types.GraphTyp
156
154
  (raise_exception----constituent_of_first---->first_sink, just----constituent_of_first---->first_sink)
157
155
  Will return 1.
158
156
  """
159
- graph_or_nodes = tuple(map(_callable_or_graph_type_to_node_or_graph_type, graphs))
157
+ graph_or_nodes = tuple(map(_to_computation_node_if_callable, graphs))
160
158
 
161
159
  def first_sink(constituent_of_first):
162
160
  return constituent_of_first
163
161
 
164
- return base_types.merge_graphs(
165
- *map(_get_edges_from_node_or_graph, graph_or_nodes),
162
+ sink = graph.make_computation_node(first_sink)
163
+
164
+ return graph.merge_graphs(
165
+ *gamla.pipe(
166
+ graph_or_nodes,
167
+ gamla.map(_get_edges_from_node_or_graph),
168
+ gamla.filter(gamla.is_instance(GraphType)),
169
+ ),
166
170
  gamla.pipe(
167
171
  graph_or_nodes,
168
- gamla.map(_infer_sink),
172
+ gamla.map(
173
+ lambda graph_or_node: (
174
+ graph_or_node.sink
175
+ if isinstance(graph_or_node, GraphType)
176
+ else graph_or_node
177
+ )
178
+ ),
169
179
  enumerate,
170
180
  gamla.map(
171
181
  gamla.star(
172
182
  lambda i, g: base_types.ComputationEdge(
173
183
  source=g,
174
- destination=graph.make_computation_node(first_sink),
184
+ destination=sink,
175
185
  key="constituent_of_first",
176
186
  args=(),
177
187
  priority=i,
@@ -179,8 +189,10 @@ def make_first(*graphs: base_types.CallableOrNodeOrGraph) -> base_types.GraphTyp
179
189
  )
180
190
  )
181
191
  ),
182
- tuple,
192
+ frozenset,
193
+ lambda edges: GraphType(edges, sink),
183
194
  ),
195
+ sink_node_or_graph=sink,
184
196
  )
185
197
 
186
198
 
@@ -188,6 +200,39 @@ def last(*args) -> base_types.GraphType:
188
200
  return make_first(*reversed(args))
189
201
 
190
202
 
203
+ def _determine_sink(
204
+ source: base_types.NodeOrGraph, destination: base_types.NodeOrGraph, is_future: bool
205
+ ) -> base_types.ComputationNode:
206
+ if is_future:
207
+ if isinstance(source, base_types.GraphType):
208
+ return source.sink
209
+ return (
210
+ destination.sink
211
+ if isinstance(destination, base_types.GraphType)
212
+ else destination
213
+ )
214
+
215
+ if isinstance(destination, base_types.GraphType):
216
+ return destination.sink
217
+
218
+ if destination.is_terminal:
219
+ return source.sink if isinstance(source, base_types.GraphType) else source
220
+ return destination
221
+
222
+
223
+ def _determine_composition_sink(
224
+ graphs: Tuple[base_types.NodeOrGraph, ...]
225
+ ) -> base_types.NodeOrGraph:
226
+ for graph_or_node in reversed(graphs):
227
+ if isinstance(graph_or_node, base_types.GraphType):
228
+ return graph_or_node.sink
229
+
230
+ if graph_or_node.is_terminal:
231
+ continue
232
+
233
+ raise AssertionError("Could not compose, failed to. find a sink")
234
+
235
+
191
236
  @gamla.curry
192
237
  def _try_connect(
193
238
  source: base_types.ComputationNode,
@@ -220,20 +265,31 @@ def _infer_composition_edges(
220
265
  source: base_types.NodeOrGraph,
221
266
  destination: base_types.NodeOrGraph,
222
267
  ) -> base_types.GraphType:
223
- try_connect = _try_connect(_infer_sink(source), key, priority, is_future)
268
+
269
+ try_connect = _try_connect(
270
+ source.sink if isinstance(source, GraphType) else source,
271
+ key,
272
+ priority,
273
+ is_future,
274
+ )
224
275
 
225
276
  if isinstance(destination, base_types.ComputationNode):
226
- return base_types.merge_graphs(
227
- (try_connect(destination, destination.signature),),
277
+ sink = _determine_sink(source, destination, is_future)
278
+ return graph.merge_graphs(
279
+ base_types.GraphType(
280
+ edges=frozenset([try_connect(destination, destination.signature)]),
281
+ sink=sink,
282
+ ),
228
283
  _get_edges_from_node_or_graph(source),
284
+ sink_node_or_graph=sink,
229
285
  )
230
-
231
286
  unbound_signature = graph.unbound_signature(
232
- graph.get_incoming_edges_for_node(destination)
287
+ graph.get_incoming_edges_for_node(destination.edges)
233
288
  )
234
- return base_types.merge_graphs(
289
+ return graph.merge_graphs(
235
290
  gamla.sync.pipe(
236
291
  destination,
292
+ gamla.attrgetter("edges"),
237
293
  gamla.sync.mapcat(graph.get_edge_nodes),
238
294
  gamla.unique,
239
295
  gamla.sync.filter(
@@ -246,24 +302,31 @@ def _infer_composition_edges(
246
302
  # Do not add edges to nodes from source that are already present in destination (cycle).
247
303
  gamla.sync.filter(
248
304
  lambda node: isinstance(source, base_types.ComputationNode)
249
- or node not in graph.get_all_nodes(source)
305
+ or node
306
+ not in graph.get_all_nodes(
307
+ source.edges if isinstance(source, GraphType) else source
308
+ )
250
309
  ),
251
310
  gamla.sync.map(
252
- lambda destination: try_connect(
253
- destination=destination,
254
- unbound_destination_signature=unbound_signature(destination),
311
+ lambda destination_node: try_connect(
312
+ destination=destination_node,
313
+ unbound_destination_signature=unbound_signature(destination_node),
255
314
  )
256
315
  ),
257
- tuple,
316
+ frozenset,
258
317
  gamla.sync.check(
259
318
  gamla.identity,
260
319
  AssertionError(
261
320
  f"Cannot compose, destination signature does not contain key '{key}'"
262
321
  ),
263
322
  ),
323
+ # Sink is a placeholder here; `merge_graphs` below sets the real sink
324
+ # via `sink_node_or_graph`.
325
+ lambda edges: GraphType(edges, None), # type: ignore[arg-type]
264
326
  ),
265
- destination,
266
327
  _get_edges_from_node_or_graph(source),
328
+ destination,
329
+ sink_node_or_graph=_determine_sink(source, destination, is_future),
267
330
  )
268
331
 
269
332
 
@@ -279,10 +342,13 @@ def _make_compose_inner(
279
342
  return gamla.sync.pipe(
280
343
  funcs,
281
344
  reversed,
282
- gamla.sync.map(_callable_or_graph_type_to_node_or_graph_type),
345
+ gamla.sync.map(_to_computation_node_if_callable),
283
346
  gamla.sliding_window(2),
284
347
  gamla.sync.map(gamla.star(_infer_composition_edges(priority, key, is_future))),
285
- gamla.sync.star(base_types.merge_graphs),
348
+ tuple,
349
+ lambda graphs: graph.merge_graphs(
350
+ *graphs, sink_node_or_graph=_determine_composition_sink(graphs)
351
+ ),
286
352
  )
287
353
 
288
354
 
@@ -309,11 +375,24 @@ def make_compose_future(
309
375
  def when_memory_unavailable():
310
376
  return default
311
377
 
312
- return base_types.merge_graphs(
378
+ if not isinstance(destination, base_types.GraphType):
379
+ destination = make_computation_node(destination)
380
+
381
+ if not isinstance(source, base_types.GraphType):
382
+ source = make_computation_node(source)
383
+
384
+ if isinstance(source, base_types.GraphType):
385
+ sink_node_or_graph = source.sink
386
+ elif isinstance(destination, base_types.GraphType):
387
+ sink_node_or_graph = destination.sink
388
+ else:
389
+ sink_node_or_graph = destination
390
+ return graph.merge_graphs(
313
391
  _make_compose_inner(destination, source, key=key, is_future=True, priority=0),
314
392
  _make_compose_inner(
315
393
  destination, when_memory_unavailable, key=key, is_future=False, priority=1
316
394
  ),
395
+ sink_node_or_graph=sink_node_or_graph,
317
396
  )
318
397
 
319
398
 
@@ -384,11 +463,22 @@ def compose_dict(
384
463
  >>> compose_dict(gamla.between, {"low": gamla.just(0), "high": gamla.just(10)})
385
464
  (just----low---->between, just----high---->between)
386
465
  """
466
+
467
+ destination = f if base_types.is_computation_graph(f) else make_computation_node(f) # type: ignore
468
+ sink = (
469
+ destination.sink # type: ignore
470
+ if base_types.is_computation_graph(destination)
471
+ else destination
472
+ )
473
+
387
474
  return gamla.pipe(
388
475
  d,
389
476
  dict.items,
390
- gamla.sync.map(gamla.star(lambda key, fn: make_compose(f, fn, key=key))),
391
- gamla.sync.star(base_types.merge_graphs),
477
+ gamla.sync.map(
478
+ gamla.star(lambda key, fn: make_compose(destination, fn, key=key))
479
+ ),
480
+ tuple,
481
+ lambda graphs: graph.merge_graphs(*graphs, sink_node_or_graph=sink), # type: ignore
392
482
  ) or compose_left_unary(f, lambda x: x)
393
483
 
394
484
 
@@ -1,5 +1,5 @@
1
1
  import computation_graph.graph
2
- from computation_graph import base_types, composers, graph_runners
2
+ from computation_graph import composers, graph, graph_runners
3
3
 
4
4
 
5
5
  def test_unary_composition_with_graph_destination():
@@ -30,14 +30,36 @@ def test_infer_sink_edge_case_all_future_edges_with_single_destination():
30
30
  def c_b(c, b):
31
31
  return b
32
32
 
33
- two_future_edges_single_dest = base_types.merge_graphs(
33
+ two_future_edges_single_dest = graph.merge_graphs(
34
34
  composers.compose_left_source(s, "c", c_b),
35
35
  composers.compose_left_source(s, "b", c_b),
36
+ sink_node_or_graph=graph.make_computation_node(c_b),
36
37
  )
37
38
 
38
39
  composers.compose_left_unary(two_future_edges_single_dest, lambda x: x)
39
40
 
40
41
 
42
+ def test_compose_dict():
43
+ def add(a, b):
44
+ return a + b
45
+
46
+ result = graph_runners.nullary_infer_sink(
47
+ composers.compose_dict(add, {"a": lambda: 3, "b": lambda: 4})
48
+ )
49
+ assert result == 7
50
+
51
+
52
+ def test_compose_dict_with_graph_destination():
53
+ def add(a, b):
54
+ return a + b
55
+
56
+ destination_graph = composers.compose_left(lambda: 3, add, key="a")
57
+ result = graph_runners.nullary_infer_sink(
58
+ composers.compose_dict(destination_graph, {"b": lambda: 4})
59
+ )
60
+ assert result == 7
61
+
62
+
41
63
  def test_ambiguity_does_not_blow_up():
42
64
  counter = 0
43
65