diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 1c67ba1ee7f..6e0f07eb624 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -36,6 +36,9 @@ Bug fixes Internal Changes ~~~~~~~~~~~~~~~~ +- Migrates ``formatting_html`` functionality for `DataTree` into ``xarray/core`` (:pull: `8930`) + By `Eni Awowale `_, `Julia Signell `_ + and `Tom Nicholas `_. - Migrates ``datatree_mapping`` functionality into ``xarray/core`` (:pull:`8948`) By `Matt Savoie `_ `Owen Littlejohns ` and `Tom Nicholas `_. diff --git a/xarray/core/datatree.py b/xarray/core/datatree.py index e24dfb504b1..57fd7222898 100644 --- a/xarray/core/datatree.py +++ b/xarray/core/datatree.py @@ -23,6 +23,9 @@ check_isomorphic, map_over_subtree, ) +from xarray.core.formatting_html import ( + datatree_repr as datatree_repr_html, +) from xarray.core.indexes import Index, Indexes from xarray.core.merge import dataset_update_method from xarray.core.options import OPTIONS as XR_OPTS @@ -38,9 +41,6 @@ from xarray.core.variable import Variable from xarray.datatree_.datatree.common import TreeAttrAccessMixin from xarray.datatree_.datatree.formatting import datatree_repr -from xarray.datatree_.datatree.formatting_html import ( - datatree_repr as datatree_repr_html, -) from xarray.datatree_.datatree.ops import ( DataTreeArithmeticMixin, MappedDatasetMethodsMixin, diff --git a/xarray/core/formatting_html.py b/xarray/core/formatting_html.py index 2c76b182207..9bf5befbe3f 100644 --- a/xarray/core/formatting_html.py +++ b/xarray/core/formatting_html.py @@ -2,9 +2,11 @@ import uuid from collections import OrderedDict +from collections.abc import Mapping from functools import lru_cache, partial from html import escape from importlib.resources import files +from typing import TYPE_CHECKING from xarray.core.formatting import ( inline_index_repr, @@ -18,6 +20,9 @@ ("xarray.static.css", "style.css"), ) +if TYPE_CHECKING: + from xarray.core.datatree import DataTree + @lru_cache(None) def _load_static_files(): @@ -341,3 +346,129 @@ def dataset_repr(ds) -> str: ] return _obj_repr(ds, header_components, sections) + + +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 + ) + + children_html = "".join( + ( + lines_callback(n, c, end=False) # Long lines + if i < N_CHILDREN + else lines_callback(n, c, end=True) + ) # Short lines + for i, (n, c) in enumerate(children.items()) + ) + + return "".join( + [ + "
", + children_html, + "
", + ] + ) + + +children_section = partial( + _mapping_section, + name="Groups", + details_func=summarize_datatree_children, + max_items_collapse=1, + expand_option_name="display_expand_groups", +) + + +def datatree_node_repr(group_title: str, dt: DataTree) -> str: + header_components = [f"
{escape(group_title)}
"] + + ds = dt.ds + + sections = [ + children_section(dt.children), + dim_section(ds), + coord_section(ds.coords), + datavar_section(ds.data_vars), + attr_section(ds.attrs), + ] + + return _obj_repr(ds, header_components, sections) + + +def _wrap_datatree_repr(r: str, end: bool = False) -> str: + """ + Wrap HTML representation with a tee to the left of it. + + Enclosing HTML tag is a
with :code:`display: inline-grid` style. + + Turns: + [ title ] + | details | + |_____________| + + into (A): + |─ [ title ] + | | details | + | |_____________| + + or (B): + └─ [ title ] + | details | + |_____________| + + Parameters + ---------- + r: str + HTML representation to wrap. + end: bool + Specify if the line on the left should continue or end. + + Default is True. + + Returns + ------- + str + Wrapped HTML representation. + + Tee color is set to the variable :code:`--xr-border-color`. + """ + # height of line + end = bool(end) + height = "100%" if end is False else "1.2em" + return "".join( + [ + "
", + "
", + "
", + "
", + "
", + "
", + r, + "
", + "
", + ] + ) + + +def datatree_repr(dt: DataTree) -> str: + obj_type = f"datatree.{type(dt).__name__}" + return datatree_node_repr(obj_type, dt) diff --git a/xarray/datatree_/datatree/formatting_html.py b/xarray/datatree_/datatree/formatting_html.py deleted file mode 100644 index 547b567a396..00000000000 --- a/xarray/datatree_/datatree/formatting_html.py +++ /dev/null @@ -1,135 +0,0 @@ -from functools import partial -from html import escape -from typing import Any, Mapping - -from xarray.core.formatting_html import ( - _mapping_section, - _obj_repr, - attr_section, - coord_section, - datavar_section, - dim_section, -) - - -def summarize_children(children: Mapping[str, Any]) -> str: - N_CHILDREN = len(children) - 1 - - # Get result from node_repr and wrap it - lines_callback = lambda n, c, end: _wrap_repr(node_repr(n, c), end=end) - - children_html = "".join( - lines_callback(n, c, end=False) # Long lines - if i < N_CHILDREN - else lines_callback(n, c, end=True) # Short lines - for i, (n, c) in enumerate(children.items()) - ) - - return "".join( - [ - "
", - children_html, - "
", - ] - ) - - -children_section = partial( - _mapping_section, - name="Groups", - details_func=summarize_children, - max_items_collapse=1, - expand_option_name="display_expand_groups", -) - - -def node_repr(group_title: str, dt: Any) -> str: - header_components = [f"
{escape(group_title)}
"] - - ds = dt.ds - - sections = [ - children_section(dt.children), - dim_section(ds), - coord_section(ds.coords), - datavar_section(ds.data_vars), - attr_section(ds.attrs), - ] - - return _obj_repr(ds, header_components, sections) - - -def _wrap_repr(r: str, end: bool = False) -> str: - """ - Wrap HTML representation with a tee to the left of it. - - Enclosing HTML tag is a
with :code:`display: inline-grid` style. - - Turns: - [ title ] - | details | - |_____________| - - into (A): - |─ [ title ] - | | details | - | |_____________| - - or (B): - └─ [ title ] - | details | - |_____________| - - Parameters - ---------- - r: str - HTML representation to wrap. - end: bool - Specify if the line on the left should continue or end. - - Default is True. - - Returns - ------- - str - Wrapped HTML representation. - - Tee color is set to the variable :code:`--xr-border-color`. - """ - # height of line - end = bool(end) - height = "100%" if end is False else "1.2em" - return "".join( - [ - "
", - "
", - "
", - "
", - "
", - "
", - "
    ", - r, - "
" "
", - "
", - ] - ) - - -def datatree_repr(dt: Any) -> str: - obj_type = f"datatree.{type(dt).__name__}" - return node_repr(obj_type, dt) diff --git a/xarray/datatree_/datatree/tests/test_formatting_html.py b/xarray/datatree_/datatree/tests/test_formatting_html.py deleted file mode 100644 index 98cdf02bff4..00000000000 --- a/xarray/datatree_/datatree/tests/test_formatting_html.py +++ /dev/null @@ -1,198 +0,0 @@ -import pytest -import xarray as xr - -from xarray.core.datatree import DataTree -from xarray.datatree_.datatree import formatting_html - - -@pytest.fixture(scope="module", params=["some html", "some other html"]) -def repr(request): - return request.param - - -class Test_summarize_children: - """ - Unit tests for summarize_children. - """ - - func = staticmethod(formatting_html.summarize_children) - - @pytest.fixture(scope="class") - def childfree_tree_factory(self): - """ - Fixture for a child-free DataTree factory. - """ - from random import randint - - def _childfree_tree_factory(): - return DataTree( - data=xr.Dataset({"z": ("y", [randint(1, 100) for _ in range(3)])}) - ) - - return _childfree_tree_factory - - @pytest.fixture(scope="class") - def childfree_tree(self, childfree_tree_factory): - """ - Fixture for a child-free DataTree. - """ - return childfree_tree_factory() - - @pytest.fixture(scope="function") - def mock_node_repr(self, monkeypatch): - """ - Apply mocking for node_repr. - """ - - def mock(group_title, dt): - """ - Mock with a simple result - """ - return group_title + " " + str(id(dt)) - - monkeypatch.setattr(formatting_html, "node_repr", mock) - - @pytest.fixture(scope="function") - def mock_wrap_repr(self, monkeypatch): - """ - Apply mocking for _wrap_repr. - """ - - def mock(r, *, end, **kwargs): - """ - Mock by appending "end" or "not end". - """ - return r + " " + ("end" if end else "not end") + "//" - - monkeypatch.setattr(formatting_html, "_wrap_repr", mock) - - def test_empty_mapping(self): - """ - Test with an empty mapping of children. - """ - children = {} - assert self.func(children) == ( - "
" "
" - ) - - def test_one_child(self, childfree_tree, mock_wrap_repr, mock_node_repr): - """ - Test with one child. - - Uses a mock of _wrap_repr and node_repr to essentially mock - the inline lambda function "lines_callback". - """ - # Create mapping of children - children = {"a": childfree_tree} - - # Expect first line to be produced from the first child, and - # wrapped as the last child - first_line = f"a {id(children['a'])} end//" - - assert self.func(children) == ( - "
" - f"{first_line}" - "
" - ) - - def test_two_children(self, childfree_tree_factory, mock_wrap_repr, mock_node_repr): - """ - Test with two level deep children. - - Uses a mock of _wrap_repr and node_repr to essentially mock - the inline lambda function "lines_callback". - """ - - # Create mapping of children - children = {"a": childfree_tree_factory(), "b": childfree_tree_factory()} - - # Expect first line to be produced from the first child, and - # wrapped as _not_ the last child - first_line = f"a {id(children['a'])} not end//" - - # Expect second line to be produced from the second child, and - # wrapped as the last child - second_line = f"b {id(children['b'])} end//" - - assert self.func(children) == ( - "
" - f"{first_line}" - f"{second_line}" - "
" - ) - - -class Test__wrap_repr: - """ - Unit tests for _wrap_repr. - """ - - func = staticmethod(formatting_html._wrap_repr) - - def test_end(self, repr): - """ - Test with end=True. - """ - r = self.func(repr, end=True) - assert r == ( - "
" - "
" - "
" - "
" - "
" - "
" - "
    " - f"{repr}" - "
" - "
" - "
" - ) - - def test_not_end(self, repr): - """ - Test with end=False. - """ - r = self.func(repr, end=False) - assert r == ( - "
" - "
" - "
" - "
" - "
" - "
" - "
    " - f"{repr}" - "
" - "
" - "
" - ) diff --git a/xarray/tests/test_formatting_html.py b/xarray/tests/test_formatting_html.py index 6540406e914..ada7f75b21b 100644 --- a/xarray/tests/test_formatting_html.py +++ b/xarray/tests/test_formatting_html.py @@ -7,6 +7,7 @@ import xarray as xr from xarray.core import formatting_html as fh from xarray.core.coordinates import Coordinates +from xarray.core.datatree import DataTree @pytest.fixture @@ -196,3 +197,197 @@ def test_nonstr_variable_repr_html() -> None: html = v._repr_html_().strip() assert "
22 :
bar
" in html assert "
  • 10: 3
  • " in html + + +@pytest.fixture(scope="module", params=["some html", "some other html"]) +def repr(request): + return request.param + + +class Test_summarize_datatree_children: + """ + Unit tests for summarize_datatree_children. + """ + + func = staticmethod(fh.summarize_datatree_children) + + @pytest.fixture(scope="class") + def childfree_tree_factory(self): + """ + Fixture for a child-free DataTree factory. + """ + from random import randint + + def _childfree_tree_factory(): + return DataTree( + data=xr.Dataset({"z": ("y", [randint(1, 100) for _ in range(3)])}) + ) + + return _childfree_tree_factory + + @pytest.fixture(scope="class") + def childfree_tree(self, childfree_tree_factory): + """ + Fixture for a child-free DataTree. + """ + return childfree_tree_factory() + + @pytest.fixture(scope="function") + def mock_datatree_node_repr(self, monkeypatch): + """ + Apply mocking for datatree_node_repr. + """ + + def mock(group_title, dt): + """ + Mock with a simple result + """ + return group_title + " " + str(id(dt)) + + monkeypatch.setattr(fh, "datatree_node_repr", mock) + + @pytest.fixture(scope="function") + def mock_wrap_datatree_repr(self, monkeypatch): + """ + Apply mocking for _wrap_datatree_repr. + """ + + def mock(r, *, end, **kwargs): + """ + Mock by appending "end" or "not end". + """ + return r + " " + ("end" if end else "not end") + "//" + + monkeypatch.setattr(fh, "_wrap_datatree_repr", mock) + + def test_empty_mapping(self): + """ + Test with an empty mapping of children. + """ + children: dict[str, DataTree] = {} + assert self.func(children) == ( + "
    " + "
    " + ) + + def test_one_child( + self, childfree_tree, mock_wrap_datatree_repr, mock_datatree_node_repr + ): + """ + Test with one child. + + Uses a mock of _wrap_datatree_repr and _datatree_node_repr to essentially mock + the inline lambda function "lines_callback". + """ + # Create mapping of children + children = {"a": childfree_tree} + + # Expect first line to be produced from the first child, and + # wrapped as the last child + first_line = f"a {id(children['a'])} end//" + + assert self.func(children) == ( + "
    " + f"{first_line}" + "
    " + ) + + def test_two_children( + self, childfree_tree_factory, mock_wrap_datatree_repr, mock_datatree_node_repr + ): + """ + Test with two level deep children. + + Uses a mock of _wrap_datatree_repr and datatree_node_repr to essentially mock + the inline lambda function "lines_callback". + """ + + # Create mapping of children + children = {"a": childfree_tree_factory(), "b": childfree_tree_factory()} + + # Expect first line to be produced from the first child, and + # wrapped as _not_ the last child + first_line = f"a {id(children['a'])} not end//" + + # Expect second line to be produced from the second child, and + # wrapped as the last child + second_line = f"b {id(children['b'])} end//" + + assert self.func(children) == ( + "
    " + f"{first_line}" + f"{second_line}" + "
    " + ) + + +class Test__wrap_datatree_repr: + """ + Unit tests for _wrap_datatree_repr. + """ + + func = staticmethod(fh._wrap_datatree_repr) + + def test_end(self, repr): + """ + Test with end=True. + """ + r = self.func(repr, end=True) + assert r == ( + "
    " + "
    " + "
    " + "
    " + "
    " + "
    " + f"{repr}" + "
    " + "
    " + ) + + def test_not_end(self, repr): + """ + Test with end=False. + """ + r = self.func(repr, end=False) + assert r == ( + "
    " + "
    " + "
    " + "
    " + "
    " + "
    " + f"{repr}" + "
    " + "
    " + )