Skip to content

Commit 54e8f6c

Browse files
committed
REFACT(OP): operation() as BUILDER_PATTERN
+ drop: operation() does not inherit Operation (that was buggy design callling for mistakes) + enh: operation().withset() to function as builder. + refact: simplify repr() using f-strings * defaults. + refact: move (new) _validate() in FuncOp class.
1 parent c0cec14 commit 54e8f6c

File tree

5 files changed

+106
-70
lines changed

5 files changed

+106
-70
lines changed

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ Graphtik Changelog
44

55
TODO
66
====
7+
See also `gg:`1`.
8+
79
+ [ ] use a "start-node" to insert input-values in solution
810
+ [ ] typo(test): overriden-->overriDDen
11+
+ [ ] support functions with ``*args`` and ``**kwargs``.
912

1013

1114
v2.0.1b0 (18 Oct 2019): better plan with perfect evictions

docs/source/plotting.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,12 @@ with the folllowing properties, as a debug aid:
8383
'executed': set(),
8484
'network': Network(
8585
+--a
86-
+--FunctionalOperation(name='screamer', needs=['a'], provides=['foo'])
86+
+--FunctionalOperation(name='screamer', needs=['a'], provides=['foo'], fn='scream')
8787
+--foo),
88-
'operation': FunctionalOperation(name='screamer', needs=['a'], provides=['foo']),
88+
'operation': FunctionalOperation(name='screamer', needs=['a'], provides=['foo'], fn='scream'),
8989
'outputs': None,
9090
'plan': ExecutionPlan(inputs=('a',), outputs=(), steps:
91-
+--FunctionalOperation(name='screamer', needs=['a'], provides=['foo'])),
91+
+--FunctionalOperation(name='screamer', needs=['a'], provides=['foo'], fn='scream')),
9292
'provides': ['foo'],
9393
'results': None,
9494
'solution': {'a': None}}

graphtik/base.py

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414
log = logging.getLogger(__name__)
1515

1616

17+
def aslist(i):
18+
"""utility used through out"""
19+
if i and not isinstance(i, str):
20+
return list(i)
21+
return i
22+
23+
1724
def jetsam(ex, locs, *salvage_vars: str, annotation="jetsam", **salvage_mappings):
1825
"""
1926
Annotate exception with salvaged values from locals() and raise!
@@ -174,6 +181,35 @@ def compute(self, named_inputs, outputs=None):
174181
"""
175182
raise NotImplementedError("Abstract %r! cannot compute()!" % self)
176183

184+
def _validate(self):
185+
from .modifiers import optional
186+
187+
if not self.name:
188+
raise ValueError(f"Operation needs a name, got: {self.name}")
189+
190+
needs = self.needs
191+
# Allow single value for needs parameter
192+
if isinstance(needs, str) and not isinstance(needs, optional):
193+
needs = [needs]
194+
if not needs:
195+
raise ValueError(f"Empty `needs` given: {needs!r}")
196+
if not all(n for n in needs):
197+
raise ValueError(f"One item in `needs` is null: {needs!r}")
198+
if not isinstance(needs, (list, tuple)):
199+
raise ValueError(f"Bad `needs`, not (list, tuple): {needs!r}")
200+
self.needs = needs
201+
202+
# Allow single value for provides parameter
203+
provides = self.provides
204+
if isinstance(provides, str):
205+
provides = [provides]
206+
if provides and not all(n for n in provides):
207+
raise ValueError(f"One item in `provides` is null: {provides!r}")
208+
provides = provides or ()
209+
if not isinstance(provides, (list, tuple)):
210+
raise ValueError(f"Bad `provides`, not (list, tuple): {provides!r}")
211+
self.provides = provides or ()
212+
177213
def _after_init(self):
178214
"""
179215
This method is a hook for you to override. It gets called after this
@@ -188,18 +224,10 @@ def __repr__(self):
188224
"""
189225
Display more informative names for the Operation class
190226
"""
191-
192-
def aslist(i):
193-
if i and not isinstance(i, str):
194-
return list(i)
195-
return i
196-
197-
return u"%s(name='%s', needs=%s, provides=%s)" % (
198-
self.__class__.__name__,
199-
getattr(self, "name", None),
200-
aslist(getattr(self, "needs", None)),
201-
aslist(getattr(self, "provides", None)),
202-
)
227+
clsname = type(self).__name__
228+
needs = aslist(self.needs)
229+
provides = aslist(self.provides)
230+
return f"{clsname}(name={self.name!r}, needs={needs!r}, provides={provides!r})"
203231

204232

205233
class NetworkOperation(Operation, plot.Plotter):

graphtik/functional.py

Lines changed: 59 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,31 @@
33
import networkx as nx
44
from boltons.setutils import IndexedSet as iset
55

6-
from .base import jetsam, NetworkOperation, Operation
6+
from .base import NetworkOperation, Operation, aslist, jetsam
77
from .modifiers import optional, sideffect
88
from .network import Network
99

1010

1111
class FunctionalOperation(Operation):
12+
"""Use operation() to build instances of this class instead"""
1213
def __init__(self, fn=None, **kwargs):
1314
self.fn = fn
1415
Operation.__init__(self, **kwargs)
16+
self._validate()
17+
18+
def __repr__(self):
19+
"""
20+
Display more informative names for the Operation class
21+
"""
22+
needs = aslist(self.needs)
23+
provides = aslist(self.provides)
24+
fn_name = self.fn and getattr(self.fn, "__name__", str(self.fn))
25+
return f"FunctionalOperation(name={self.name!r}, needs={needs!r}, provides={provides!r}, fn={fn_name!r})"
26+
27+
def _validate(self):
28+
super()._validate()
29+
if not self.fn or not callable(self.fn):
30+
raise ValueError(f"Operation was not provided with a callable: {self.fn}")
1531

1632
def compute(self, named_inputs, outputs=None):
1733
try:
@@ -65,57 +81,63 @@ def __call__(self, *args, **kwargs):
6581
return self.fn(*args, **kwargs)
6682

6783

68-
class operation(Operation):
84+
class operation:
6985
"""
70-
This object represents an operation in a computation graph. Its
71-
relationship to other operations in the graph is specified via its
72-
``needs`` and ``provides`` arguments.
86+
A builder for graph-operations wrapping functions.
7387
7488
:param function fn:
7589
The function used by this operation. This does not need to be
7690
specified when the operation object is instantiated and can instead
7791
be set via ``__call__`` later.
78-
7992
:param str name:
8093
The name of the operation in the computation graph.
81-
8294
:param list needs:
8395
Names of input data objects this operation requires. These should
8496
correspond to the ``args`` of ``fn``.
85-
8697
:param list provides:
8798
Names of output data objects this operation provides.
8899
89-
"""
100+
:return:
101+
when called, it returns a :class:`FunctionalOperation`
90102
91-
def __init__(self, fn=None, **kwargs):
92-
self.fn = fn
93-
Operation.__init__(self, **kwargs)
103+
**Example:**
104+
105+
Here it is an example of it use with the "builder pattern"::
106+
107+
>>> from graphtik import operation
108+
109+
>>> opb = operation(name='add_op')
110+
>>> opb.withset(needs=['a', 'b'])
111+
operation(name='add_op', needs=['a', 'b'], provides=None, fn=None)
112+
>>> opb.withset(provides='SUM', fn=sum)
113+
operation(name='add_op', needs=['a', 'b'], provides='SUM', fn='sum')
114+
115+
You may keep calling ``withset()`` till you invoke ``__call__()`` on the builder;
116+
then you get te actual :class:`Operation` instance::
94117
95-
def _normalize_kwargs(self, kwargs):
118+
>>> # Create `Operation` and overwrite function at the last moment.
119+
>>> opb(sum)
120+
FunctionalOperation(name='add_op', needs=['a', 'b'], provides=['SUM'], fn='sum')
121+
"""
96122

97-
# Allow single value for needs parameter
98-
needs = kwargs["needs"]
99-
if isinstance(needs, str) and not isinstance(needs, optional):
100-
assert needs, "empty string provided for `needs` parameters"
101-
kwargs["needs"] = [needs]
123+
fn = name = needs = provides = None
102124

103-
# Allow single value for provides parameter
104-
provides = kwargs.get("provides")
105-
if isinstance(provides, str):
106-
assert provides, "empty string provided for `needs` parameters"
107-
kwargs["provides"] = [provides]
125+
def __init__(self, fn=None, *, name=None, needs=None, provides=None):
126+
self.withset(fn=fn, name=name, needs=needs, provides=provides)
108127

109-
assert kwargs["name"], "operation needs a name"
110-
assert isinstance(kwargs["needs"], list), "no `needs` parameter provided"
111-
assert isinstance(kwargs["provides"], list), "no `provides` parameter provided"
112-
assert hasattr(
113-
kwargs["fn"], "__call__"
114-
), "operation was not provided with a callable"
128+
def withset(self, *, fn=None, name=None, needs=None, provides=None):
129+
if fn is not None:
130+
self.fn = fn
131+
if name is not None:
132+
self.name = name
133+
if needs is not None:
134+
self.needs = needs
135+
if provides is not None:
136+
self.provides = provides
115137

116-
return kwargs
138+
return self
117139

118-
def __call__(self, fn=None, **kwargs):
140+
def __call__(self, fn=None, *, name=None, needs=None, provides=None):
119141
"""
120142
This enables ``operation`` to act as a decorator or as a functional
121143
operation, for example::
@@ -138,35 +160,18 @@ def myadd(a, b):
138160
composed into a computation graph.
139161
"""
140162

141-
if fn is not None:
142-
self.fn = fn
143-
144-
total_kwargs = {}
145-
total_kwargs.update(vars(self))
146-
total_kwargs.update(kwargs)
147-
total_kwargs = self._normalize_kwargs(total_kwargs)
163+
self.withset(fn=fn, name=name, needs=needs, provides=provides)
148164

149-
return FunctionalOperation(**total_kwargs)
165+
return FunctionalOperation(**vars(self))
150166

151167
def __repr__(self):
152168
"""
153169
Display more informative names for the Operation class
154170
"""
155-
156-
def aslist(i):
157-
if i and not isinstance(i, str):
158-
return list(i)
159-
return i
160-
161-
func_name = getattr(self, "fn")
162-
func_name = func_name and getattr(func_name, "__name__", None)
163-
return u"%s(name='%s', needs=%s, provides=%s, fn=%s)" % (
164-
self.__class__.__name__,
165-
getattr(self, "name", None),
166-
aslist(getattr(self, "needs", None)),
167-
aslist(getattr(self, "provides", None)),
168-
func_name,
169-
)
171+
needs = aslist(self.needs)
172+
provides = aslist(self.provides)
173+
fn_name = self.fn and getattr(self.fn, "__name__", str(self.fn))
174+
return f"operation(name={self.name!r}, needs={needs!r}, provides={provides!r}, fn={fn_name!r})"
170175

171176

172177
class compose(object):

graphtik/modifiers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ class sideffect(str):
104104
... name="upd_prices",
105105
... needs=["sales_df", "price"],
106106
... provides=[sideffect("price")])
107-
operation(name='upd_prices', needs=['sales_df', 'price'], provides=['sideffect(price)'], fn=upd_prices)
107+
operation(name='upd_prices', needs=['sales_df', 'price'], provides=['sideffect(price)'], fn='upd_prices')
108108
109109
.. note::
110110
An ``operation`` with *sideffects* outputs only, have functions that return

0 commit comments

Comments
 (0)