Skip to content

Commit 700e75c

Browse files
committed
BREAK/FEAT(OP, DOC): Unify OP-BUILDER ...
Specifically the last empty call at the end ``()`` is not needed (or possible):: operation(str, name=...)() became simply like that:: operation(str, name=...) + DOC: review and shorten `operations.rst`. + DROP: `operator` class--> function. + ENH: Fancy Op-decorator supports reasonab;le behavior.
1 parent 3cef25b commit 700e75c

File tree

10 files changed

+366
-376
lines changed

10 files changed

+366
-376
lines changed

CHANGES.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ v6.0.0 (13 Apr 2020, @ankostis): New Plotting Device...
203203

204204
>>> from graphtik import operation, varargs
205205
>>> from graphtik.plot import get_active_plotter
206-
>>> op = operation(print, name='print-something', needs=varargs("any"), provides="str")()
206+
>>> op = operation(print, name='print-something', needs=varargs("any"), provides="str")
207207
>>> dot = op.plot(plotter=get_active_plotter().with_styles(kw_legend=None))
208208

209209
+ ENH: Convey graph, node & edge ("non-private") attributes from the *networkx* graph
@@ -704,7 +704,7 @@ The first non pre-release for 2.x train.
704704
+ break(jetsam): drop "graphtik_` prefix from annotated attribute
705705

706706
+ ENH(op): now ``operation()`` supported the "builder pattern" with
707-
:meth:`.operation.withset()`.
707+
``.operation.withset()`` method.
708708

709709
+ REFACT: renamed internal package `functional --> nodes` and moved classes around,
710710
to break cycles easier, (``base`` works as supposed to), not to import early everything,

docs/source/arch.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@ Architecture
4949
corresponding `network`\s.
5050

5151
.. Tip::
52-
- Use :class:`.operation` builder class to construct
53-
:class:`.FunctionalOperation` instances.
52+
- Use :func:`.operation` factory to construct :class:`.FunctionalOperation` instances.
5453
- Use :func:`~.graphtik.compose()` factory to prepare the `net` internally,
5554
and build :class:`.NetworkOperation` instances.
5655

docs/source/composition.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ Operations with partial outputs (*rescheduled*)
267267
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
268268
In case the actually produce `outputs` depend on some condition in the `inputs`,
269269
the `solution` has to :term:`reschedule` the plan amidst execution, and consider the
270-
actual `provides` delivered.
270+
actual `provides` delivered:
271271

272272

