Skip to content

Commit 6d62719

Browse files
Migrate datatreee assertions/extensions/formatting (#8967)
* DAS-2067 - Migrate formatting.py. * DAS-2067 - Migrate datatree/extensions.py. * DAS-2067 - Migrate datatree/tests/test_dataset_api.py. * DAS-2067 - Migrate datatree_render.py. * DAS-2067 - Migrate DataTree assertions into xarray/testing/assertions.py. * DAS-2067 - Update doc/whats-new.rst. * DAS-2067 - Fix doctests for DataTreeRender.by_attr. * DAS-2067 - Fix comments in doctests examples for datatree_render. * DAS-2067 - Implement PR feedback, fix RenderDataTree.__str__. * DAS-2067 - Add overload for xarray.testing.assert_equal and xarray.testing.assert_identical. * DAS-2067 - Remove out-of-date comments. * Remove test of printing datatree --------- Co-authored-by: Tom Nicholas <tom@cworthy.org>
1 parent 8a23e24 commit 6d62719

23 files changed

+763
-945
lines changed

doc/whats-new.rst

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,17 @@ Bug fixes
4242

4343
Internal Changes
4444
~~~~~~~~~~~~~~~~
45-
- Migrates ``formatting_html`` functionality for `DataTree` into ``xarray/core`` (:pull: `8930`)
45+
- Migrates ``formatting_html`` functionality for ``DataTree`` into ``xarray/core`` (:pull: `8930`)
4646
By `Eni Awowale <https://github.com/eni-awowale>`_, `Julia Signell <https://github.com/jsignell>`_
4747
and `Tom Nicholas <https://github.com/TomNicholas>`_.
4848
- Migrates ``datatree_mapping`` functionality into ``xarray/core`` (:pull:`8948`)
4949
By `Matt Savoie <https://github.com/flamingbear>`_ `Owen Littlejohns
50-
<https://github.com/owenlittlejohns>` and `Tom Nicholas <https://github.com/TomNicholas>`_.
50+
<https://github.com/owenlittlejohns>`_ and `Tom Nicholas <https://github.com/TomNicholas>`_.
51+
- Migrates ``extensions``, ``formatting`` and ``datatree_render`` functionality for
52+
``DataTree`` into ``xarray/core``. Also migrates ``testing`` functionality into
53+
``xarray/testing/assertions`` for ``DataTree``. (:pull:`8967`)
54+
By `Owen Littlejohns <https://github.com/owenlittlejohns>`_ and
55+
`Tom Nicholas <https://github.com/TomNicholas>`_.
5156

5257

5358
.. _whats-new.2024.03.0:

xarray/core/datatree.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
check_isomorphic,
2424
map_over_subtree,
2525
)
26+
from xarray.core.datatree_render import RenderDataTree
27+
from xarray.core.formatting import datatree_repr
2628
from xarray.core.formatting_html import (
2729
datatree_repr as datatree_repr_html,
2830
)
@@ -40,13 +42,11 @@
4042
)
4143
from xarray.core.variable import Variable
4244
from xarray.datatree_.datatree.common import TreeAttrAccessMixin
43-
from xarray.datatree_.datatree.formatting import datatree_repr
4445
from xarray.datatree_.datatree.ops import (
4546
DataTreeArithmeticMixin,
4647
MappedDatasetMethodsMixin,
4748
MappedDataWithCoords,
4849
)
49-
from xarray.datatree_.datatree.render import RenderTree
5050

