Skip to content

Commit c99fc19

Browse files
committed
Support for single-parent Nodes
1 parent 3d73930 commit c99fc19

File tree

1 file changed

+106
-26
lines changed

1 file changed

+106
-26
lines changed

Diff for: src/evaluation/node/node.py

+106-26
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ def dict_or_list_iter(obj):
4949
else:
5050
raise TypeError("obj must be either dict or list")
5151

52+
def dict_or_list_map(obj, func):
53+
if type(obj) is list:
54+
return [func(n) for n in obj]
55+
elif type(obj) is dict:
56+
return {k : func(n) for k, n in obj.items()}
57+
else:
58+
raise TypeError("obj must be either dict or list")
59+
60+
class EmptyParentKey:
61+
pass
62+
5263
class ColorCycle:
5364
def __init__(self, colors):
5465
self.colors = colors
@@ -111,8 +122,7 @@ class EvalNode(ABC, metaclass=InstanceCounterMeta):
111122
:type name: string
112123
113124
:param parents: The parents of this *EvalNode*. Can also be understood as
114-
the data sources. If a single *EvalNode* instance is passed, expect a
115-
list containing a single item for *parent_data*, else *parent_data*
125+
the data sources. *parent_data*
116126
will have the same structure as this parameter with *EvalNodes*
117127
exchanged for the return value of :py:meth:`EvalNode.do`
118128
:type parents: list or dict or :py:class:`EvalNode`
@@ -205,15 +215,16 @@ def __init__(self, name, parents=None, plot=False, cache=None, ignore_cache=Fals
205215
if self.parents is None:
206216
self.parents = parents
207217

218+
# access _parent_data keys while iterating by using _set_parent_data and
219+
# _get_parent_data to automatically handle the case of only a single parent
208220
if type(self.parents) is list:
209221
self._parent_data = [None] * len(self.parents)
210222
elif type(self.parents) is dict:
211223
self._parent_data = {}
212-
elif isinstance(self.parents, EvalNode):
213-
self.parents = [self.parents]
214-
self._parent_data = [None]
224+
elif isinstance(self.parents, EvalNode) or self.parents is None:
225+
self._parent_data = None
215226
else:
216-
raise TypeError("Parents must be either list or dict or EvalNode")
227+
raise TypeError("Parents must be either list or dict or EvalNode or NoneType")
217228

218229
@property
219230
def id(self):
@@ -296,17 +307,27 @@ def copy(self, name_suffix, plot=None, ignore_cache=None, last_parents=None, las
296307
new_parents = [None] * len(self.parents)
297308
elif type(self.parents) is dict:
298309
new_parents = {}
310+
elif isinstance(self.parents, EvalNode) or self.parents is None:
311+
new_parents = None
312+
else:
313+
raise TypeError("parents have an invalid type")
299314

300315
if self in memo:
301316
new = memo[self]
302317
else:
318+
is_last_node = True
303319
for n, p in self.parents_iter:
304320
cpy_or_memo = p.copy(name_suffix, plot, ignore_cache, last_parents, last_kwargs, memo=memo)#, **kwargs)
305-
new_parents[n] = cpy_or_memo
321+
is_last_node = False
322+
if n is EmptyParentKey:
323+
# this can only happen once because EmptyParentKey is used only if there's no more than one parents
324+
new_parents = cpy_or_memo
325+
else:
326+
new_parents[n] = cpy_or_memo
306327

307328
kwargs_use = copy.deepcopy(self.ext)
308329

309-
if len(new_parents) == 0:
330+
if is_last_node:
310331
new_parents = last_parents
311332

312333
# merge items whose values are dicts
@@ -377,18 +398,25 @@ def __contains__(self, item):
377398
def parents_iter(self):
378399
"""
379400
:return: An iterator that can be handled uniformly
380-
for any type of parents (list or dict).
401+
for any type of parents (list or dict or :py:class:`EvalNode`).
381402
For lists, it is enumerate(:py:attr:`parents`), and for dict
382-
it is :py:attr:`parents`.items()
403+
it is :py:attr:`parents`.items(). For a single parent of class
404+
:py:class:`EvalNode`, an iterator yielding
405+
:py:class:`EmptyParentKey`: *parent* is returned so that this
406+
property can be used uniformly as iterator.
383407
384408
:rtype: iterator
385409
"""
386410
if type(self.parents) is list:
387411
return enumerate(self.parents)
388412
elif type(self.parents) is dict:
389413
return self.parents.items()
414+
elif isinstance(self.parents, EvalNode):
415+
return {EmptyParentKey: self.parents}.items()
416+
elif self.parents is None:
417+
return {}.items()
390418
else:
391-
raise TypeError("parents must be either dict or list")
419+
raise TypeError("parents must be either dict or list or EvalNode or None")
392420

393421
def is_parent(self, possible_parent):
394422
"""
@@ -399,21 +427,31 @@ def is_parent(self, possible_parent):
399427
return possible_parent in self.parents
400428
elif type(self.parents) is dict:
401429
return possible_parent in self.parents.values()
430+
elif isinstance(self.parents, EvalNode):
431+
return possible_parent is self.parents
432+
elif self.parents is None:
433+
return False
402434
else:
403-
raise TypeError("parents must be either dict or list")
435+
raise TypeError("parents must be either dict or list or EvalNode instance or None")
404436

405437
def parents_contains(self, key):
406438
"""
407439
:return: True if the parents iterator contains an item with the given key.
408440
For parents stored as list, this is equal to 'key < len(parents)'
409441
and for parents stored as dict, this is eqal to 'key in parents'
442+
If the parents are a single :py:class:`EvalNode` this always returns
443+
False.
410444
411445
:rtype: bool
412446
"""
413447
if type(self.parents) is list:
414448
return key < len(self.parents)
415449
elif type(self.parents) is dict:
416450
return key in self.parents
451+
elif isinstance(self.parents, EvalNode):
452+
return False
453+
elif self.parents is None:
454+
return False
417455
else:
418456
raise TypeError("parents must be either dict or list")
419457

@@ -428,6 +466,9 @@ def parents(self):
428466
@parents.setter
429467
def parents(self, parents):
430468
self._parents = parents
469+
if not (isinstance(parents, EvalNode) or type(parents) is list or
470+
type(parents) is dict or parents is None):
471+
raise TypeError("invalid type for parents (got {})".format(type(parents)))
431472

432473
@property
433474
def plot_on(self):
@@ -624,6 +665,27 @@ def _check_data_needed(self):
624665
self._log_debug("Does not need data")
625666
return False
626667

668+
def _set_parent_data(self, key, data):
669+
"""
670+
Sets the parent data of parent with key.
671+
Use this method to correctly handle the case of a single parent, i.e.
672+
the key being EmptyParentKey as returned by :py:attr:`parents_iter`.
673+
This enables concise iterating over the parents
674+
675+
:return: *None*
676+
"""
677+
678+
if key is EmptyParentKey:
679+
self._parent_data = data
680+
else:
681+
self._parent_data[key] = data
682+
683+
def _get_parent_data(self, key):
684+
if key is EmptyParentKey:
685+
return self._parent_data
686+
else:
687+
return self._parent_data[key]
688+
627689
def _maybe_fill_parent_data(self, this_need_data, common, kwargs):
628690
"""
629691
Fills parent data but allows parents to return None if this_need_data is False.
@@ -638,7 +700,7 @@ def _maybe_fill_parent_data(self, this_need_data, common, kwargs):
638700
for k, p in self.parents_iter:
639701
maybe_data = p._data(this_need_data, common, kwargs)
640702
assert not (maybe_data is None and this_need_data) # if we requested data, but recieved None, quit
641-
self._parent_data[k] = maybe_data
703+
self._set_parent_data(k, maybe_data)
642704
must_fill = must_fill or not maybe_data is None
643705

644706
return must_fill
@@ -656,8 +718,8 @@ def _fill_parent_data(self, common, kwargs):
656718
"""
657719
self._log_debug("Some parents returned data, so we assume regeneration is neccessary and traverse the tree a second time to get all data")
658720
for k, p in self.parents_iter:
659-
if self._parent_data[k] is None:
660-
self._parent_data[k] = p._data(True, common, kwargs)
721+
if self._get_parent_data(k) is None:
722+
self._set_parent_data(k, p._data(True, common, kwargs))
661723

662724
def _data(self, need_data, common, kwargs):
663725
# request data from parents using this info
@@ -673,7 +735,7 @@ def _data(self, need_data, common, kwargs):
673735

674736
self._log_debug("Requesting regeneration or RAM cache retrieval of data")
675737
v = self._generate_v(common, kwargs)
676-
elif len(self.parents) == 0 and this_need_data:
738+
elif self.parent_count == 0 and this_need_data:
677739
# if there are no parents but data is needed
678740
self._log_debug("This node has no parents but it needs data")
679741
self._log_debug("Requesting regeneration or RAM cache retrieval of data")
@@ -697,6 +759,7 @@ def _data(self, need_data, common, kwargs):
697759

698760
def _plot(self, ax, plot_group, common, memo, kwargs):
699761
for k, p in self.parents_iter:
762+
self._log_debug("[_plot] traversing tree upwards")
700763
p._plot(ax, plot_group, common, memo, kwargs)
701764

702765
if self.in_plot_group(plot_group) and not self in memo:
@@ -705,10 +768,10 @@ def _plot(self, ax, plot_group, common, memo, kwargs):
705768
# modification: at this place, when v == None, v was loaded from
706769
# and returned, so child notes regenerated.
707770
# this has been removed but I dont know if it was actually senseful
708-
self._log_debug("Request loading data from cache or RAM cache anyways for plotting")
771+
self._log_debug("[_plot] Request loading data from cache or RAM cache anyways for plotting")
709772
v = self._data(True, common, kwargs)
710773

711-
self._log_debug("Plot the node")
774+
self._log_debug("[_plot] Plot the node")
712775

713776
self._plot_handles = self.plot(v, plot_on, common, **(self.ext | kwargs))
714777

@@ -719,24 +782,30 @@ def _plot(self, ax, plot_group, common, memo, kwargs):
719782
if not self._plot_handles[0] is None and self._color is None:
720783
self._color = self._plot_handles[0].get_color()
721784

722-
723-
def map_parents(self, callback):
785+
def map_parents(self, callback, call_on_none=False):
724786
"""
725787
Executes callback for every parent of this instance
726788
727789
:param callback: The :py:class:`Callable` to be executed
728790
:type callback: :py:class:`Callable` (:py:class:`EvalNode`) -> object
729791
792+
:param call_on_none: If this :py:class:`EvalNode` has no parents
793+
call the callback with *None* as argument.
794+
:type call_on_none: :py:class:`bool`, Default: *False*
795+
730796
:return: object with structure like :py:attr:`parents` but the nodes
731797
replaced with the objects returned by *callback*
732798
:rtype: same as :py:attr:`parents`
733799
"""
734-
if type(self.parents) is list:
735-
return [callback(n) for n in self.parents]
736-
elif type(self.parents) is dict:
737-
return {k : callback(n) for k, n in self.parents.items()}
800+
if isinstance(self.parents, EvalNode):
801+
return callback(self.parents)
802+
elif self.parents is None:
803+
if call_on_none:
804+
return callback(None)
805+
else:
806+
return None
738807
else:
739-
raise TypeError("parents must be either dict or list")
808+
return dict_or_list_map(self.parents, callback)
740809

741810
def search_parent(self, starts_with):
742811
"""
@@ -854,6 +923,14 @@ def handles_complete_tree(self):
854923

855924
return l
856925

926+
@property
927+
def parent_count(self):
928+
if self.parents is None:
929+
return 0
930+
elif isinstance(self.parents, EvalNode):
931+
return 1
932+
else:
933+
return len(self.parents)
857934

858935
def def_kwargs(self, **kwargs):
859936
"""
@@ -981,7 +1058,9 @@ def get_color(self):
9811058
return self._color
9821059

9831060
for _, p in self.parents_iter:
984-
return p.get_color()
1061+
p_color = p.get_color()
1062+
if not p_color is None:
1063+
return p_color
9851064

9861065
return None
9871066

@@ -1016,6 +1095,7 @@ def do(self, parent_data, common, **kwargs):
10161095

10171096
class NodePlotGroup(EvalNode):
10181097
def subclass_init(self, parents, **kwargs):
1098+
raise TypeError("Using NodePlotGroup is deprecated because it hasn't been updated, will probably not work and I don't know what its purpose was anymore.")
10191099
super().subclass_init(parents, **kwargs)
10201100
self.never_cache = True
10211101

0 commit comments

Comments
 (0)