Skip to content
forked from pydata/xarray

Commit 8db6bc9

Browse files
authored
Forbid modifying names of DataTree objects with parents (pydata#9494)
1 parent 18e5c87 commit 8db6bc9

File tree

2 files changed

+40
-10
lines changed

2 files changed

+40
-10
lines changed

Diff for: xarray/core/treenode.py

+21-9
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,14 @@ def same_tree(self, other: Tree) -> bool:
640640
AnyNamedNode = TypeVar("AnyNamedNode", bound="NamedNode")
641641

642642

643+
def _validate_name(name: str | None) -> None:
644+
if name is not None:
645+
if not isinstance(name, str):
646+
raise TypeError("node name must be a string or None")
647+
if "/" in name:
648+
raise ValueError("node names cannot contain forward slashes")
649+
650+
643651
class NamedNode(TreeNode, Generic[Tree]):
644652
"""
645653
A TreeNode which knows its own name.
@@ -653,8 +661,8 @@ class NamedNode(TreeNode, Generic[Tree]):
653661

654662
def __init__(self, name=None, children=None):
655663
super().__init__(children=children)
656-
self._name = None
657-
self.name = name
664+
_validate_name(name)
665+
self._name = name
658666

659667
@property
660668
def name(self) -> str | None:
@@ -663,11 +671,13 @@ def name(self) -> str | None:
663671

664672
@name.setter
665673
def name(self, name: str | None) -> None:
666-
if name is not None:
667-
if not isinstance(name, str):
668-
raise TypeError("node name must be a string or None")
669-
if "/" in name:
670-
raise ValueError("node names cannot contain forward slashes")
674+
if self.parent is not None:
675+
raise ValueError(
676+
"cannot set the name of a node which already has a parent. "
677+
"Consider creating a detached copy of this node via .copy() "
678+
"on the parent node."
679+
)
680+
_validate_name(name)
671681
self._name = name
672682

673683
def __repr__(self, level=0):
@@ -677,11 +687,13 @@ def __repr__(self, level=0):
677687
return repr_value
678688

679689
def __str__(self) -> str:
680-
return f"NamedNode('{self.name}')" if self.name else "NamedNode()"
690+
name_repr = repr(self.name) if self.name is not None else ""
691+
return f"NamedNode({name_repr})"
681692

682693
def _post_attach(self: AnyNamedNode, parent: AnyNamedNode, name: str) -> None:
683694
"""Ensures child has name attribute corresponding to key under which it has been stored."""
684-
self.name = name
695+
_validate_name(name) # is this check redundant?
696+
self._name = name
685697

686698
def _copy_node(
687699
self: AnyNamedNode,

Diff for: xarray/tests/test_datatree.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,28 @@ def test_empty(self):
2424
assert dt.children == {}
2525
assert_identical(dt.to_dataset(), xr.Dataset())
2626

27-
def test_unnamed(self):
27+
def test_name(self):
2828
dt = DataTree()
2929
assert dt.name is None
3030

31+
dt = DataTree(name="foo")
32+
assert dt.name == "foo"
33+
34+
dt.name = "bar"
35+
assert dt.name == "bar"
36+
37+
dt = DataTree(children={"foo": DataTree()})
38+
assert dt["/foo"].name == "foo"
39+
with pytest.raises(
40+
ValueError, match="cannot set the name of a node which already has a parent"
41+
):
42+
dt["/foo"].name = "bar"
43+
44+
detached = dt["/foo"].copy()
45+
assert detached.name == "foo"
46+
detached.name = "bar"
47+
assert detached.name == "bar"
48+
3149
def test_bad_names(self):
3250
with pytest.raises(TypeError):
3351
DataTree(name=5) # type: ignore[arg-type]

0 commit comments

Comments
 (0)