Skip to content

Commit 307a3d9

Browse files
committed
REFACT: MOVE CLASSES & RENAME MODULES
+ REFACT: restructure modules: base+functional into -> base+nodes + enh(net): don't import plot early; clean up forgotten imports + ENH(plot): import `pydot` early to detect missing depndency.
1 parent 7cfd55b commit 307a3d9

File tree

10 files changed

+264
-258
lines changed

10 files changed

+264
-258
lines changed

docs/source/reference.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Module: `base`
1717
Module: `operations`
1818
====================
1919

20-
.. automodule:: graphtik.operations
20+
.. automodule:: graphtik.nodes
2121
:members:
2222
:undoc-members:
2323

@@ -29,7 +29,7 @@ Module: `network`
2929
:undoc-members:
3030

3131
Module: `plot`
32-
==============
32+
===============
3333

3434
.. automodule:: graphtik.plot
3535
:members:

graphtik/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,5 @@
1010
__summary__ = __doc__.splitlines()[0]
1111
__uri__ = "https://github.com/pygraphkit/graphtik"
1212

13-
from .operations import operation, compose
13+
from .nodes import operation, compose
1414
from .modifiers import * # noqa, on purpose to include any new modifiers
15-

graphtik/base.py

Lines changed: 125 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11
# Copyright 2016, Yahoo Inc.
22
# Licensed under the terms of the Apache License, Version 2.0. See the LICENSE file associated with the project for terms.
3+
import abc
34
import logging
45
from collections import namedtuple
56

6-
try:
7-
from collections import abc
8-
except ImportError:
9-
import collections as abc
10-
11-
from . import plot
12-
13-
147
log = logging.getLogger(__name__)
158

9+
from .modifiers import optional
10+
1611

