Skip to content

Commit 729c4fa

Browse files
jsignellIllviljandcherian
authored
Shorten text repr for DataTree (#10139)
Co-authored-by: Illviljan <[email protected]> Co-authored-by: Deepak Cherian <[email protected]>
1 parent 9807780 commit 729c4fa

File tree

7 files changed

+207
-24
lines changed

7 files changed

+207
-24
lines changed

doc/whats-new.rst

+6
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ Breaking changes
5252
now return objects indexed by :py:meth:`pandas.IntervalArray` objects,
5353
instead of numpy object arrays containing tuples. This change enables interval-aware indexing of
5454
such Xarray objects. (:pull:`9671`). By `Ilan Gold <https://github.com/ilan-gold>`_.
55+
- The html and text ``repr`` for ``DataTree`` are now truncated. Up to 6 children are displayed
56+
for each node -- the first 3 and the last 3 children -- with a ``...`` between them. The number
57+
of children to include in the display is configurable via options. For instance use
58+
``set_options(display_max_children=8)`` to display 8 children rather than the default 6. (:pull:`10139`)
59+
By `Julia Signell <https://github.com/jsignell>`_.
60+
5561

5662
Deprecations
5763
~~~~~~~~~~~~

xarray/core/datatree_render.py

+40-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from collections import namedtuple
1212
from collections.abc import Iterable, Iterator
13+
from math import ceil
1314
from typing import TYPE_CHECKING
1415

1516
if TYPE_CHECKING:
@@ -79,6 +80,7 @@ def __init__(
7980
style=None,
8081
childiter: type = list,
8182
maxlevel: int | None = None,
83+
maxchildren: int | None = None,
8284
):
8385
"""
8486
Render tree starting at `node`.
@@ -88,6 +90,7 @@ def __init__(
8890
Iterables that change the order of children cannot be used
8991
(e.g., `reversed`).
9092
maxlevel: Limit rendering to this depth.
93+
maxchildren: Limit number of children at each node.
9194
:any:`RenderDataTree` is an iterator, returning a tuple with 3 items:
9295
`pre`
9396
tree prefix.
@@ -160,6 +163,16 @@ def __init__(
160163
root
161164
├── sub0
162165
└── sub1
166+
167+
# `maxchildren` limits the number of children per node
168+
169+
>>> print(RenderDataTree(root, maxchildren=1).by_attr("name"))
170+
root
171+
├── sub0
172+
│ ├── sub0B
173+
│ ...
174+
...
175+
163176
"""
164177
if style is None:
165178
style = ContStyle()
@@ -169,24 +182,44 @@ def __init__(
169182
self.style = style
170183
self.childiter = childiter
171184
self.maxlevel = maxlevel
185+
self.maxchildren = maxchildren
172186

173187
def __iter__(self) -> Iterator[Row]:
174188
return self.__next(self.node, tuple())
175189

176190
def __next(
177-
self, node: DataTree, continues: tuple[bool, ...], level: int = 0
191+
self,
192+
node: DataTree,
193+
continues: tuple[bool, ...],
194+
level: int = 0,
178195
) -> Iterator[Row]:
179196
yield RenderDataTree.__item(node, continues, self.style)
180197
children = node.children.values()
181198
level += 1
182199
if children and (self.maxlevel is None or level < self.maxlevel):
200+
nchildren = len(children)
183201
children = self.childiter(children)
184-
for child, is_last in _is_last(children):
185-
yield from self.__next(child, continues + (not is_last,), level=level)
202+
for i, (child, is_last) in enumerate(_is_last(children)):
203+
if (
204+
self.maxchildren is None
205+
or i < ceil(self.maxchildren / 2)
206+
or i >= ceil(nchildren - self.maxchildren / 2)
207+
):
208+
yield from self.__next(
209+
child,
210+
continues + (not is_last,),
211+
level=level,
212+
)
213+
if (
214+
self.maxchildren is not None
215+
and nchildren > self.maxchildren
216+
and i == ceil(self.maxchildren / 2)
217+
):
218+
yield RenderDataTree.__item("...", continues, self.style)
186219

187220
@staticmethod
188221
def __item(
189-
node: DataTree, continues: tuple[bool, ...], style: AbstractStyle
222+
node: DataTree | str, continues: tuple[bool, ...], style: AbstractStyle
190223
) -> Row:
191224
if not continues:
192225
return Row("", "", node)
@@ -244,6 +277,9 @@ def by_attr(self, attrname: str = "name") -> str:
244277

245278
def get() -> Iterator[str]:
246279
for pre, fill, node in self:
280+
if isinstance(node, str):
281+
yield f"{fill}{node}"
282+
continue
247283
attr = (
248284
attrname(node)
249285
if callable(attrname)

xarray/core/formatting.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -1139,14 +1139,21 @@ def _datatree_node_repr(node: DataTree, show_inherited: bool) -> str:
11391139

11401140
def datatree_repr(dt: DataTree) -> str:
11411141
"""A printable representation of the structure of this entire tree."""
1142-
renderer = RenderDataTree(dt)
1142+
max_children = OPTIONS["display_max_children"]
1143+
1144+
renderer = RenderDataTree(dt, maxchildren=max_children)
11431145

11441146
name_info = "" if dt.name is None else f" {dt.name!r}"
11451147
header = f"<xarray.DataTree{name_info}>"
11461148

11471149
lines = [header]
11481150
show_inherited = True
1151+
11491152
for pre, fill, node in renderer:
1153+
if isinstance(node, str):
1154+
lines.append(f"{fill}{node}")
1155+
continue
1156+
11501157
node_repr = _datatree_node_repr(node, show_inherited=show_inherited)
11511158
show_inherited = False # only show inherited coords on the root
11521159

xarray/core/formatting_html.py

+31-19
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@
66
from functools import lru_cache, partial
77
from html import escape
88
from importlib.resources import files
9-
from typing import TYPE_CHECKING
9+
from math import ceil
10+
from typing import TYPE_CHECKING, Literal
1011

1112
from xarray.core.formatting import (
1213
inherited_vars,
1314
inline_index_repr,
1415
inline_variable_array_repr,
1516
short_data_repr,
1617
)
17-
from xarray.core.options import _get_boolean_with_default
18+
from xarray.core.options import OPTIONS, _get_boolean_with_default
1819

1920
STATIC_FILES = (
2021
("xarray.static.html", "icons-svg-inline.html"),
@@ -192,16 +193,29 @@ def collapsible_section(
192193

193194

194195
def _mapping_section(
195-
mapping, name, details_func, max_items_collapse, expand_option_name, enabled=True
196+
mapping,
197+
name,
198+
details_func,
199+
max_items_collapse,
200+
expand_option_name,
201+
enabled=True,
202+
max_option_name: Literal["display_max_children"] | None = None,
196203
) -> str:
197204
n_items = len(mapping)
198205
expanded = _get_boolean_with_default(
199206
expand_option_name, n_items < max_items_collapse
200207
)
201208
collapsed = not expanded
202209

210+
inline_details = ""
211+
if max_option_name and max_option_name in OPTIONS:
212+
max_items = int(OPTIONS[max_option_name])
213+
if n_items > max_items:
214+
inline_details = f"({max_items}/{n_items})"
215+
203216
return collapsible_section(
204217
name,
218+
inline_details=inline_details,
205219
details=details_func(mapping),
206220
n_items=n_items,
207221
enabled=enabled,
@@ -348,26 +362,23 @@ def dataset_repr(ds) -> str:
348362

349363

350364
def summarize_datatree_children(children: Mapping[str, DataTree]) -> str:
351-
N_CHILDREN = len(children) - 1
352-
353-
# Get result from datatree_node_repr and wrap it
354-
lines_callback = lambda n, c, end: _wrap_datatree_repr(
355-
datatree_node_repr(n, c), end=end
356-
)
357-
358-
children_html = "".join(
359-
(
360-
lines_callback(n, c, end=False) # Long lines
361-
if i < N_CHILDREN
362-
else lines_callback(n, c, end=True)
363-
) # Short lines
364-
for i, (n, c) in enumerate(children.items())
365-
)
365+
MAX_CHILDREN = OPTIONS["display_max_children"]
366+
n_children = len(children)
367+
368+
children_html = []
369+
for i, (n, c) in enumerate(children.items()):
370+
if i < ceil(MAX_CHILDREN / 2) or i >= ceil(n_children - MAX_CHILDREN / 2):
371+
is_last = i == (n_children - 1)
372+
children_html.append(
373+
_wrap_datatree_repr(datatree_node_repr(n, c), end=is_last)
374+
)
375+
elif n_children > MAX_CHILDREN and i == ceil(MAX_CHILDREN / 2):
376+
children_html.append("<div>...</div>")
366377

367378
return "".join(
368379
[
369380
"<div style='display: inline-grid; grid-template-columns: 100%; grid-column: 1 / -1'>",
370-
children_html,
381+
"".join(children_html),
371382
"</div>",
372383
]
373384
)
@@ -378,6 +389,7 @@ def summarize_datatree_children(children: Mapping[str, DataTree]) -> str:
378389
name="Groups",
379390
details_func=summarize_datatree_children,
380391
max_items_collapse=1,
392+
max_option_name="display_max_children",
381393
expand_option_name="display_expand_groups",
382394
)
383395

xarray/core/options.py

+6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"chunk_manager",
1414
"cmap_divergent",
1515
"cmap_sequential",
16+
"display_max_children",
1617
"display_max_rows",
1718
"display_values_threshold",
1819
"display_style",
@@ -40,6 +41,7 @@ class T_Options(TypedDict):
4041
chunk_manager: str
4142
cmap_divergent: str | Colormap
4243
cmap_sequential: str | Colormap
44+
display_max_children: int
4345
display_max_rows: int
4446
display_values_threshold: int
4547
display_style: Literal["text", "html"]
@@ -67,6 +69,7 @@ class T_Options(TypedDict):
6769
"chunk_manager": "dask",
6870
"cmap_divergent": "RdBu_r",
6971
"cmap_sequential": "viridis",
72+
"display_max_children": 6,
7073
"display_max_rows": 12,
7174
"display_values_threshold": 200,
7275
"display_style": "html",
@@ -99,6 +102,7 @@ def _positive_integer(value: Any) -> bool:
99102
_VALIDATORS = {
100103
"arithmetic_broadcast": lambda value: isinstance(value, bool),
101104
"arithmetic_join": _JOIN_OPTIONS.__contains__,
105+
"display_max_children": _positive_integer,
102106
"display_max_rows": _positive_integer,
103107
"display_values_threshold": _positive_integer,
104108
"display_style": _DISPLAY_OPTIONS.__contains__,
@@ -222,6 +226,8 @@ class set_options:
222226
* ``True`` : to always expand indexes
223227
* ``False`` : to always collapse indexes
224228
* ``default`` : to expand unless over a pre-defined limit (always collapse for html style)
229+
display_max_children : int, default: 6
230+
Maximum number of children to display for each node in a DataTree.
225231
display_max_rows : int, default: 12
226232
Maximum display rows.
227233
display_values_threshold : int, default: 200

xarray/tests/test_datatree.py

+70
Original file line numberDiff line numberDiff line change
@@ -1219,6 +1219,76 @@ def test_repr_two_children(self) -> None:
12191219
).strip()
12201220
assert result == expected
12211221

1222+
def test_repr_truncates_nodes(self) -> None:
1223+
# construct a datatree with 50 nodes
1224+
number_of_files = 10
1225+
number_of_groups = 5
1226+
tree_dict = {}
1227+
for f in range(number_of_files):
1228+
for g in range(number_of_groups):
1229+
tree_dict[f"file_{f}/group_{g}"] = Dataset({"g": f * g})
1230+
1231+
tree = DataTree.from_dict(tree_dict)
1232+
with xr.set_options(display_max_children=3):
1233+
result = repr(tree)
1234+
1235+
expected = dedent(
1236+
"""
1237+
<xarray.DataTree>
1238+
Group: /
1239+
├── Group: /file_0
1240+
│ ├── Group: /file_0/group_0
1241+
│ │ Dimensions: ()
1242+
│ │ Data variables:
1243+
│ │ g int64 8B 0
1244+
│ ├── Group: /file_0/group_1
1245+
│ │ Dimensions: ()
1246+
│ │ Data variables:
1247+
│ │ g int64 8B 0
1248+
│ ...
1249+
│ └── Group: /file_0/group_4
1250+
│ Dimensions: ()
1251+
│ Data variables:
1252+
│ g int64 8B 0
1253+
├── Group: /file_1
1254+
│ ├── Group: /file_1/group_0
1255+
│ │ Dimensions: ()
1256+
│ │ Data variables:
1257+
│ │ g int64 8B 0
1258+
│ ├── Group: /file_1/group_1
1259+
│ │ Dimensions: ()
1260+
│ │ Data variables:
1261+
│ │ g int64 8B 1
1262+
│ ...
1263+
│ └── Group: /file_1/group_4
1264+
│ Dimensions: ()
1265+
│ Data variables:
1266+
│ g int64 8B 4
1267+
...
1268+
└── Group: /file_9
1269+
├── Group: /file_9/group_0
1270+
│ Dimensions: ()
1271+
│ Data variables:
1272+
│ g int64 8B 0
1273+
├── Group: /file_9/group_1
1274+
│ Dimensions: ()
1275+
│ Data variables:
1276+
│ g int64 8B 9
1277+
...
1278+
└── Group: /file_9/group_4
1279+
Dimensions: ()
1280+
Data variables:
1281+
g int64 8B 36
1282+
"""
1283+
).strip()
1284+
assert expected == result
1285+
1286+
with xr.set_options(display_max_children=10):
1287+
result = repr(tree)
1288+
1289+
for key in tree_dict:
1290+
assert key in result
1291+
12221292
def test_repr_inherited_dims(self) -> None:
12231293
tree = DataTree.from_dict(
12241294
{

0 commit comments

Comments
 (0)