5151
try:
5252
from xarray.core.variable import calculate_dimensions
@@ -1451,7 +1451,7 @@ def pipe(
14511451

14521452
def render(self):
14531453
"""Print tree structure, including any data stored at each node."""
1454-
for pre, fill, node in RenderTree(self):
1454+
for pre, fill, node in RenderDataTree(self):
14551455
print(f"{pre}DataTree('{self.name}')")
14561456
for ds_line in repr(node.ds)[1:]:
14571457
print(f"{fill}{ds_line}")

xarray/core/datatree_mapping.py

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import functools
44
import sys
55
from itertools import repeat
6-
from textwrap import dedent
76
from typing import TYPE_CHECKING, Callable
87

9-
from xarray import DataArray, Dataset
10-
from xarray.core.iterators import LevelOrderIter
8+
from xarray.core.dataarray import DataArray
9+
from xarray.core.dataset import Dataset
10+
from xarray.core.formatting import diff_treestructure
1111
from xarray.core.treenode import NodePath, TreeNode
1212

1313
if TYPE_CHECKING:
@@ -71,37 +71,6 @@ def check_isomorphic(
7171
raise TreeIsomorphismError("DataTree objects are not isomorphic:\n" + diff)
7272

7373

74-
def diff_treestructure(a: DataTree, b: DataTree, require_names_equal: bool) -> str:
75-
"""
76-
Return a summary of why two trees are not isomorphic.
77-
If they are isomorphic return an empty string.
78-
"""
79-
80-
# Walking nodes in "level-order" fashion means walking down from the root breadth-first.
81-
# Checking for isomorphism by walking in this way implicitly assumes that the tree is an ordered tree
82-
# (which it is so long as children are stored in a tuple or list rather than in a set).
83-
for node_a, node_b in zip(LevelOrderIter(a), LevelOrderIter(b)):
84-
path_a, path_b = node_a.path, node_b.path
85-
86-
if require_names_equal and node_a.name != node_b.name:
87-
diff = dedent(
88-
f"""\
89-
Node '{path_a}' in the left object has name '{node_a.name}'
90-
Node '{path_b}' in the right object has name '{node_b.name}'"""
91-
)
92-
return diff
93-
94-
if len(node_a.children) != len(node_b.children):
95-
diff = dedent(
96-
f"""\
97-
Number of children on node '{path_a}' of the left object: {len(node_a.children)}
98-
Number of children on node '{path_b}' of the right object: {len(node_b.children)}"""
99-
)
100-
return diff
101-
102-
return ""
103-
104-
10574
def map_over_subtree(func: Callable) -> Callable:
10675
"""
10776
Decorator which turns a function which acts on (and returns) Datasets into one which acts on and returns DataTrees.

xarray/core/datatree_render.py

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
"""
2+
String Tree Rendering. Copied from anytree.
3+
4+
Minor changes to `RenderDataTree` include accessing `children.values()`, and
5+
type hints.
6+
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from collections import namedtuple
12+
from collections.abc import Iterable, Iterator
13+
from typing import TYPE_CHECKING
14+
15+
if TYPE_CHECKING:
16+
from xarray.core.datatree import DataTree
17+
18+
Row = namedtuple("Row", ("pre", "fill", "node"))
19+
20+
21+
class AbstractStyle:
22+
def __init__(self, vertical: str, cont: str, end: str):
23+
"""
24+
Tree Render Style.
25+
Args:
26+
vertical: Sign for vertical line.
27+
cont: Chars for a continued branch.
28+
end: Chars for the last branch.
29+
"""
30+
super().__init__()
31+
self.vertical = vertical
32+
self.cont = cont
33+
self.end = end
34+
assert (
35+
len(cont) == len(vertical) == len(end)
36+
), f"'{vertical}', '{cont}' and '{end}' need to have equal length"
37+
38+
@property
39+
def empty(self) -> str:
40+
"""Empty string as placeholder."""
41+
return " " * len(self.end)
42+
43+
def __repr__(self) -> str:
44+
return f"{self.__class__.__name__}()"
45+
46+
47+
class ContStyle(AbstractStyle):
48+
def __init__(self):
49+
"""
50+
Continued style, without gaps.
51+
52+
>>> from xarray.core.datatree import DataTree
53+
>>> from xarray.core.datatree_render import RenderDataTree
54+
>>> root = DataTree(name="root")
55+
>>> s0 = DataTree(name="sub0", parent=root)
56+
>>> s0b = DataTree(name="sub0B", parent=s0)
57+
>>> s0a = DataTree(name="sub0A", parent=s0)
58+
>>> s1 = DataTree(name="sub1", parent=root)
59+
>>> print(RenderDataTree(root))
60+
DataTree('root', parent=None)
61+
├── DataTree('sub0')
62+
│ ├── DataTree('sub0B')
63+
│ └── DataTree('sub0A')
64+
└── DataTree('sub1')
65+
"""
66+
super().__init__("\u2502 ", "\u251c\u2500\u2500 ", "\u2514\u2500\u2500 ")
67+
68+
69+
class RenderDataTree:
70+
def __init__(
71+
self,
72+
node: DataTree,
73+
style=ContStyle(),
74+
childiter: type = list,
75+
maxlevel: int | None = None,
76+
):
77+
"""
78+
Render tree starting at `node`.
79+
Keyword Args:
80+
style (AbstractStyle): Render Style.
81+
childiter: Child iterator. Note, due to the use of node.children.values(),
82+
Iterables that change the order of children cannot be used
83+
(e.g., `reversed`).
84+
maxlevel: Limit rendering to this depth.
85+
:any:`RenderDataTree` is an iterator, returning a tuple with 3 items:
86+
`pre`
87+
tree prefix.
88+
`fill`
89+
filling for multiline entries.
90+
`node`
91+
:any:`NodeMixin` object.
92+
It is up to the user to assemble these parts to a whole.
93+
94+
Examples
95+
--------
96+
97+
>>> from xarray import Dataset
98+
>>> from xarray.core.datatree import DataTree
99+
>>> from xarray.core.datatree_render import RenderDataTree
100+
>>> root = DataTree(name="root", data=Dataset({"a": 0, "b": 1}))
101+
>>> s0 = DataTree(name="sub0", parent=root, data=Dataset({"c": 2, "d": 3}))
102+
>>> s0b = DataTree(name="sub0B", parent=s0, data=Dataset({"e": 4}))
103+
>>> s0a = DataTree(name="sub0A", parent=s0, data=Dataset({"f": 5, "g": 6}))
104+
>>> s1 = DataTree(name="sub1", parent=root, data=Dataset({"h": 7}))
105+
106+
# Simple one line:
107+
108+
>>> for pre, _, node in RenderDataTree(root):
109+
... print(f"{pre}{node.name}")
110+
...
111+
root
112+
├── sub0
113+
│ ├── sub0B
114+
│ └── sub0A
115+
└── sub1
116+
117+
# Multiline:
118+
119+
>>> for pre, fill, node in RenderDataTree(root):
120+
... print(f"{pre}{node.name}")
121+
... for variable in node.variables:
122+
... print(f"{fill}{variable}")
123+
...
124+
root
125+
a
126+
b
127+
├── sub0
128+
│ c
129+
│ d
130+
│ ├── sub0B
131+
│ │ e
132+
│ └── sub0A
133+
│ f
134+
│ g
135+
└── sub1
136+
h
137+
138+
:any:`by_attr` simplifies attribute rendering and supports multiline:
139+
>>> print(RenderDataTree(root).by_attr())
140+
root
141+
├── sub0
142+
│ ├── sub0B
143+
│ └── sub0A
144+
└── sub1
145+
146+
# `maxlevel` limits the depth of the tree:
147+
148+
>>> print(RenderDataTree(root, maxlevel=2).by_attr("name"))
149+
root
150+
├── sub0
151+
└── sub1
152+
"""
153+
if not isinstance(style, AbstractStyle):
154+
style = style()
155+
self.node = node
156+
self.style = style
157+
self.childiter = childiter
158+
self.maxlevel = maxlevel
159+
160+
def __iter__(self) -> Iterator[Row]:
161+
return self.__next(self.node, tuple())
162+
163+
def __next(
164+
self, node: DataTree, continues: tuple[bool, ...], level: int = 0
165+
) -> Iterator[Row]:
166+
yield RenderDataTree.__item(node, continues, self.style)
167+
children = node.children.values()
168+
level += 1
169+
if children and (self.maxlevel is None or level < self.maxlevel):
170+
children = self.childiter(children)
171+
for child, is_last in _is_last(children):
172+
yield from self.__next(child, continues + (not is_last,), level=level)
173+
174+
@staticmethod
175+
def __item(
176+
node: DataTree, continues: tuple[bool, ...], style: AbstractStyle
177+
) -> Row:
178+
if not continues:
179+
return Row("", "", node)
180+
else:
181+
items = [style.vertical if cont else style.empty for cont in continues]
182+
indent = "".join(items[:-1])
183+
branch = style.cont if continues[-1] else style.end
184+
pre = indent + branch
185+
fill = "".join(items)
186+
return Row(pre, fill, node)
187+
188+
def __str__(self) -> str:
189+
return str(self.node)
190+
191+
def __repr__(self) -> str:
192+
classname = self.__class__.__name__
193+
args = [
194+
repr(self.node),
195+
f"style={repr(self.style)}",
196+
f"childiter={repr(self.childiter)}",
197+
]
198+
return f"{classname}({', '.join(args)})"
199+
200+
def by_attr(self, attrname: str = "name") -> str:
201+
"""
202+
Return rendered tree with node attribute `attrname`.
203+
204+
Examples
205+
--------
206+
207+
>>> from xarray import Dataset
208+
>>> from xarray.core.datatree import DataTree
209+
>>> from xarray.core.datatree_render import RenderDataTree
210+
>>> root = DataTree(name="root")
211+
>>> s0 = DataTree(name="sub0", parent=root)
212+
>>> s0b = DataTree(
213+
... name="sub0B", parent=s0, data=Dataset({"foo": 4, "bar": 109})
214+
... )
215+
>>> s0a = DataTree(name="sub0A", parent=s0)
216+
>>> s1 = DataTree(name="sub1", parent=root)
217+
>>> s1a = DataTree(name="sub1A", parent=s1)
218+
>>> s1b = DataTree(name="sub1B", parent=s1, data=Dataset({"bar": 8}))
219+
>>> s1c = DataTree(name="sub1C", parent=s1)
220+
>>> s1ca = DataTree(name="sub1Ca", parent=s1c)
221+
>>> print(RenderDataTree(root).by_attr("name"))
222+
root
223+
├── sub0
224+
│ ├── sub0B
225+
│ └── sub0A
226+
└── sub1
227+
├── sub1A
228+
├── sub1B
229+
└── sub1C
230+
└── sub1Ca
231+
"""
232+
233+
def get() -> Iterator[str]:
234+
for pre, fill, node in self:
235+
attr = (
236+
attrname(node)
237+
if callable(attrname)
238+
else getattr(node, attrname, "")
239+
)
240+
if isinstance(attr, (list, tuple)):
241+
lines = attr
242+
else:
243+
lines = str(attr).split("\n")
244+
yield f"{pre}{lines[0]}"
245+
for line in lines[1:]:
246+
yield f"{fill}{line}"
247+
248+
return "\n".join(get())
249+
250+
251+
def _is_last(iterable: Iterable) -> Iterator[tuple[DataTree, bool]]:
252+
iter_ = iter(iterable)
253+
try:
254+
nextitem = next(iter_)
255+
except StopIteration:
256+
pass
257+
else:
258+
item = nextitem
259+
while True:
260+
try:
261+
nextitem = next(iter_)
262+
yield item, False
263+
except StopIteration:
264+
yield nextitem, True
265+
break
266+
item = nextitem

0 commit comments

Comments
 (0)