From c34530b3b7cb36b70ff0b6c0995c73bf3c14534b Mon Sep 17 00:00:00 2001 From: Julia Signell Date: Mon, 17 Mar 2025 17:01:25 -0400 Subject: [PATCH 01/13] Shorten text repr for ``DataTree`` * only show max 12 children in text ``DataTree`` repr * use ``set_options(display_max_rows=12)`` to configure this setting * always include last item in ``DataTree`` * insert "..." before last item in ``DataTree`` --- xarray/core/datatree_render.py | 33 +++++++++++++++++++++++++++++++-- xarray/core/formatting.py | 14 ++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/xarray/core/datatree_render.py b/xarray/core/datatree_render.py index 11336cd9689..fd406f75fe9 100644 --- a/xarray/core/datatree_render.py +++ b/xarray/core/datatree_render.py @@ -79,6 +79,7 @@ def __init__( style=None, childiter: type = list, maxlevel: int | None = None, + maxchildren: int | None = None, ): """ Render tree starting at `node`. @@ -88,6 +89,10 @@ def __init__( Iterables that change the order of children cannot be used (e.g., `reversed`). maxlevel: Limit rendering to this depth. + maxchildren: Limit number of children to roughly this number. In practice, + for an arbitrarily large DataTree the number of children returned + will be (maxchildren * maxchildren - 1) / 2. The last child is also + included. :any:`RenderDataTree` is an iterator, returning a tuple with 3 items: `pre` tree prefix. @@ -160,6 +165,14 @@ def __init__( root ├── sub0 └── sub1 + + # `maxchildren` roughly limits the total number of children + + >>> print(RenderDataTree(root, maxchildren=3)) + root + ├── sub0 + │ ├── sub0B + └── sub1 """ if style is None: style = ContStyle() @@ -169,12 +182,17 @@ def __init__( self.style = style self.childiter = childiter self.maxlevel = maxlevel + self.maxchildren = maxchildren def __iter__(self) -> Iterator[Row]: return self.__next(self.node, tuple()) def __next( - self, node: DataTree, continues: tuple[bool, ...], level: int = 0 + self, + node: DataTree, + continues: tuple[bool, ...], + level: int = 0, + nchildren: int = 0, ) -> Iterator[Row]: yield RenderDataTree.__item(node, continues, self.style) children = node.children.values() @@ -182,7 +200,18 @@ def __next( if children and (self.maxlevel is None or level < self.maxlevel): children = self.childiter(children) for child, is_last in _is_last(children): - yield from self.__next(child, continues + (not is_last,), level=level) + nchildren += 1 + if ( + self.maxchildren is None + or nchildren < self.maxchildren + or (not any(continues) and is_last) + ): + yield from self.__next( + child, + continues + (not is_last,), + level=level, + nchildren=nchildren, + ) @staticmethod def __item( diff --git a/xarray/core/formatting.py b/xarray/core/formatting.py index 993cddf2b57..13360148a9f 100644 --- a/xarray/core/formatting.py +++ b/xarray/core/formatting.py @@ -1137,14 +1137,24 @@ def _datatree_node_repr(node: DataTree, show_inherited: bool) -> str: def datatree_repr(dt: DataTree) -> str: """A printable representation of the structure of this entire tree.""" - renderer = RenderDataTree(dt) + max_rows = OPTIONS["display_max_rows"] + + renderer = RenderDataTree(dt, maxchildren=max_rows) name_info = "" if dt.name is None else f" {dt.name!r}" header = f"" lines = [header] show_inherited = True - for pre, fill, node in renderer: + + rendered_items = list(renderer) + for i, (pre, fill, node) in enumerate(rendered_items): + if len(rendered_items) > max_rows: + if i == max_rows: + lines.append("...") + if i >= max_rows and i != (len(rendered_items) - 1): + continue + node_repr = _datatree_node_repr(node, show_inherited=show_inherited) show_inherited = False # only show inherited coords on the root From fac6e5d99252300ef8b80528cbc5920511dd8c57 Mon Sep 17 00:00:00 2001 From: Julia Signell Date: Mon, 17 Mar 2025 17:50:15 -0400 Subject: [PATCH 02/13] Truncate children in html repr --- xarray/core/formatting_html.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/xarray/core/formatting_html.py b/xarray/core/formatting_html.py index eb9073cd869..b9c03453018 100644 --- a/xarray/core/formatting_html.py +++ b/xarray/core/formatting_html.py @@ -14,7 +14,7 @@ inline_variable_array_repr, short_data_repr, ) -from xarray.core.options import _get_boolean_with_default +from xarray.core.options import OPTIONS, _get_boolean_with_default STATIC_FILES = ( ("xarray.static.html", "icons-svg-inline.html"), @@ -192,16 +192,27 @@ def collapsible_section( def _mapping_section( - mapping, name, details_func, max_items_collapse, expand_option_name, enabled=True + mapping, + name, + details_func, + max_items_collapse, + expand_option_name, + enabled=True, + max_items_truncate: int | None = None, ) -> str: n_items = len(mapping) expanded = _get_boolean_with_default( expand_option_name, n_items < max_items_collapse ) collapsed = not expanded + truncated = max_items_truncate is not None and n_items > max_items_truncate + inline_details = ( + f"Only first {max_items_truncate} will show in dropdown" if truncated else "" + ) return collapsible_section( name, + inline_details=inline_details, details=details_func(mapping), n_items=n_items, enabled=enabled, @@ -349,19 +360,12 @@ def dataset_repr(ds) -> str: def summarize_datatree_children(children: Mapping[str, DataTree]) -> str: N_CHILDREN = len(children) - 1 - - # Get result from datatree_node_repr and wrap it - lines_callback = lambda n, c, end: _wrap_datatree_repr( - datatree_node_repr(n, c), end=end - ) + MAX_CHILDREN = OPTIONS["display_max_rows"] children_html = "".join( - ( - lines_callback(n, c, end=False) # Long lines - if i < N_CHILDREN - else lines_callback(n, c, end=True) - ) # Short lines + _wrap_datatree_repr(datatree_node_repr(n, c), end=i == N_CHILDREN) for i, (n, c) in enumerate(children.items()) + if i < MAX_CHILDREN ) return "".join( @@ -378,6 +382,7 @@ def summarize_datatree_children(children: Mapping[str, DataTree]) -> str: name="Groups", details_func=summarize_datatree_children, max_items_collapse=1, + max_items_truncate=OPTIONS["display_max_rows"], expand_option_name="display_expand_groups", ) @@ -422,7 +427,7 @@ def datatree_node_repr(group_title: str, node: DataTree, show_inherited=False) - return _obj_repr(ds, header_components, sections) -def _wrap_datatree_repr(r: str, end: bool = False) -> str: +def _wrap_datatree_repr(r: str, end: bool = False, skipped_some: bool = False) -> str: """ Wrap HTML representation with a tee to the left of it. From 51868e6ea2b63aca41b1507195a51d677d366cbc Mon Sep 17 00:00:00 2001 From: Julia Signell Date: Tue, 18 Mar 2025 10:34:56 -0400 Subject: [PATCH 03/13] Add tests --- xarray/core/datatree_render.py | 2 +- xarray/tests/test_datatree.py | 86 ++++++++++++++++++++++++++++ xarray/tests/test_formatting_html.py | 38 ++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) diff --git a/xarray/core/datatree_render.py b/xarray/core/datatree_render.py index fd406f75fe9..98cf738f20d 100644 --- a/xarray/core/datatree_render.py +++ b/xarray/core/datatree_render.py @@ -168,7 +168,7 @@ def __init__( # `maxchildren` roughly limits the total number of children - >>> print(RenderDataTree(root, maxchildren=3)) + >>> print(RenderDataTree(root, maxchildren=3).by_attr()) root ├── sub0 │ ├── sub0B diff --git a/xarray/tests/test_datatree.py b/xarray/tests/test_datatree.py index 55b809307e4..c7a8a561b08 100644 --- a/xarray/tests/test_datatree.py +++ b/xarray/tests/test_datatree.py @@ -1196,6 +1196,92 @@ def test_repr_two_children(self) -> None: ).strip() assert result == expected + def test_repr_truncates_nodes(self) -> None: + # construct a datatree with 50 nodes + number_of_files = 10 + number_of_groups = 5 + tree_dict = {} + for f in range(number_of_files): + for g in range(number_of_groups): + tree_dict[f"file_{f}/group_{g}"] = Dataset({"g": f * g}) + + tree = DataTree.from_dict(tree_dict) + result = repr(tree) + expected = dedent( + """ + + Group: / + ├── Group: /file_0 + │ ├── Group: /file_0/group_0 + │ │ Dimensions: () + │ │ Data variables: + │ │ g int64 8B 0 + │ ├── Group: /file_0/group_1 + │ │ Dimensions: () + │ │ Data variables: + │ │ g int64 8B 0 + │ ├── Group: /file_0/group_2 + │ │ Dimensions: () + │ │ Data variables: + │ │ g int64 8B 0 + │ ├── Group: /file_0/group_3 + │ │ Dimensions: () + │ │ Data variables: + │ │ g int64 8B 0 + │ └── Group: /file_0/group_4 + │ Dimensions: () + │ Data variables: + │ g int64 8B 0 + ├── Group: /file_1 + │ ├── Group: /file_1/group_0 + │ │ Dimensions: () + │ │ Data variables: + │ │ g int64 8B 0 + │ ├── Group: /file_1/group_1 + │ │ Dimensions: () + │ │ Data variables: + │ │ g int64 8B 1 + │ ├── Group: /file_1/group_2 + │ │ Dimensions: () + │ │ Data variables: + │ │ g int64 8B 2 + │ ├── Group: /file_1/group_3 + │ │ Dimensions: () + │ │ Data variables: + │ │ g int64 8B 3 + ... + └── Group: /file_9/group_4 + Dimensions: () + Data variables: + g int64 8B 36 + """ + ).strip() + assert expected == result + + with xr.set_options(display_max_rows=4): + result = repr(tree) + expected = dedent( + """ + + Group: / + ├── Group: /file_0 + │ ├── Group: /file_0/group_0 + │ │ Dimensions: () + │ │ Data variables: + │ │ g int64 8B 0 + │ ├── Group: /file_0/group_1 + │ │ Dimensions: () + │ │ Data variables: + │ │ g int64 8B 0 + ... + └── Group: /file_9/group_4 + Dimensions: () + Data variables: + g int64 8B 36 + """ + ).strip() + assert expected == result + def test_repr_inherited_dims(self) -> None: tree = DataTree.from_dict( { diff --git a/xarray/tests/test_formatting_html.py b/xarray/tests/test_formatting_html.py index 7c9cdbeaaf5..94156de9a43 100644 --- a/xarray/tests/test_formatting_html.py +++ b/xarray/tests/test_formatting_html.py @@ -320,6 +320,44 @@ def test_two_children( ) +class TestDataTreeTruncatesNodes: + def test_many_nodes(self): + # construct a datatree with 500 nodes + number_of_files = 20 + number_of_groups = 25 + tree_dict = {} + for f in range(number_of_files): + for g in range(number_of_groups): + tree_dict[f"file_{f}/group_{g}"] = xr.Dataset({"g": f * g}) + + tree = xr.DataTree.from_dict(tree_dict) + with xr.set_options(display_style="html"): + result = tree._repr_html_() + + assert "Only first 12 will show in dropdown" + assert "file_0" in result + assert "file_11" in result + assert "file_12" not in result + assert "file_19" not in result + assert "group_0" in result + assert "group_11" in result + assert "group_12" not in result + assert "group_24" not in result + + with xr.set_options(display_style="html", display_max_rows=4): + result = tree._repr_html_() + + assert "Only first 4 will show in dropdown" + assert "file_0" in result + assert "file_3" in result + assert "file_4" not in result + assert "file_19" not in result + assert "group_0" in result + assert "group_3" in result + assert "group_4" not in result + assert "group_24" not in result + + class TestDataTreeInheritance: def test_inherited_section_present(self) -> None: dt = xr.DataTree.from_dict( From 5b3dbe4f16dccf071098cef4491a4906f09f64e4 Mon Sep 17 00:00:00 2001 From: Julia Signell Date: Mon, 7 Apr 2025 17:03:29 -0400 Subject: [PATCH 04/13] Use new `display_max_children`: * create new option to control max number of children * consolidate logic to counting number of children per node * show first n/2 children then ... then last n/2 children --- xarray/core/datatree_render.py | 36 ++++++++++------ xarray/core/formatting.py | 14 +++---- xarray/core/formatting_html.py | 44 +++++++++++++------- xarray/core/options.py | 6 +++ xarray/tests/test_datatree.py | 62 +++++++++++----------------- xarray/tests/test_formatting_html.py | 48 ++++++++++++--------- 6 files changed, 114 insertions(+), 96 deletions(-) diff --git a/xarray/core/datatree_render.py b/xarray/core/datatree_render.py index 98cf738f20d..5b8124a9c82 100644 --- a/xarray/core/datatree_render.py +++ b/xarray/core/datatree_render.py @@ -10,6 +10,7 @@ from collections import namedtuple from collections.abc import Iterable, Iterator +from math import ceil from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -89,10 +90,10 @@ def __init__( Iterables that change the order of children cannot be used (e.g., `reversed`). maxlevel: Limit rendering to this depth. - maxchildren: Limit number of children to roughly this number. In practice, - for an arbitrarily large DataTree the number of children returned - will be (maxchildren * maxchildren - 1) / 2. The last child is also - included. + maxchildren: Limit number total number of nodes to roughly this number. In practice, + for an arbitrarily large DataTree the number of nodes returned + will be (maxchildren * maxchildren - 1) / 2. The included nodes are half from + the start of the tree and half from the end of the tree. :any:`RenderDataTree` is an iterator, returning a tuple with 3 items: `pre` tree prefix. @@ -166,13 +167,15 @@ def __init__( ├── sub0 └── sub1 - # `maxchildren` roughly limits the total number of children + # `maxchildren` limits the number of children per node - >>> print(RenderDataTree(root, maxchildren=3).by_attr()) + >>> print(RenderDataTree(root, maxchildren=1).by_attr("name")) root ├── sub0 │ ├── sub0B - └── sub1 + │ ... + ... + """ if style is None: style = ContStyle() @@ -192,26 +195,30 @@ def __next( node: DataTree, continues: tuple[bool, ...], level: int = 0, - nchildren: int = 0, ) -> Iterator[Row]: yield RenderDataTree.__item(node, continues, self.style) children = node.children.values() level += 1 if children and (self.maxlevel is None or level < self.maxlevel): + nchildren = len(children) children = self.childiter(children) - for child, is_last in _is_last(children): - nchildren += 1 + for i, (child, is_last) in enumerate(_is_last(children)): if ( self.maxchildren is None - or nchildren < self.maxchildren - or (not any(continues) and is_last) + or i < ceil(self.maxchildren / 2) + or i >= ceil(nchildren - self.maxchildren / 2) ): yield from self.__next( child, continues + (not is_last,), level=level, - nchildren=nchildren, ) + if ( + self.maxchildren is not None + and nchildren > self.maxchildren + and i == ceil(self.maxchildren / 2) + ): + yield RenderDataTree.__item("...", continues, self.style) @staticmethod def __item( @@ -273,6 +280,9 @@ def by_attr(self, attrname: str = "name") -> str: def get() -> Iterator[str]: for pre, fill, node in self: + if isinstance(node, str): + yield f"{fill}{node}" + continue attr = ( attrname(node) if callable(attrname) diff --git a/xarray/core/formatting.py b/xarray/core/formatting.py index 13360148a9f..00d0ecdcb86 100644 --- a/xarray/core/formatting.py +++ b/xarray/core/formatting.py @@ -1137,9 +1137,9 @@ def _datatree_node_repr(node: DataTree, show_inherited: bool) -> str: def datatree_repr(dt: DataTree) -> str: """A printable representation of the structure of this entire tree.""" - max_rows = OPTIONS["display_max_rows"] + max_children = OPTIONS["display_max_children"] - renderer = RenderDataTree(dt, maxchildren=max_rows) + renderer = RenderDataTree(dt, maxchildren=max_children) name_info = "" if dt.name is None else f" {dt.name!r}" header = f"" @@ -1148,12 +1148,10 @@ def datatree_repr(dt: DataTree) -> str: show_inherited = True rendered_items = list(renderer) - for i, (pre, fill, node) in enumerate(rendered_items): - if len(rendered_items) > max_rows: - if i == max_rows: - lines.append("...") - if i >= max_rows and i != (len(rendered_items) - 1): - continue + for pre, fill, node in rendered_items: + if isinstance(node, str): + lines.append(f"{fill}{node}") + continue node_repr = _datatree_node_repr(node, show_inherited=show_inherited) show_inherited = False # only show inherited coords on the root diff --git a/xarray/core/formatting_html.py b/xarray/core/formatting_html.py index b9c03453018..010186bb883 100644 --- a/xarray/core/formatting_html.py +++ b/xarray/core/formatting_html.py @@ -6,6 +6,7 @@ from functools import lru_cache, partial from html import escape from importlib.resources import files +from math import ceil from typing import TYPE_CHECKING from xarray.core.formatting import ( @@ -198,17 +199,16 @@ def _mapping_section( max_items_collapse, expand_option_name, enabled=True, - max_items_truncate: int | None = None, + max_option_name: str | None = None, ) -> str: n_items = len(mapping) expanded = _get_boolean_with_default( expand_option_name, n_items < max_items_collapse ) + max_items = OPTIONS.get(max_option_name) collapsed = not expanded - truncated = max_items_truncate is not None and n_items > max_items_truncate - inline_details = ( - f"Only first {max_items_truncate} will show in dropdown" if truncated else "" - ) + truncated = max_items is not None and n_items > max_items + inline_details = f"({max_items}/{n_items})" if truncated else "" return collapsible_section( name, @@ -359,19 +359,31 @@ def dataset_repr(ds) -> str: def summarize_datatree_children(children: Mapping[str, DataTree]) -> str: - N_CHILDREN = len(children) - 1 - MAX_CHILDREN = OPTIONS["display_max_rows"] - - children_html = "".join( - _wrap_datatree_repr(datatree_node_repr(n, c), end=i == N_CHILDREN) - for i, (n, c) in enumerate(children.items()) - if i < MAX_CHILDREN - ) + MAX_CHILDREN = OPTIONS["display_max_children"] + n_children = len(children) + + children_html = [] + for i, (n, c) in enumerate(children.items()): + if ( + MAX_CHILDREN is None + or i < ceil(MAX_CHILDREN / 2) + or i >= ceil(n_children - MAX_CHILDREN / 2) + ): + is_last = i == (n_children - 1) + children_html.append( + _wrap_datatree_repr(datatree_node_repr(n, c), end=is_last) + ) + elif ( + MAX_CHILDREN is not None + and n_children > MAX_CHILDREN + and i == ceil(MAX_CHILDREN / 2) + ): + children_html.append("
...
") return "".join( [ "
", - children_html, + "".join(children_html), "
", ] ) @@ -382,7 +394,7 @@ def summarize_datatree_children(children: Mapping[str, DataTree]) -> str: name="Groups", details_func=summarize_datatree_children, max_items_collapse=1, - max_items_truncate=OPTIONS["display_max_rows"], + max_option_name="display_max_children", expand_option_name="display_expand_groups", ) @@ -427,7 +439,7 @@ def datatree_node_repr(group_title: str, node: DataTree, show_inherited=False) - return _obj_repr(ds, header_components, sections) -def _wrap_datatree_repr(r: str, end: bool = False, skipped_some: bool = False) -> str: +def _wrap_datatree_repr(r: str, end: bool = False) -> str: """ Wrap HTML representation with a tee to the left of it. diff --git a/xarray/core/options.py b/xarray/core/options.py index 2d69e4b6584..1f7e48c1623 100644 --- a/xarray/core/options.py +++ b/xarray/core/options.py @@ -13,6 +13,7 @@ "chunk_manager", "cmap_divergent", "cmap_sequential", + "display_max_children", "display_max_rows", "display_values_threshold", "display_style", @@ -40,6 +41,7 @@ class T_Options(TypedDict): chunk_manager: str cmap_divergent: str | Colormap cmap_sequential: str | Colormap + display_max_children: int display_max_rows: int display_values_threshold: int display_style: Literal["text", "html"] @@ -67,6 +69,7 @@ class T_Options(TypedDict): "chunk_manager": "dask", "cmap_divergent": "RdBu_r", "cmap_sequential": "viridis", + "display_max_children": 6, "display_max_rows": 12, "display_values_threshold": 200, "display_style": "html", @@ -99,6 +102,7 @@ def _positive_integer(value: Any) -> bool: _VALIDATORS = { "arithmetic_broadcast": lambda value: isinstance(value, bool), "arithmetic_join": _JOIN_OPTIONS.__contains__, + "display_max_children": _positive_integer, "display_max_rows": _positive_integer, "display_values_threshold": _positive_integer, "display_style": _DISPLAY_OPTIONS.__contains__, @@ -222,6 +226,8 @@ class set_options: * ``True`` : to always expand indexes * ``False`` : to always collapse indexes * ``default`` : to expand unless over a pre-defined limit (always collapse for html style) + display_max_children : int, default: 6 + Maximum number of children to display for each node in a DataTree display_max_rows : int, default: 12 Maximum display rows. display_values_threshold : int, default: 200 diff --git a/xarray/tests/test_datatree.py b/xarray/tests/test_datatree.py index c7a8a561b08..5e00a7831e0 100644 --- a/xarray/tests/test_datatree.py +++ b/xarray/tests/test_datatree.py @@ -1206,7 +1206,9 @@ def test_repr_truncates_nodes(self) -> None: tree_dict[f"file_{f}/group_{g}"] = Dataset({"g": f * g}) tree = DataTree.from_dict(tree_dict) - result = repr(tree) + with xr.set_options(display_max_children=3): + result = repr(tree) + expected = dedent( """ @@ -1220,14 +1222,7 @@ def test_repr_truncates_nodes(self) -> None: │ │ Dimensions: () │ │ Data variables: │ │ g int64 8B 0 - │ ├── Group: /file_0/group_2 - │ │ Dimensions: () - │ │ Data variables: - │ │ g int64 8B 0 - │ ├── Group: /file_0/group_3 - │ │ Dimensions: () - │ │ Data variables: - │ │ g int64 8B 0 + │ ... │ └── Group: /file_0/group_4 │ Dimensions: () │ Data variables: @@ -1241,15 +1236,22 @@ def test_repr_truncates_nodes(self) -> None: │ │ Dimensions: () │ │ Data variables: │ │ g int64 8B 1 - │ ├── Group: /file_1/group_2 - │ │ Dimensions: () - │ │ Data variables: - │ │ g int64 8B 2 - │ ├── Group: /file_1/group_3 - │ │ Dimensions: () - │ │ Data variables: - │ │ g int64 8B 3 + │ ... + │ └── Group: /file_1/group_4 + │ Dimensions: () + │ Data variables: + │ g int64 8B 4 ... + └── Group: /file_9 + ├── Group: /file_9/group_0 + │ Dimensions: () + │ Data variables: + │ g int64 8B 0 + ├── Group: /file_9/group_1 + │ Dimensions: () + │ Data variables: + │ g int64 8B 9 + ... └── Group: /file_9/group_4 Dimensions: () Data variables: @@ -1258,29 +1260,11 @@ def test_repr_truncates_nodes(self) -> None: ).strip() assert expected == result - with xr.set_options(display_max_rows=4): + with xr.set_options(display_max_children=10): result = repr(tree) - expected = dedent( - """ - - Group: / - ├── Group: /file_0 - │ ├── Group: /file_0/group_0 - │ │ Dimensions: () - │ │ Data variables: - │ │ g int64 8B 0 - │ ├── Group: /file_0/group_1 - │ │ Dimensions: () - │ │ Data variables: - │ │ g int64 8B 0 - ... - └── Group: /file_9/group_4 - Dimensions: () - Data variables: - g int64 8B 36 - """ - ).strip() - assert expected == result + + for key in tree_dict: + assert key in result def test_repr_inherited_dims(self) -> None: tree = DataTree.from_dict( diff --git a/xarray/tests/test_formatting_html.py b/xarray/tests/test_formatting_html.py index 94156de9a43..46d3bad3c5a 100644 --- a/xarray/tests/test_formatting_html.py +++ b/xarray/tests/test_formatting_html.py @@ -334,28 +334,36 @@ def test_many_nodes(self): with xr.set_options(display_style="html"): result = tree._repr_html_() - assert "Only first 12 will show in dropdown" - assert "file_0" in result - assert "file_11" in result - assert "file_12" not in result - assert "file_19" not in result - assert "group_0" in result - assert "group_11" in result - assert "group_12" not in result - assert "group_24" not in result - - with xr.set_options(display_style="html", display_max_rows=4): + assert "6/20" in result + for i in range(number_of_files): + if i < 3 or i >= (number_of_files - 3): + assert f"file_{i}" in result + else: + assert f"file_{i}" not in result + + assert "6/25" in result + for i in range(number_of_groups): + if i < 3 or i >= (number_of_groups - 3): + assert f"group_{i}" in result + else: + assert f"group_{i}" not in result + + with xr.set_options(display_style="html", display_max_children=3): result = tree._repr_html_() - assert "Only first 4 will show in dropdown" - assert "file_0" in result - assert "file_3" in result - assert "file_4" not in result - assert "file_19" not in result - assert "group_0" in result - assert "group_3" in result - assert "group_4" not in result - assert "group_24" not in result + assert "3/20" in result + for i in range(number_of_files): + if i < 2 or i >= (number_of_files - 1): + assert f"file_{i}" in result + else: + assert f"file_{i}" not in result + + assert "3/25" in result + for i in range(number_of_groups): + if i < 2 or i >= (number_of_groups - 1): + assert f"group_{i}" in result + else: + assert f"group_{i}" not in result class TestDataTreeInheritance: From db14707fdf625c02f6c02dc795825286e0dfc147 Mon Sep 17 00:00:00 2001 From: Julia Signell Date: Mon, 7 Apr 2025 17:06:57 -0400 Subject: [PATCH 05/13] Fix docstring --- xarray/core/datatree_render.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/xarray/core/datatree_render.py b/xarray/core/datatree_render.py index 5b8124a9c82..f6fa84f4164 100644 --- a/xarray/core/datatree_render.py +++ b/xarray/core/datatree_render.py @@ -90,10 +90,7 @@ def __init__( Iterables that change the order of children cannot be used (e.g., `reversed`). maxlevel: Limit rendering to this depth. - maxchildren: Limit number total number of nodes to roughly this number. In practice, - for an arbitrarily large DataTree the number of nodes returned - will be (maxchildren * maxchildren - 1) / 2. The included nodes are half from - the start of the tree and half from the end of the tree. + maxchildren: Limit number of children at each node. :any:`RenderDataTree` is an iterator, returning a tuple with 3 items: `pre` tree prefix. From 65b6831e184b3a5155348fa89b1e4182098ce248 Mon Sep 17 00:00:00 2001 From: Julia Signell Date: Mon, 7 Apr 2025 17:09:44 -0400 Subject: [PATCH 06/13] Tidy up PR --- xarray/core/formatting_html.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/core/formatting_html.py b/xarray/core/formatting_html.py index 010186bb883..453ad262188 100644 --- a/xarray/core/formatting_html.py +++ b/xarray/core/formatting_html.py @@ -205,8 +205,8 @@ def _mapping_section( expanded = _get_boolean_with_default( expand_option_name, n_items < max_items_collapse ) - max_items = OPTIONS.get(max_option_name) collapsed = not expanded + max_items = OPTIONS.get(max_option_name) truncated = max_items is not None and n_items > max_items inline_details = f"({max_items}/{n_items})" if truncated else "" From 8a374dabc19efe8e704d902a06bb2f670bbeed62 Mon Sep 17 00:00:00 2001 From: Julia Signell Date: Mon, 7 Apr 2025 17:09:58 -0400 Subject: [PATCH 07/13] Tidy up PR --- xarray/core/formatting.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/xarray/core/formatting.py b/xarray/core/formatting.py index 00d0ecdcb86..eeb9bfa5759 100644 --- a/xarray/core/formatting.py +++ b/xarray/core/formatting.py @@ -1147,8 +1147,7 @@ def datatree_repr(dt: DataTree) -> str: lines = [header] show_inherited = True - rendered_items = list(renderer) - for pre, fill, node in rendered_items: + for pre, fill, node in renderer: if isinstance(node, str): lines.append(f"{fill}{node}") continue From cc55994059c13a65f1509bacb3b104f1298df3f2 Mon Sep 17 00:00:00 2001 From: Julia Signell Date: Wed, 16 Apr 2025 16:37:29 -0400 Subject: [PATCH 08/13] Mypy fixes --- xarray/core/datatree_render.py | 2 +- xarray/core/formatting_html.py | 25 ++++++++++--------------- xarray/core/options.py | 2 +- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/xarray/core/datatree_render.py b/xarray/core/datatree_render.py index f6fa84f4164..f1042d9eeef 100644 --- a/xarray/core/datatree_render.py +++ b/xarray/core/datatree_render.py @@ -219,7 +219,7 @@ def __next( @staticmethod def __item( - node: DataTree, continues: tuple[bool, ...], style: AbstractStyle + node: DataTree | str, continues: tuple[bool, ...], style: AbstractStyle ) -> Row: if not continues: return Row("", "", node) diff --git a/xarray/core/formatting_html.py b/xarray/core/formatting_html.py index 453ad262188..c0601e3326a 100644 --- a/xarray/core/formatting_html.py +++ b/xarray/core/formatting_html.py @@ -7,7 +7,7 @@ from html import escape from importlib.resources import files from math import ceil -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal from xarray.core.formatting import ( inherited_vars, @@ -199,16 +199,19 @@ def _mapping_section( max_items_collapse, expand_option_name, enabled=True, - max_option_name: str | None = None, + max_option_name: Literal["display_max_children"] | None = None, ) -> str: n_items = len(mapping) expanded = _get_boolean_with_default( expand_option_name, n_items < max_items_collapse ) collapsed = not expanded - max_items = OPTIONS.get(max_option_name) - truncated = max_items is not None and n_items > max_items - inline_details = f"({max_items}/{n_items})" if truncated else "" + + inline_details = "" + if max_option_name and max_option_name in OPTIONS: + max_items = int(OPTIONS[max_option_name]) + if n_items > max_items: + inline_details = f"({max_items}/{n_items})" return collapsible_section( name, @@ -364,20 +367,12 @@ def summarize_datatree_children(children: Mapping[str, DataTree]) -> str: children_html = [] for i, (n, c) in enumerate(children.items()): - if ( - MAX_CHILDREN is None - or i < ceil(MAX_CHILDREN / 2) - or i >= ceil(n_children - MAX_CHILDREN / 2) - ): + if i < ceil(MAX_CHILDREN / 2) or i >= ceil(n_children - MAX_CHILDREN / 2): is_last = i == (n_children - 1) children_html.append( _wrap_datatree_repr(datatree_node_repr(n, c), end=is_last) ) - elif ( - MAX_CHILDREN is not None - and n_children > MAX_CHILDREN - and i == ceil(MAX_CHILDREN / 2) - ): + elif n_children > MAX_CHILDREN and i == ceil(MAX_CHILDREN / 2): children_html.append("
...
") return "".join( diff --git a/xarray/core/options.py b/xarray/core/options.py index 1f7e48c1623..adaa563d09b 100644 --- a/xarray/core/options.py +++ b/xarray/core/options.py @@ -227,7 +227,7 @@ class set_options: * ``False`` : to always collapse indexes * ``default`` : to expand unless over a pre-defined limit (always collapse for html style) display_max_children : int, default: 6 - Maximum number of children to display for each node in a DataTree + Maximum number of children to display for each node in a DataTree. display_max_rows : int, default: 12 Maximum display rows. display_values_threshold : int, default: 200 From 6eb7df166d65b394d53b731a0dc2d9f9ade969da Mon Sep 17 00:00:00 2001 From: Julia Signell Date: Wed, 16 Apr 2025 16:37:47 -0400 Subject: [PATCH 09/13] Update xarray/tests/test_formatting_html.py Co-authored-by: Illviljan <14371165+Illviljan@users.noreply.github.com> --- xarray/tests/test_formatting_html.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/test_formatting_html.py b/xarray/tests/test_formatting_html.py index 46d3bad3c5a..a17fffc3683 100644 --- a/xarray/tests/test_formatting_html.py +++ b/xarray/tests/test_formatting_html.py @@ -321,7 +321,7 @@ def test_two_children( class TestDataTreeTruncatesNodes: - def test_many_nodes(self): + def test_many_nodes(self) -> None: # construct a datatree with 500 nodes number_of_files = 20 number_of_groups = 25 From db96a7e1ab39de4a3fb3956526e1ad5c86af4fcf Mon Sep 17 00:00:00 2001 From: Julia Signell Date: Fri, 18 Apr 2025 10:22:33 -0400 Subject: [PATCH 10/13] Update ubuntu runner since 20.04 has been removed --- .github/workflows/benchmarks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index ee778f6bfd9..6106400ebdd 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -12,7 +12,7 @@ jobs: benchmark: if: ${{ contains( github.event.pull_request.labels.*.name, 'run-benchmark') && github.event_name == 'pull_request' || contains( github.event.pull_request.labels.*.name, 'topic-performance') && github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} name: Linux - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 env: ASV_DIR: "./asv_bench" CONDA_ENV_FILE: ci/requirements/environment.yml From 3ca0c2770888bdeb302fae5516ad5286db45594b Mon Sep 17 00:00:00 2001 From: Julia Signell Date: Mon, 28 Apr 2025 13:56:56 -0400 Subject: [PATCH 11/13] Add to What's New --- doc/whats-new.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 20bbdc7ec69..c10236fc999 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -49,6 +49,12 @@ Breaking changes now return objects indexed by :py:meth:`pandas.IntervalArray` objects, instead of numpy object arrays containing tuples. This change enables interval-aware indexing of such Xarray objects. (:pull:`9671`). By `Ilan Gold `_. +- The html and text ``repr`` for ``DataTree`` are now truncated. Up to 6 children are displayed + for each node -- the first 3 and the last 3 children -- with a ``...`` between them. The number + of children to include in the display is configurable via options. For instance use + ``set_options(display_max_children=8)`` to display 8 children rather than the default 6. (:pull:`10139`) + By `Julia Signell `_. + Deprecations ~~~~~~~~~~~~ From 86343473332366223ed5384c882a773909f28dc4 Mon Sep 17 00:00:00 2001 From: Deepak Cherian Date: Mon, 28 Apr 2025 12:16:52 -0600 Subject: [PATCH 12/13] Update doc/whats-new.rst --- doc/whats-new.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index c10236fc999..b4a615368ea 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -53,7 +53,7 @@ Breaking changes for each node -- the first 3 and the last 3 children -- with a ``...`` between them. The number of children to include in the display is configurable via options. For instance use ``set_options(display_max_children=8)`` to display 8 children rather than the default 6. (:pull:`10139`) - By `Julia Signell `_. + By `Julia Signell `_. Deprecations From d8ce8f3fbddb4d3189c98dad7069125299b27302 Mon Sep 17 00:00:00 2001 From: Deepak Cherian Date: Mon, 28 Apr 2025 12:17:07 -0600 Subject: [PATCH 13/13] Update .github/workflows/benchmarks.yml --- .github/workflows/benchmarks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 6106400ebdd..e8d411ec927 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -12,7 +12,7 @@ jobs: benchmark: if: ${{ contains( github.event.pull_request.labels.*.name, 'run-benchmark') && github.event_name == 'pull_request' || contains( github.event.pull_request.labels.*.name, 'topic-performance') && github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} name: Linux - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest env: ASV_DIR: "./asv_bench" CONDA_ENV_FILE: ci/requirements/environment.yml