Skip to content

Commit 705dea3

Browse files
committed
Implements #314, make index_of() public in graph api, document node connections and let outputs_of() take anything resolvable by index_of().
1 parent 1267b91 commit 705dea3

File tree

7 files changed

+218
-119
lines changed

7 files changed

+218
-119
lines changed

bonobo/__init__.py

Lines changed: 5 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,52 +8,15 @@
88
import sys
99
from pathlib import Path
1010

11+
from bonobo._api import *
1112
from bonobo._api import (
12-
CsvReader,
13-
CsvWriter,
14-
FileReader,
15-
FileWriter,
16-
Filter,
17-
FixedWindow,
18-
Format,
19-
Graph,
20-
JsonReader,
21-
JsonWriter,
22-
LdjsonReader,
23-
LdjsonWriter,
24-
Limit,
25-
MapFields,
26-
OrderFields,
27-
PickleReader,
28-
PickleWriter,
29-
PrettyPrinter,
30-
RateLimited,
31-
Rename,
32-
SetFields,
33-
Tee,
34-
UnpackItems,
35-
__all__,
36-
__doc__,
37-
count,
38-
create_reader,
39-
create_strategy,
40-
create_writer,
41-
get_argument_parser,
42-
get_examples_path,
43-
identity,
44-
inspect,
45-
noop,
46-
open_examples_fs,
47-
open_fs,
48-
parse_args,
49-
run,
13+
CsvReader, CsvWriter, FileReader, FileWriter, Filter, FixedWindow, Format, Graph, JsonReader, JsonWriter,
14+
LdjsonReader, LdjsonWriter, Limit, MapFields, OrderFields, PickleReader, PickleWriter, PrettyPrinter, RateLimited,
15+
Rename, SetFields, Tee, UnpackItems, __all__, __doc__, count, create_reader, create_strategy, create_writer,
16+
get_argument_parser, get_examples_path, identity, inspect, noop, open_examples_fs, open_fs, parse_args, run
5017
)
5118
from bonobo._version import __version__
5219

53-
from bonobo._api import *
54-
from bonobo._api import __all__, __doc__
55-
from bonobo._version import __version__
56-
5720
if sys.version_info < (3, 5):
5821
raise RuntimeError("Python 3.5+ is required to use Bonobo.")
5922

bonobo/contrib/django/commands.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
from types import GeneratorType
33

44
from colorama import Back, Fore, Style
5-
from django.core.management import BaseCommand
6-
from django.core.management.base import OutputWrapper
75
from mondrian import term
86

97
import bonobo
108
from bonobo.plugins.console import ConsoleOutputPlugin
119
from bonobo.util.term import CLEAR_EOL
10+
from django.core.management import BaseCommand
11+
from django.core.management.base import OutputWrapper
1212

1313
from .utils import create_or_update
1414

bonobo/execution/strategies/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@
77
88
"""
99
from bonobo.execution.strategies.executor import (
10-
AsyncThreadPoolExecutorStrategy,
11-
ProcessPoolExecutorStrategy,
12-
ThreadPoolExecutorStrategy,
10+
AsyncThreadPoolExecutorStrategy, ProcessPoolExecutorStrategy, ThreadPoolExecutorStrategy
1311
)
1412
from bonobo.execution.strategies.naive import NaiveStrategy
1513

bonobo/structs/graphs.py

Lines changed: 98 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -59,65 +59,127 @@ def __init__(self, *chain):
5959
self.edges = {BEGIN: set()}
6060
self.named = {}
6161
self.nodes = []
62-
self.add_chain(*chain)
62+
if len(chain):
63+
self.add_chain(*chain)
6364

6465
def __iter__(self):
6566
yield from self.nodes
6667

6768
def __len__(self):
68-
""" Node count.
69+
"""Node count.
6970
"""
7071
return len(self.nodes)
7172

7273
def __getitem__(self, key):
7374
return self.nodes[key]
7475

7576
def get_cursor(self, ref=BEGIN):
76-
return GraphCursor(self, last=self._resolve_index(ref))
77+
return GraphCursor(self, last=self.index_of(ref))
7778

