Skip to content

Commit c33c9e1

Browse files
authored
[patch] Detatched path (#457)
* [patch] carry semantic path information through get/set state process * Refactor: handle the parent-child recursion more gracefully So that semantic parents no longer need to be updating private attributes of their children to avoid the recursion. It was barely tolerable when it was a single attribute, but now that we also track the detached parent path it is totally unacceptable. * Refactor: condense if clause
1 parent f163074 commit c33c9e1

File tree

3 files changed

+78
-12
lines changed

3 files changed

+78
-12
lines changed

pyiron_workflow/mixin/semantics.py

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def __init__(
3838
):
3939
self._label = None
4040
self._parent = None
41+
self._detached_parent_path = None
4142
self.label = label
4243
self.parent = parent
4344
super().__init__(*args, **kwargs)
@@ -64,17 +65,21 @@ def parent(self, new_parent: SemanticParent | None) -> None:
6465
# Exit early if nothing is changing
6566
return
6667

67-
if new_parent is not None:
68-
if not isinstance(new_parent, SemanticParent):
69-
raise ValueError(
70-
f"Expected None or a {SemanticParent.__name__} for the parent of "
71-
f"{self.label}, but got {new_parent}"
72-
)
68+
if new_parent is not None and not isinstance(new_parent, SemanticParent):
69+
raise ValueError(
70+
f"Expected None or a {SemanticParent.__name__} for the parent of "
71+
f"{self.label}, but got {new_parent}"
72+
)
7373

74-
if self._parent is not None and new_parent is not self._parent:
74+
if (
75+
self._parent is not None
76+
and new_parent is not self._parent
77+
and self in self._parent.children
78+
):
7579
self._parent.remove_child(self)
7680
self._parent = new_parent
77-
if self._parent is not None:
81+
self._detached_parent_path = None
82+
if self._parent is not None and self not in self._parent.children:
7883
self._parent.add_child(self)
7984

8085
@property
@@ -83,9 +88,34 @@ def semantic_path(self) -> str:
8388
The path of node labels from the graph root (parent-most node) down to this
8489
node.
8590
"""
86-
prefix = self.parent.semantic_path if isinstance(self.parent, Semantic) else ""
91+
if self.parent is None and self.detached_parent_path is None:
92+
prefix = ""
93+
elif self.parent is None and self.detached_parent_path is not None:
94+
prefix = self.detached_parent_path
95+
elif self.parent is not None and self.detached_parent_path is None:
96+
prefix = self.parent.semantic_path
97+
else:
98+
raise ValueError(
99+
f"The parent and detached path should not be able to take non-None "
100+
f"values simultaneously, but got {self.parent} and "
101+
f"{self.detached_parent_path}, respectively. Please raise an issue on GitHub "
102+
f"outlining how your reached this state."
103+
)
87104
return prefix + self.semantic_delimiter + self.label
88105

106+
@property
107+
def detached_parent_path(self) -> str | None:
108+
"""
109+
The get/set state cycle of :class:`Semantic` de-parents objects, but we may
110+
still be interested in the semantic path -- e.g. if we `pickle` dump and load
111+
the object we will lose parent information, but this will still hold what the
112+
path _was_ before the orphaning process.
113+
114+
The detached path will get cleared if a new parent is set, but is otherwise
115+
used as the root for the purposes of finding the semantic path.
116+
"""
117+
return self._detached_parent_path
118+
89119
@property
90120
def full_label(self) -> str:
91121
"""
@@ -109,6 +139,8 @@ def as_path(self, root: Path | str | None = None) -> Path:
109139

110140
def __getstate__(self):
111141
state = super().__getstate__()
142+
if self.parent is not None:
143+
state["_detached_parent_path"] = self.parent.semantic_path
112144
state["_parent"] = None
113145
# Regarding removing parent from state:
114146
# Basically we want to avoid recursion during (de)serialization; when the
@@ -240,7 +272,7 @@ def add_child(
240272
# Finally, update label and reflexively form the parent-child relationship
241273
child.label = label
242274
self.children[child.label] = child
243-
child._parent = self
275+
child.parent = self
244276
return child
245277

246278
@staticmethod
@@ -316,7 +348,7 @@ def remove_child(self, child: Semantic | str) -> Semantic:
316348
f"{Semantic.__name__} but got {child}"
317349
)
318350

319-
child._parent = None
351+
child.parent = None
320352

321353
return child
322354

@@ -361,7 +393,7 @@ def __setstate__(self, state):
361393
# but rather can send just the requested object and its scope (semantic
362394
# children). So, now return their parent to them:
363395
for child in self:
364-
child._parent = self
396+
child.parent = self
365397

366398

367399
class ParentMostError(TypeError):

pyiron_workflow/nodes/composite.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ def _parse_remotely_executed_self(self, other_self):
212212
# Un-parent existing nodes before ditching them
213213
for node in self:
214214
node._parent = None
215+
node._detached_parent_path = None
215216
other_self.running = False # It's done now
216217
state = self._get_state_from_remote_other(other_self)
217218
self.__setstate__(state)

tests/unit/mixin/test_semantics.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,39 @@ def test_as_path(self):
9797
msg="Path root"
9898
)
9999

100+
def test_detached_parent_path(self):
101+
orphan = Semantic("orphan")
102+
orphan.__setstate__(self.child2.__getstate__())
103+
self.assertIsNone(
104+
orphan.parent,
105+
msg="We still should not explicitly have a parent"
106+
)
107+
self.assertListEqual(
108+
orphan.detached_parent_path.split(orphan.semantic_delimiter),
109+
self.child2.semantic_path.split(orphan.semantic_delimiter)[:-1],
110+
msg="Despite not having a parent, the detached path should store semantic "
111+
"path info through the get/set state routine"
112+
)
113+
self.assertEqual(
114+
orphan.semantic_path,
115+
self.child2.semantic_path,
116+
msg="The detached path should carry through to semantic path in the "
117+
"absence of a parent"
118+
)
119+
orphan.label = "orphan" # Re-set label after getting state
120+
orphan.parent = self.child2.parent
121+
self.assertIsNone(
122+
orphan.detached_parent_path,
123+
msg="Detached paths aren't necessary and shouldn't co-exist with the "
124+
"presence of a parent"
125+
)
126+
self.assertListEqual(
127+
orphan.semantic_path.split(orphan.semantic_delimiter)[:-1],
128+
self.child2.semantic_path.split(self.child2.semantic_delimiter)[:-1],
129+
msg="Sanity check -- except for the now-different labels, we should be "
130+
"recovering the usual semantic path on setting a parent."
131+
)
132+
100133

101134
if __name__ == '__main__':
102135
unittest.main()

0 commit comments

Comments
 (0)