1712
def aslist(i):
1813
"""utility used through out"""
@@ -124,7 +119,7 @@ def jetsam(ex, locs, *salvage_vars: str, annotation="jetsam", **salvage_mappings
124119
raise # noqa #re-raise without ex-arg, not to insert my frame
125120

126121

127-
class Operation(object):
122+
class Operation(abc.ABC):
128123
"""An abstract class representing a data transformation by :meth:`.compute()`."""
129124

130125
def __init__(self, name=None, needs=None, provides=None, **kwargs):
@@ -167,6 +162,7 @@ def __hash__(self):
167162
"""
168163
return hash(self.name)
169164

165+
@abc.abstractmethod
170166
def compute(self, named_inputs, outputs=None):
171167
"""
172168
Compute from a given set of inputs an optional set of outputs.
@@ -179,11 +175,9 @@ def compute(self, named_inputs, outputs=None):
179175
the results of running the feed-forward computation on
180176
``inputs``.
181177
"""
182-
raise NotImplementedError("Abstract %r! cannot compute()!" % self)
178+
pass
183179

184180
def _validate(self):
185-
from .modifiers import optional
186-
187181
if not self.name:
188182
raise ValueError(f"Operation needs a name, got: {self.name}")
189183

@@ -230,108 +224,129 @@ def __repr__(self):
230224
return f"{clsname}(name={self.name!r}, needs={needs!r}, provides={provides!r})"
231225

232226

233-
class NetworkOperation(Operation, plot.Plotter):
234-
#: The execution_plan of the last call to compute(), cached as debugging aid.
235-
last_plan = None
236-
#: set execution mode to single-threaded sequential by default
237-
method = "sequential"
238-
overwrites_collector = None
239-
240-
def __init__(self, net, method="sequential", overwrites_collector=None, **kwargs):
241-
"""
242-
:param method:
243-
if ``"parallel"``, launches multi-threading.
244-
Set when invoking a composed graph or by
245-
:meth:`~NetworkOperation.set_execution_method()`.
246-
247-
:param overwrites_collector:
248-
(optional) a mutable dict to be fillwed with named values.
249-
If missing, values are simply discarded.
250-
251-
"""
252-
self.net = net
253-
Operation.__init__(self, **kwargs)
254-
self.set_execution_method(method)
255-
self.set_overwrites_collector(overwrites_collector)
227+
class Plotter(abc.ABC):
228+
"""
229+
Classes wishing to plot their graphs should inherit this and ...
256230
257-
def _build_pydot(self, **kws):
258-
"""delegate to network"""
259-
kws.setdefault("title", self.name)
260-
plotter = self.last_plan or self.net
261-
return plotter._build_pydot(**kws)
231+
implement property ``plot`` to return a "partial" callable that somehow
232+
ends up calling :func:`plot.render_pydot()` with the `graph` or any other
233+
args binded appropriately.
234+
The purpose is to avoid copying this function & documentation here around.
235+
"""
262236

263-
def compute(self, named_inputs, outputs=None):
237+
def plot(self, filename=None, show=False, **kws):
264238
"""
265-
Solve & execute the graph, sequentially or parallel.
266-
267-
:param dict named_inputs:
268-
A maping of names --> values that must contain at least
269-
the compulsory inputs that were specified when the plan was built
270-
(but cannot enforce that!).
271-
Cloned, not modified.
272-
239+
:param str filename:
240+
Write diagram into a file.
241+
Common extensions are ``.png .dot .jpg .jpeg .pdf .svg``
242+
call :func:`plot.supported_plot_formats()` for more.
243+
:param show:
244+
If it evaluates to true, opens the diagram in a matplotlib window.
245+
If it equals `-1`, it plots but does not open the Window.
246+
:param inputs:
247+
an optional name list, any nodes in there are plotted
248+
as a "house"
273249
:param outputs:
274-
a string or a list of strings with all data asked to compute.
275-
If you set this variable to ``None``, all data nodes will be kept
276-
and returned at runtime.
277-
278-
:returns: a dictionary of output data objects, keyed by name.
279-
"""
280-
try:
281-
if isinstance(outputs, str):
282-
outputs = [outputs]
283-
elif not isinstance(outputs, (list, tuple)) and outputs is not None:
284-
raise ValueError(
285-
"The outputs argument must be a list or None, was: %s", outputs
286-
)
287-
288-
net = self.net
289-
290-
# Build the execution plan.
291-
self.last_plan = plan = net.compile(named_inputs.keys(), outputs)
292-
293-
solution = plan.execute(
294-
named_inputs, self.overwrites_collector, self.execution_method
295-
)
296-
297-
return solution
298-
except Exception as ex:
299-
jetsam(ex, locals(), "plan", "solution", "outputs", network="net")
300-
301-
def __call__(
302-
self, named_inputs, outputs=None, method=None, overwrites_collector=None
303-
):
304-
return self.compute(named_inputs, outputs=outputs)
305-
306-
def set_execution_method(self, method):
307-
"""
308-
Determine how the network will be executed.
309-
310-
:param str method:
311-
If "parallel", execute graph operations concurrently
312-
using a threadpool.
250+
an optional name list, any nodes in there are plotted
251+
as an "inverted-house"
252+
:param solution:
253+
an optional dict with values to annotate nodes, drawn "filled"
254+
(currently content not shown, but node drawn as "filled")
255+
:param executed:
256+
an optional container with operations executed, drawn "filled"
257+
:param title:
258+
an optional string to display at the bottom of the graph
259+
:param node_props:
260+
an optional nested dict of Grapvhiz attributes for certain nodes
261+
:param edge_props:
262+
an optional nested dict of Grapvhiz attributes for certain edges
263+
:param clusters:
264+
an optional mapping of nodes --> cluster-names, to group them
265+
266+
:return:
267+
A ``pydot.Dot`` instance.
268+
NOTE that the returned instance is monkeypatched to support
269+
direct rendering in *jupyter cells* as SVG.
270+
271+
272+
Note that the `graph` argument is absent - Each Plotter provides
273+
its own graph internally; use directly :func:`render_pydot()` to provide
274+
a different graph.
275+
276+
.. image:: images/GraphtikLegend.svg
277+
:alt: Graphtik Legend
278+
279+
*NODES:*
280+
281+
oval
282+
function
283+
egg
284+
subgraph operation
285+
house
286+
given input
287+
inversed-house
288+
asked output
289+
polygon
290+
given both as input & asked as output (what?)
291+
square
292+
intermediate data, neither given nor asked.
293+
red frame
294+
evict-instruction, to free up memory.
295+
blue frame
296+
pinned-instruction, not to overwrite intermediate inputs.
297+
filled
298+
data node has a value in `solution` OR function has been executed.
299+
thick frame
300+
function/data node in execution `steps`.
301+
302+
*ARROWS*
303+
304+
solid black arrows
305+
dependencies (source-data *need*-ed by target-operations,
306+
sources-operations *provides* target-data)
307+
dashed black arrows
308+
optional needs
309+
blue arrows
310+
sideffect needs/provides
311+
wheat arrows
312+
broken dependency (``provide``) during pruning
313+
green-dotted arrows
314+
execution steps labeled in succession
315+
316+
317+
To generate the **legend**, see :func:`legend()`.
318+
319+
**Sample code:**
320+
321+
>>> from graphtik import compose, operation
322+
>>> from graphtik.modifiers import optional
323+
>>> from operator import add
324+
325+
>>> graphop = compose(name="graphop")(
326+
... operation(name="add", needs=["a", "b1"], provides=["ab1"])(add),
327+
... operation(name="sub", needs=["a", optional("b2")], provides=["ab2"])(lambda a, b=1: a-b),
328+
... operation(name="abb", needs=["ab1", "ab2"], provides=["asked"])(add),
329+
... )
330+
331+
>>> graphop.plot(show=True); # plot just the graph in a matplotlib window # doctest: +SKIP
332+
>>> inputs = {'a': 1, 'b1': 2}
333+
>>> solution = graphop(inputs) # now plots will include the execution-plan
334+
335+
>>> graphop.plot('plot1.svg', inputs=inputs, outputs=['asked', 'b1'], solution=solution); # doctest: +SKIP
336+
>>> dot = graphop.plot(solution=solution); # just get the `pydoit.Dot` object, renderable in Jupyter
337+
>>> print(dot)
338+
digraph G {
339+
fontname=italic;
340+
label=graphop;
341+
a [fillcolor=wheat, shape=invhouse, style=filled];
342+
...
343+
...
313344
"""
314-
choices = ["parallel", "sequential"]
315-
if method not in choices:
316-
raise ValueError(
317-
"Invalid computation method %r! Must be one of %s" % (method, choices)
318-
)
319-
self.execution_method = method
320-
321-
def set_overwrites_collector(self, collector):
322-
"""
323-
Asks to put all *overwrites* into the `collector` after computing
345+
from .plot import render_pydot
324346

325-
An "overwrites" is intermediate value calculated but NOT stored
326-
into the results, becaues it has been given also as an intemediate
327-
input value, and the operation that would overwrite it MUST run for
328-
its other results.
347+
dot = self._build_pydot(**kws)
348+
return render_pydot(dot, filename=filename, show=show)
329349

330-
:param collector:
331-
a mutable dict to be fillwed with named values
332-
"""
333-
if collector is not None and not isinstance(collector, abc.MutableMapping):
334-
raise ValueError(
335-
"Overwrites collector was not a MutableMapping, but: %r" % collector
336-
)
337-
self.overwrites_collector = collector
350+
@abc.abstractmethod
351+
def _build_pydot(self, **kws):
352+
pass

graphtik/network.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,15 @@
6565
(aka "pinned") input-values.
6666
"""
6767
import logging
68-
import os
6968
import sys
7069
import time
7170
from collections import defaultdict, namedtuple
72-
from io import StringIO
73-
from itertools import chain
7471

7572
import networkx as nx
7673
from boltons.setutils import IndexedSet as iset
7774
from networkx import DiGraph
7875

79-
from . import plot
80-
from .base import Operation, jetsam
76+
from .base import Operation, Plotter, jetsam
8177
from .modifiers import optional, sideffect
8278

8379
log = logging.getLogger(__name__)
@@ -142,7 +138,7 @@ def __repr__(self):
142138

143139

144140
class ExecutionPlan(
145-
namedtuple("_ExePlan", "net inputs outputs dag broken_edges steps"), plot.Plotter
141+
namedtuple("_ExePlan", "net inputs outputs dag broken_edges steps"), Plotter
146142
):
147143
"""
148144
The result of the network's compilation phase.
@@ -391,7 +387,7 @@ def execute(self, named_inputs, overwrites=None, method=None):
391387
jetsam(ex, locals(), "solution", "executed")
392388

393389

394-
class Network(plot.Plotter):
390+
class Network(Plotter):
395391
"""
396392
Assemble operations & data into a directed-acyclic-graph (DAG) to run them.
397393

0 commit comments

Comments
 (0)