273273
>>> @operation(rescheduled=1,

docs/source/index.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ Compose the ``abspow`` function along with ``mul`` & ``sub`` built-ins
7878
into a computation :term:`graph`:
7979

8080
>>> graphop = compose("graphop",
81-
... operation(needs=["a", "b"], provides=["ab"])(mul),
82-
... operation(sub, needs=["a", "ab"], provides=["a_minus_ab"])(),
81+
... operation(mul, needs=["a", "b"], provides=["ab"]),
82+
... operation(sub, needs=["a", "ab"], provides=["a_minus_ab"]),
8383
... abs_qubed,
8484
... )
8585
>>> graphop

docs/source/operations.rst

Lines changed: 122 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,79 @@
11
Operations
22
==========
33

4-
At a high level, an operation is a node in a computation graph.
5-
Graphtik uses an :class:`.Operation` class to abstractly represent these computations.
6-
The class specifies the *requirements* for a function to participate
7-
in a computation graph; those are its input-data **needs**, and the output-data
8-
it **provides**.
4+
At a high level, an :term:`operation` is a function in a computation :term:`pipeline`,
5+
abstractly represented by the :class:`.Operation` class.
6+
This class specifies the :term:`dependencies <dependency>` of the *operation*
7+
in the *pipeline*.
98

10-
The :class:`.FunctionalOperation` provides a lightweight wrapper
11-
around an arbitrary function to define those specifications.
9+
The :class:`.FunctionalOperation` provides a concrete lightweight wrapper
10+
around any arbitrary function to define those *dependencies*.
11+
Instead of constructing it directly, prefer to instantiate it by calling
12+
the :func:`.operation()` factory.
1213

13-
.. autoclass:: graphtik.op.FunctionalOperation
14-
:members: __init__, __call__, compute
15-
:noindex:
16-
17-
The ``operation`` builder factory
18-
---------------------------------
19-
20-
There is a better way to instantiate an ``FunctionalOperation`` than simply constructing it:
21-
use the ``operation`` builder class:
22-
23-
.. autoclass:: graphtik.operation
24-
:members: withset, __call__
25-
:special-members:
26-
:noindex:
2714

2815
Operations are just functions
2916
------------------------------
3017

31-
At the heart of each ``operation`` is just a function, any arbitrary function.
32-
Indeed, you can instantiate an ``operation`` with a function and then call it
18+
At the heart of each `operation` is just a function, any arbitrary function.
19+
Indeed, you can wrap an existing function into an `operation`, and then call it
3320
just like the original function, e.g.:
3421

3522
>>> from operator import add
3623
>>> from graphtik import operation
3724

38-
>>> add_op = operation(name='add_op', needs=['a', 'b'], provides=['a_plus_b'])(add)
25+
>>> add_op = operation(add,
26+
... needs=['a', 'b'],
27+
... provides=['a_plus_b'])
28+
>>> add_op
29+
FunctionalOperation(name='add', needs=['a', 'b'], provides=['a_plus_b'], fn='add')
3930

4031
>>> add_op(3, 4) == add(3, 4)
4132
True
4233

34+
But ``__call__()`` is just to facilitate quick experimentation - it does not
35+
perform any checks or matching of *needs*/*provides* to function arguments
36+
& results (which happen when :term:`pipeline`\s :term:`compute`).
37+
38+
The way Graphtik works is by invoking their :meth:`.Operation.compute()` method,
39+
which, among others, allow to specify what results you desire to receive back
40+
(read more on :ref:`graph-computations`).
41+
4342

4443
Specifying graph structure: ``provides`` and ``needs``
4544
------------------------------------------------------
4645

47-
Of course, each ``operation`` is more than just a function.
48-
It is a node in a computation graph, depending on other nodes in the graph for input data and
49-
supplying output data that may be used by other nodes in the graph (or as a graph output).
50-
This graph structure is specified via the ``provides`` and ``needs`` arguments
51-
to the ``operation`` constructor. Specifically:
46+
Each :term:`operation` is a node in a computation :term:`graph`,
47+
depending and supplying data from and to other nodes (via the :term:`solution`),
48+
in order to :term:`compute`.
5249

53-
* ``provides``: this argument names the outputs (i.e. the returned values) of a given ``operation``.
54-
If multiple outputs are specified by ``provides``, then the return value of the function
55-
comprising the ``operation`` must return an iterable.
50+
This graph structure is specified (mostly) via the ``provides`` and ``needs`` arguments
51+
to the :func:`.operation` factory, specifically:
5652

57-
* ``needs``: this argument names data that is needed as input by a given ``operation``.
58-
Each piece of data named in needs may either be provided by another ``operation``
59-
in the same graph (i.e. specified in the ``provides`` argument of that ``operation``),
60-
or it may be specified as a named input to a graph computation
61-
(more on graph computations :ref:`here <graph-computations>`).
53+
``needs``
54+
this argument names the list of (positionally ordered) :term:`inputs` data the `operation`
55+
requires to receive from *solution*.
56+
The list corresponds, roughly, to the arguments of the underlying function
57+
(plus any :term:`sideffects`).
6258

63-
When many operations are composed into a computation graph (see :ref:`graph-composition` for more on that),
64-
Graphtik matches up the values in their ``needs`` and ``provides`` to form the edges of that graph.
59+
It can be a single string, in which case a 1-element iterable is assumed.
6560

66-
Let's look again at the operations from the script in :ref:`quick-start`,
67-
for example:
61+
:seealso: :term:`needs`, :term:`modifier`, :attr:`.FunctionalOperation.needs`,
62+
:attr:`.FunctionalOperation.op_needs`, :attr:`.FunctionalOperation._fn_needs`
6863

69-
>>> from operator import mul, sub
70-
>>> from functools import partial
71-
>>> from graphtik import compose, operation
72-
73-
>>> # Computes |a|^p.
74-
>>> def abspow(a, p):
75-
... c = abs(a) ** p
76-
... return c
64+
``provides``
65+
this argument names the list of (positionally ordered) :term:`outputs` data
66+
the operation provides into the *solution*.
67+
The list corresponds, roughly, to the returned values of the `fn`
68+
(plus any :term:`sideffects` & :term:`alias`\es).
7769

78-
>>> # Compose the mul, sub, and abspow operations into a computation graph.
79-
>>> graphop = compose("graphop",
80-
... operation(name="mul1", needs=["a", "b"], provides=["ab"])(mul),
81-
... operation(name="sub1", needs=["a", "ab"], provides=["a_minus_ab"])(sub),
82-
... operation(name="abspow1", needs=["a_minus_ab"], provides=["abs_a_minus_ab_cubed"])
83-
... (partial(abspow, p=3))
84-
... )
70+
It can be a single string, in which case a 1-element iterable is assumed.
8571

86-
.. Tip::
87-
Notice the use of :func:`functools.partial()` to set parameter ``p`` to a constant value.
88-
89-
The ``needs`` and ``provides`` arguments to the operations in this script define
90-
a computation graph that looks like this (where the oval are *operations*,
91-
squares/houses are *data*):
92-
93-
.. graphtik::
94-
95-
.. Tip::
96-
See :ref:`plotting` on how to make diagrams like this.
72+
If they are more than one, the underlying function must return an iterable
73+
with same number of elements (unless it :term:`returns dictionary`).
9774

75+
:seealso: :term:`provides`, :term:`modifier`, :attr:`.FunctionalOperation.provides`,
76+
:attr:`.FunctionalOperation.op_provides`, :attr:`.FunctionalOperation._fn_provides`
9877

9978
.. _aliases:
10079

@@ -110,87 +89,113 @@ on the `provides` side:
11089
... name="`provides` with `aliases`",
11190
... needs="anything",
11291
... provides="real thing",
113-
... aliases=("real thing", "phony"))()
92+
... aliases=("real thing", "phony"))
11493

11594
.. graphtik::
11695

117-
Instantiating operations
118-
------------------------
11996

120-
There are several ways to instantiate an ``operation``, each of which might be more suitable for different scenarios.
97+
Considerations for when building pipelines
98+
------------------------------------------
99+
When many operations are composed into a computation graph, Graphtik matches up
100+
the values in their *needs* and *provides* to form the edges of that graph
101+
(see :ref:`graph-composition` for more on that), like the operations from the script
102+
in :ref:`quick-start`:
121103

122-
Decorator specification
123-
^^^^^^^^^^^^^^^^^^^^^^^
104+
>>> from operator import mul, sub
105+
>>> from functools import partial
106+
>>> from graphtik import compose, operation
124107

125-
If you are defining your computation graph and the functions that comprise it all in the same script,
126-
the decorator specification of ``operation`` instances might be particularly useful,
127-
as it allows you to assign computation graph structure to functions as they are defined.
128-
Here's an example:
108+
>>> def abspow(a, p):
109+
... """Compute |a|^p. """
110+
... c = abs(a) ** p
111+
... return c
129112

130-
>>> from graphtik import operation, compose
113+
>>> # Compose the mul, sub, and abspow operations into a computation graph.
114+
>>> graphop = compose("graphop",
115+
... operation(mul, needs=["a", "b"], provides=["ab"]),
116+
... operation(sub, needs=["a", "ab"], provides=["a_minus_ab"]),
117+
... operation(name="abspow1", needs=["a_minus_ab"], provides=["abs_a_minus_ab_cubed"])
118+
... (partial(abspow, p=3))
119+
... )
120+
>>> graphop
121+
NetworkOperation('graphop',
122+
needs=['a', 'b', 'ab', 'a_minus_ab'],
123+
provides=['ab', 'a_minus_ab', 'abs_a_minus_ab_cubed'],
124+
x3 ops: mul, sub, abspow1)
131125

132-
>>> @operation(name='foo_op', needs=['a', 'b', 'c'], provides='foo')
133-
... def foo(a, b, c):
134-
... return c * (a + b)
135126

136-
>>> graphop = compose('foo_graph', foo)
127+
- Notice that if ``name`` is not given, it is deduced from the function name.
128+
- Notice the use of :func:`functools.partial()` to set parameter ``p`` to a constant value.
129+
- And this is done by calling once more the returned "decorator* from :func:`operation()`,
130+
when called without a functions.
131+
132+
The ``needs`` and ``provides`` arguments to the operations in this script define
133+
a computation graph that looks like this:
137134

138135
.. graphtik::
136+
.. Tip::
137+
See :ref:`plotting` on how to make diagrams like this.
139138

139+
Builder pattern
140+
^^^^^^^^^^^^^^^
141+
There 2 ways to instantiate an :class:`.FunctionalOperation`\s, each one suitable
142+
for different scenarios, and so far we have only seen the 1st one:
140143

141-
Functional specification
142-
^^^^^^^^^^^^^^^^^^^^^^^^
144+
We've seen that calling manually :func:`.operation()` allows putting into a pipeline
145+
functions that are defined elsewhere (e.g. in another module, or are system functions).
143146

144-
If the functions underlying your computation graph operations are defined elsewhere
145-
than the script in which your graph itself is defined (e.g. they are defined in another module,
146-
or they are system functions), you can use the functional specification of ``operation`` instances:
147+
But that method is also useful if you want to create multiple operation instances
148+
with similar attributes, e.g. ``needs``:
147149

148-
>>> from operator import add, mul
149-
>>> from graphtik import operation, compose
150+
>>> op_factory = operation(needs=['a'])
150151

151-
>>> add_op = operation(name='add_op', needs=['a', 'b'], provides='sum')(add)
152-
>>> mul_op = operation(name='mul_op', needs=['c', 'sum'], provides='product')(mul)
152+
Notice that we specified a `fn`, in order to get back a :class:`.FunctionalOperation`
153+
instance (and not a decorator).
153154

154-
>>> graphop = compose('add_mul_graph', add_op, mul_op)
155+
>>> from functools import partial
155156

156-
.. graphtik::
157+
>>> def mypow(a, p=2):
158+
... return a ** p
157159

160+
>>> pow_op2 = op_factory.withset(fn=mypow, provides="^2")
161+
>>> pow_op3 = op_factory.withset(fn=partial(mypow, p=3), name='pow_3', provides='^3')
162+
>>> pow_op0 = op_factory.withset(fn=lambda a: 1, name='pow_0', provides='^0')
158163

159-
The functional specification is also useful if you want to create multiple ``operation``
160-
instances from the same function, perhaps with different parameter values, e.g.:
164+
>>> graphop = compose('powers', pow_op2, pow_op3, pow_op0)
165+
>>> graphop
166+
NetworkOperation('powers', needs=['a'], provides=['^2', '^3', '^0'], x3 ops:
167+
mypow, pow_3, pow_0)
161168

162-
>>> from functools import partial
163169

164-
>>> def mypow(a, p=2):
165-
... return a ** p
170+
>>> graphop(a=2)
171+
{'a': 2, '^2': 4, '^3': 8, '^0': 1}
166172

167-
>>> pow_op1 = operation(name='pow_op1', needs=['a'], provides='a_squared')(mypow)
168-
>>> pow_op2 = operation(name='pow_op2', needs=['a'], provides='a_cubed')(partial(mypow, p=3))
173+
.. graphtik::
169174

170-
>>> graphop = compose('two_pows_graph', pow_op1, pow_op2)
171175

172-
A slightly different approach can be used here to accomplish the same effect
173-
by creating an operation "builder pattern":
176+
Decorator specification
177+
^^^^^^^^^^^^^^^^^^^^^^^
174178

175-
>>> def mypow(a, p=2):
176-
... return a ** p
179+
If you are defining your computation graph and the functions that comprise it all in the same script,
180+
the decorator specification of ``operation`` instances might be particularly useful,
181+
as it allows you to assign computation graph structure to functions as they are defined.
182+
Here's an example:
177183

178-
>>> pow_op_factory = operation(mypow, needs=['a'], provides='a_squared')
184+
>>> from graphtik import operation, compose
179185

180-
>>> pow_op1 = pow_op_factory(name='pow_op1')
181-
>>> pow_op2 = pow_op_factory.withset(name='pow_op2', provides='a_cubed')(partial(mypow, p=3))
182-
>>> pow_op3 = pow_op_factory(lambda a: 1, name='pow_op3')
186+
>>> @operation(name='foo_op', needs=['a', 'b', 'c'], provides='foo')
187+
... def foo(a, b, c):
188+
... return c * (a + b)
183189

184-
>>> graphop = compose('two_pows_graph', pow_op1, pow_op2, pow_op3)
185-
>>> graphop(a=2)
186-
{'a': 2, 'a_squared': 4, 'a_cubed': 1}
190+
>>> graphop = compose('foo_graph', foo)
191+
192+
.. graphtik::
187193

188-
.. Note::
189-
You cannot call again the factory to overwrite the *function*,
190-
you have to use either the ``fn=`` keyword with ``withset()`` method or
191-
call once more.
192194

193195

194196
Modifiers on `operation` `needs` and `provides`
195197
-----------------------------------------------
196-
see `mod:`.modifiers`.
198+
Annotations on a `dependency` such as `optionals` & `sideffects` modify their behavior,
199+
and eventually the :term:`pipeline`.
200+
201+
Read `mod:`.modifiers` for more.

graphtik/netop.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def __repr__(self):
157157
steps = (
158158
"".join(f"\n +--{s}" for s in ops)
159159
if is_debug()
160-
else ", ".join(s.name for s in ops)
160+
else ", ".join(str(s.name) for s in ops)
161161
)
162162
return f"{clsname}({self.name!r}, needs={needs}, provides={provides}, x{len(ops)} ops: {steps})"
163163

0 commit comments

Comments
 (0)