From 66afbb6e715a3799fd63fb4b9fab944334c6c881 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:15:05 +0000 Subject: [PATCH 1/5] Initial plan From 70284ba41368802eaa69a27d75a337487c5af389 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:21:30 +0000 Subject: [PATCH 2/5] Make unlink() with no arguments clear all children Co-authored-by: ConnorStoneAstro <78555321+ConnorStoneAstro@users.noreply.github.com> --- docs/source/notebooks/BeginnersGuide.ipynb | 35 +++++++++++++++++++++- src/caskade/base.py | 18 +++++++++-- tests/test_base.py | 6 ++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/docs/source/notebooks/BeginnersGuide.ipynb b/docs/source/notebooks/BeginnersGuide.ipynb index bf39e9c..67a7f86 100644 --- a/docs/source/notebooks/BeginnersGuide.ipynb +++ b/docs/source/notebooks/BeginnersGuide.ipynb @@ -391,6 +391,39 @@ "combinedsim.graphviz()" ] }, + { + "cell_type": "markdown", + "id": "unlink_md", + "metadata": {}, + "source": [ + "### Unlinking nodes\n", + "\n", + "You can unlink a child node from a parent by calling `unlink` with the key or node to remove. ", + "Calling `unlink()` with no arguments is a convenient way to remove **all** children at once." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "unlink_code", + "metadata": {}, + "outputs": [], + "source": [ + "# Unlink a specific child by passing its key or Node object\n", + "secondsim.x0.unlink(simtime)\n", + "print(\"After unlinking simtime from secondsim.x0:\", secondsim.x0.children)\n", + "\n", + "# Re-link it\n", + "secondsim.x0.link(simtime)\n", + "\n", + "# Calling unlink() with no arguments removes all children at once\n", + "secondsim.x0.unlink()\n", + "print(\"After unlink() with no args:\", secondsim.x0.children)\n", + "\n", + "# Re-link for the rest of the tutorial\n", + "secondsim.x0.link(simtime)" + ] + }, { "cell_type": "code", "execution_count": null, @@ -568,4 +601,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/src/caskade/base.py b/src/caskade/base.py index 85e3b4c..ff498eb 100644 --- a/src/caskade/base.py +++ b/src/caskade/base.py @@ -207,8 +207,22 @@ def _unlink(self, key: str): del self.children[key] self.update_graph() - def unlink(self, key: Union[str, "Node", list, tuple]): - """Unlink the current ``Node`` object from another ``Node`` object which is a child.""" + def unlink(self, key: Union[str, "Node", list, tuple, None] = None): + """Unlink the current ``Node`` object from another ``Node`` object which is a child. + + Parameters + ---------- + key: (str, Node, list, tuple, or None, optional) + The key, ``Node`` object, or collection of keys/nodes to unlink. + If a string, the child with that key is unlinked. If a ``Node`` + object, the matching child is located and unlinked. If a list or + tuple, each element is unlinked in turn. If ``None`` (the + default), all children are unlinked. + """ + if key is None: + for k in list(self.children): + self._unlink(k) + return if isinstance(key, Node): for node in self.children: if self.children[node] is key: diff --git a/tests/test_base.py b/tests/test_base.py index e566889..9b474bc 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -87,6 +87,12 @@ def test_linking(node_graph): a.e a.unlink((b, c)) + # Check unlink with no arguments clears all children + a.link(e) + assert len(a.children) > 0 + a.unlink() + assert len(a.children) == 0 + def test_graphviz(node_graph): a, *_ = node_graph From 21027bcb225e44e634ede8ca92d80dabe0b302a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:45:16 +0000 Subject: [PATCH 3/5] Address review: use recursive unlink for no-args, move notebook section Co-authored-by: ConnorStoneAstro <78555321+ConnorStoneAstro@users.noreply.github.com> --- docs/source/notebooks/BeginnersGuide.ipynb | 69 +++++++++++----------- src/caskade/base.py | 3 +- 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/docs/source/notebooks/BeginnersGuide.ipynb b/docs/source/notebooks/BeginnersGuide.ipynb index 67a7f86..e1e05a4 100644 --- a/docs/source/notebooks/BeginnersGuide.ipynb +++ b/docs/source/notebooks/BeginnersGuide.ipynb @@ -312,6 +312,42 @@ "As you can see, a `pointer` parameter is represented in the graph as a shaded arrow. It will now return the same value as the `x0` parameter in `secondsim`." ] }, + { + "cell_type": "markdown", + "id": "link_unlink_md", + "metadata": {}, + "source": [ + "### Linking and unlinking params\n", + "\n", + "Pointer parameters can be linked to and unlinked from other nodes. ", + "Use `link(node)` to connect a child node, and `unlink(node)` (or a key string) to disconnect a specific child. ", + "Calling `unlink()` with no arguments removes **all** children at once, acting as a convenient clear." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "link_unlink_code", + "metadata": {}, + "outputs": [], + "source": [ + "time_param = ck.Param(\"mytime\") # a standalone param to link against\n", + "shared_x0 = ck.Param(\"shared_x0\", shape=(2,))\n", + "\n", + "# Link a child node using a key or by passing the node directly\n", + "shared_x0.link(\"mytime\", time_param)\n", + "print(\"Children after link:\", list(shared_x0.children))\n", + "\n", + "# Unlink a specific child by key or node reference\n", + "shared_x0.unlink(\"mytime\")\n", + "print(\"Children after unlink(key):\", list(shared_x0.children))\n", + "\n", + "# Re-link and then clear all children at once\n", + "shared_x0.link(\"mytime\", time_param)\n", + "shared_x0.unlink() # removes all children\n", + "print(\"Children after unlink():\", list(shared_x0.children))" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -391,39 +427,6 @@ "combinedsim.graphviz()" ] }, - { - "cell_type": "markdown", - "id": "unlink_md", - "metadata": {}, - "source": [ - "### Unlinking nodes\n", - "\n", - "You can unlink a child node from a parent by calling `unlink` with the key or node to remove. ", - "Calling `unlink()` with no arguments is a convenient way to remove **all** children at once." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "unlink_code", - "metadata": {}, - "outputs": [], - "source": [ - "# Unlink a specific child by passing its key or Node object\n", - "secondsim.x0.unlink(simtime)\n", - "print(\"After unlinking simtime from secondsim.x0:\", secondsim.x0.children)\n", - "\n", - "# Re-link it\n", - "secondsim.x0.link(simtime)\n", - "\n", - "# Calling unlink() with no arguments removes all children at once\n", - "secondsim.x0.unlink()\n", - "print(\"After unlink() with no args:\", secondsim.x0.children)\n", - "\n", - "# Re-link for the rest of the tutorial\n", - "secondsim.x0.link(simtime)" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/src/caskade/base.py b/src/caskade/base.py index ff498eb..65dc8d3 100644 --- a/src/caskade/base.py +++ b/src/caskade/base.py @@ -220,8 +220,7 @@ def unlink(self, key: Union[str, "Node", list, tuple, None] = None): default), all children are unlinked. """ if key is None: - for k in list(self.children): - self._unlink(k) + self.unlink(list(self.children)) return if isinstance(key, Node): for node in self.children: From d4fc89afc9eb019b834699261e54b1d772c263f0 Mon Sep 17 00:00:00 2001 From: Connor Stone Date: Tue, 3 Mar 2026 11:20:53 -0500 Subject: [PATCH 4/5] add check for trying to unlink key that isnt a child --- pyproject.toml | 2 +- src/caskade/base.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4ac2230..0f62a30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ keywords = [ "pytorch" ] classifiers=[ - "Development Status :: 1 - Planning", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", diff --git a/src/caskade/base.py b/src/caskade/base.py index 65dc8d3..c958a71 100644 --- a/src/caskade/base.py +++ b/src/caskade/base.py @@ -227,6 +227,8 @@ def unlink(self, key: Union[str, "Node", list, tuple, None] = None): if self.children[node] is key: key = node break + else: + raise KeyError(f"Node {key.name} not found in parent {self.name}") elif isinstance(key, (tuple, list)): for k in key: self.unlink(k) From f6ef9623dc5cf7a391ec4d3732fa1462271ce7e6 Mon Sep 17 00:00:00 2001 From: Connor Stone Date: Tue, 3 Mar 2026 11:23:27 -0500 Subject: [PATCH 5/5] make test for unlink missing key --- src/caskade/base.py | 2 ++ tests/test_base.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/caskade/base.py b/src/caskade/base.py index c958a71..754c993 100644 --- a/src/caskade/base.py +++ b/src/caskade/base.py @@ -233,6 +233,8 @@ def unlink(self, key: Union[str, "Node", list, tuple, None] = None): for k in key: self.unlink(k) return + if key not in self.children: + raise KeyError(f"Child key '{key}' not found in parent {self.name}") self.__delattr__(key) def topological_ordering(self) -> tuple["Node"]: diff --git a/tests/test_base.py b/tests/test_base.py index 9b474bc..0929710 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -85,6 +85,10 @@ def test_linking(node_graph): assert e in a.topological_ordering() with pytest.raises(AttributeError): a.e + with pytest.raises(KeyError): + a.unlink(e) + with pytest.raises(KeyError): + a.unlink("e") a.unlink((b, c)) # Check unlink with no arguments clears all children