78-
def outputs_of(self, idx, create=False):
79-
""" Get a set of the outputs for a given node index.
79+
def index_of(self, mixed):
8080
"""
81-
if create and not idx in self.edges:
82-
self.edges[idx] = set()
83-
return self.edges[idx]
81+
Find the index based on various strategies for a node, probably an input or output of chain. Supported
82+
inputs are indexes, node values or names.
83+
84+
"""
85+
if mixed is None:
86+
return None
87+
88+
if type(mixed) is int or mixed in self.edges:
89+
return mixed
90+
91+
if isinstance(mixed, str) and mixed in self.named:
92+
return self.named[mixed]
93+
94+
if mixed in self.nodes:
95+
return self.nodes.index(mixed)
8496

85-
def add_node(self, c):
86-
""" Add a node without connections in this graph and returns its index.
97+
raise ValueError("Cannot find node matching {!r}.".format(mixed))
98+
99+
def outputs_of(self, idx_or_node, create=False):
100+
"""Get a set of the outputs for a given node, node index or name.
101+
"""
102+
idx_or_node = self.index_of(idx_or_node)
103+
104+
if create and not idx_or_node in self.edges:
105+
self.edges[idx_or_node] = set()
106+
return self.edges[idx_or_node]
107+
108+
def add_node(self, c, *, _name=None):
109+
"""Add a node without connections in this graph and returns its index.
110+
If _name is specified, name this node (string reference for further usage).
87111
"""
88112
idx = len(self.nodes)
89113
self.edges[idx] = set()
90114
self.nodes.append(c)
115+
116+
if _name:
117+
if _name in self.named:
118+
raise KeyError("Duplicate name {!r} in graph.".format(_name))
119+
self.named[_name] = idx
120+
91121
return idx
92122

93123
def add_chain(self, *nodes, _input=BEGIN, _output=None, _name=None):
94-
""" Add a chain in this graph.
124+
"""Add `nodes` as a chain in this graph.
125+
126+
**Input rules**
127+
128+
* By default, this chain will be connected to `BEGIN`, a.k.a the special node that kickstarts transformations.
129+
* If `_input` is set to `None`, then this chain won't receive any input unless you connect it manually to
130+
something.
131+
* If `_input` is something that can resolve to another node using `index_of` rules, then the chain will
132+
receive the output stream of referenced node.
133+
134+
**Output rules**
135+
136+
* By default, this chain won't send its output anywhere. This is, most of the time, what you want.
137+
* If `_output` is set to something (that can resolve to a node), then the last node in the chain will send its
138+
outputs to the given node. This means you can provide an object, a name, or an index.
139+
140+
**Naming**
141+
142+
* If a `_name` is given, the first node in the chain will be named this way (same effect as providing a `_name`
143+
to add_node).
144+
145+
**Special cases**
146+
147+
* You can use this method to connect two other chains (in fact, two nodes) by not giving any `nodes`, but
148+
still providing values to `_input` and `_output`.
149+
95150
"""
96-
if len(nodes):
97-
_input = self._resolve_index(_input)
98-
_output = self._resolve_index(_output)
99-
_first = None
100-
_last = None
101-
102-
for i, node in enumerate(nodes):
103-
_last = self.add_node(node)
104-
if not i and _name:
105-
if _name in self.named:
106-
raise KeyError("Duplicate name {!r} in graph.".format(_name))
107-
self.named[_name] = _last
108-
if _first is None:
109-
_first = _last
110-
self.outputs_of(_input, create=True).add(_last)
111-
_input = _last
112-
113-
if _output is not None:
114-
self.outputs_of(_input, create=True).add(_output)
115-
116-
if hasattr(self, "_topologcally_sorted_indexes_cache"):
117-
del self._topologcally_sorted_indexes_cache
118-
119-
return GraphRange(self, _first, _last)
120-
return GraphRange(self, None, None)
151+
_input = self.index_of(_input)
152+
_output = self.index_of(_output)
153+
_first = None
154+
_last = None
155+
156+
# Sanity checks.
157+
if not len(nodes):
158+
if _input is None or _output is None:
159+
raise ValueError(
160+
"Using add_chain(...) without nodes is only possible if you provide both _input and _output values."
161+
)
162+
163+
if _name is not None:
164+
raise RuntimeError("Using add_chain(...) without nodes does not allow to use the _name parameter.")
165+
166+
for i, node in enumerate(nodes):
167+
_last = self.add_node(node, _name=_name if not i else None)
168+
169+
if _first is None:
170+
_first = _last
171+
172+
self.outputs_of(_input, create=True).add(_last)
173+
174+
_input = _last
175+
176+
if _output is not None:
177+
self.outputs_of(_input, create=True).add(_output)
178+
179+
if hasattr(self, "_topologcally_sorted_indexes_cache"):
180+
del self._topologcally_sorted_indexes_cache
181+
182+
return GraphRange(self, _first, _last)
121183

122184
def copy(self):
123185
g = Graph()
@@ -191,26 +253,6 @@ def _repr_html_(self):
191253
except (ExecutableNotFound, FileNotFoundError) as exc:
192254
return "<strong>{}</strong>: {}".format(type(exc).__name__, str(exc))
193255

194-
def _resolve_index(self, mixed):
195-
"""
196-
Find the index based on various strategies for a node, probably an input or output of chain. Supported
197-
inputs are indexes, node values or names.
198-
199-
"""
200-
if mixed is None:
201-
return None
202-
203-
if type(mixed) is int or mixed in self.edges:
204-
return mixed
205-
206-
if isinstance(mixed, str) and mixed in self.named:
207-
return self.named[mixed]
208-
209-
if mixed in self.nodes:
210-
return self.nodes.index(mixed)
211-
212-
raise ValueError("Cannot find node matching {!r}.".format(mixed))
213-
214256

215257
def _get_graphviz_node_id(graph, i):
216258
escaped_index = str(i)

bonobo/util/__init__.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,7 @@
66
from bonobo.util.collections import cast, ensure_tuple, sortedlist, tuplize
77
from bonobo.util.compat import deprecated, deprecated_alias
88
from bonobo.util.inspect import (
9-
inspect_node,
10-
isconfigurable,
11-
isconfigurabletype,
12-
iscontextprocessor,
13-
isdict,
14-
ismethod,
15-
isoption,
16-
istuple,
17-
istype,
9+
inspect_node, isconfigurable, isconfigurabletype, iscontextprocessor, isdict, ismethod, isoption, istuple, istype
1810
)
1911
from bonobo.util.objects import ValueHolder, get_attribute_or_create, get_name
2012

docs/guide/graphs.rst

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -213,24 +213,22 @@ Named nodes
213213

214214
Using above code to create convergences often leads to code which is hard to read, because you have to define the "target" stream
215215
before the streams that logically goes to the beginning of the transformation graph. To overcome that, one can use
216-
"named" nodes:
216+
"named" nodes.
217217

218-
graph.add_chain(x, y, z, _name='zed')
219-
graph.add_chain(f, g, h, _input='zed')
218+
Please note that naming a chain is exactly the same thing as naming the first node of a chain.
220219

221220
.. code-block:: python
222221
223222
import bonobo
224223
225224
graph = bonobo.Graph()
226225
227-
# Add two different chains
228-
graph.add_chain(a, b, _output="load")
229-
graph.add_chain(f, g, _output="load")
230-
231226
# Here we mark _input to None, so normalize won't get the "begin" impulsion.
232227
graph.add_chain(normalize, store, _input=None, _name="load")
233228
229+
# Add two different chains that will output to the "load" node
230+
graph.add_chain(a, b, _output="load")
231+
graph.add_chain(f, g, _output="load")
234232
235233
Resulting graph:
236234

@@ -249,6 +247,43 @@ Resulting graph:
249247
"normalize (load)" -> "store"
250248
}
251249

250+
You can also create single nodes, and the api provide the same capability on single nodes.
251+
252+
.. code-block:: python
253+
254+
import bonobo
255+
256+
graph = bonobo.Graph()
257+
258+
# Create a node without any connection, name it.
259+
graph.add_node(foo, _name="foo")
260+
261+
# Use it somewhere else as the data source.
262+
graph.add_chain(..., _input="foo")
263+
264+
# ... or as the data sink.
265+
graph.add_chain(..., _output="foo")
266+
267+
268+
Connecting two nodes
269+
::::::::::::::::::::
270+
271+
You may want to connect two nodes at some point. You can use `add_chain` without nodes to achieve it.
272+
273+
.. code-block:: python
274+
275+
import bonobo
276+
277+
graph = bonobo.Graph()
278+
279+
# Create two "anonymous" nodes
280+
graph.add_node(a)
281+
graph.add_node(b)
282+
283+
# Connect them
284+
graph.add_chain(_input=a, _output=b)
285+
286+
252287
253288
Inspecting graphs
254289
:::::::::::::::::

0 commit comments

Comments
 (0)