Skip to content

Commit d68ae3c

Browse files
committed
DAS-2066: formatting_html.py merge
1 parent 473b87f commit d68ae3c

File tree

2 files changed

+325
-0
lines changed

2 files changed

+325
-0
lines changed

xarray/core/formatting_html.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import uuid
44
from collections import OrderedDict
5+
from collections.abc import Mapping
56
from functools import lru_cache, partial
67
from html import escape
78
from importlib.resources import files
9+
from typing import Any
810

911
from xarray.core.formatting import (
1012
inline_index_repr,
@@ -18,6 +20,10 @@
1820
("xarray.static.css", "style.css"),
1921
)
2022

23+
from xarray.core.options import OPTIONS
24+
25+
OPTIONS["display_expand_groups"] = "default"
26+
2127

2228
@lru_cache(None)
2329
def _load_static_files():
@@ -341,3 +347,128 @@ def dataset_repr(ds) -> str:
341347
]
342348

343349
return _obj_repr(ds, header_components, sections)
350+
351+
352+
def summarize_children(children: Mapping[str, Any]) -> str:
353+
N_CHILDREN = len(children) - 1
354+
355+
# Get result from node_repr and wrap it
356+
lines_callback = lambda n, c, end: _wrap_repr(node_repr(n, c), end=end)
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+
)
366+
367+
return "".join(
368+
[
369+
"<div style='display: inline-grid; grid-template-columns: 100%'>",
370+
children_html,
371+
"</div>",
372+
]
373+
)
374+
375+
376+
children_section = partial(
377+
_mapping_section,
378+
name="Groups",
379+
details_func=summarize_children,
380+
max_items_collapse=1,
381+
expand_option_name="display_expand_groups",
382+
)
383+
384+
385+
def node_repr(group_title: str, dt: Any) -> str:
386+
header_components = [f"<div class='xr-obj-type'>{escape(group_title)}</div>"]
387+
388+
ds = dt.ds
389+
390+
sections = [
391+
children_section(dt.children),
392+
dim_section(ds),
393+
coord_section(ds.coords),
394+
datavar_section(ds.data_vars),
395+
attr_section(ds.attrs),
396+
]
397+
398+
return _obj_repr(ds, header_components, sections)
399+
400+
401+
def _wrap_repr(r: str, end: bool = False) -> str:
402+
"""
403+
Wrap HTML representation with a tee to the left of it.
404+
405+
Enclosing HTML tag is a <div> with :code:`display: inline-grid` style.
406+
407+
Turns:
408+
[ title ]
409+
| details |
410+
|_____________|
411+
412+
into (A):
413+
|─ [ title ]
414+
| | details |
415+
| |_____________|
416+
417+
or (B):
418+
└─ [ title ]
419+
| details |
420+
|_____________|
421+
422+
Parameters
423+
----------
424+
r: str
425+
HTML representation to wrap.
426+
end: bool
427+
Specify if the line on the left should continue or end.
428+
429+
Default is True.
430+
431+
Returns
432+
-------
433+
str
434+
Wrapped HTML representation.
435+
436+
Tee color is set to the variable :code:`--xr-border-color`.
437+
"""
438+
# height of line
439+
end = bool(end)
440+
height = "100%" if end is False else "1.2em"
441+
return "".join(
442+
[
443+
"<div style='display: inline-grid;'>",
444+
"<div style='",
445+
"grid-column-start: 1;",
446+
"border-right: 0.2em solid;",
447+
"border-color: var(--xr-border-color);",
448+
f"height: {height};",
449+
"width: 0px;",
450+
"'>",
451+
"</div>",
452+
"<div style='",
453+
"grid-column-start: 2;",
454+
"grid-row-start: 1;",
455+
"height: 1em;",
456+
"width: 20px;",
457+
"border-bottom: 0.2em solid;",
458+
"border-color: var(--xr-border-color);",
459+
"'>",
460+
"</div>",
461+
"<div style='",
462+
"grid-column-start: 3;",
463+
"'>",
464+
"<ul class='xr-sections'>",
465+
r,
466+
"</ul>" "</div>",
467+
"</div>",
468+
]
469+
)
470+
471+
472+
def datatree_repr(dt: Any) -> str:
473+
obj_type = f"datatree.{type(dt).__name__}"
474+
return node_repr(obj_type, dt)

xarray/tests/test_formatting_html.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import xarray as xr
88
from xarray.core import formatting_html as fh
99
from xarray.core.coordinates import Coordinates
10+
from xarray.core.datatree import DataTree
1011

1112

1213
@pytest.fixture
@@ -196,3 +197,196 @@ def test_nonstr_variable_repr_html() -> None:
196197
html = v._repr_html_().strip()
197198
assert "<dt><span>22 :</span></dt><dd>bar</dd>" in html
198199
assert "<li><span>10</span>: 3</li></ul>" in html
200+
201+
202+
@pytest.fixture(scope="module", params=["some html", "some other html"])
203+
def repr(request):
204+
return request.param
205+
206+
207+
class Test_summarize_children:
208+
"""
209+
Unit tests for summarize_children.
210+
"""
211+
212+
func = staticmethod(fh.summarize_children)
213+
214+
@pytest.fixture(scope="class")
215+
def childfree_tree_factory(self):
216+
"""
217+
Fixture for a child-free DataTree factory.
218+
"""
219+
from random import randint
220+
221+
def _childfree_tree_factory():
222+
return DataTree(
223+
data=xr.Dataset({"z": ("y", [randint(1, 100) for _ in range(3)])})
224+
)
225+
226+
return _childfree_tree_factory
227+
228+
@pytest.fixture(scope="class")
229+
def childfree_tree(self, childfree_tree_factory):
230+
"""
231+
Fixture for a child-free DataTree.
232+
"""
233+
return childfree_tree_factory()
234+
235+
@pytest.fixture(scope="function")
236+
def mock_node_repr(self, monkeypatch):
237+
"""
238+
Apply mocking for node_repr.
239+
"""
240+
241+
def mock(group_title, dt):
242+
"""
243+
Mock with a simple result
244+
"""
245+
return group_title + " " + str(id(dt))
246+
247+
monkeypatch.setattr(fh, "node_repr", mock)
248+
249+
@pytest.fixture(scope="function")
250+
def mock_wrap_repr(self, monkeypatch):
251+
"""
252+
Apply mocking for _wrap_repr.
253+
"""
254+
255+
def mock(r, *, end, **kwargs):
256+
"""
257+
Mock by appending "end" or "not end".
258+
"""
259+
return r + " " + ("end" if end else "not end") + "//"
260+
261+
monkeypatch.setattr(fh, "_wrap_repr", mock)
262+
263+
def test_empty_mapping(self):
264+
"""
265+
Test with an empty mapping of children.
266+
"""
267+
children = {}
268+
assert self.func(children) == (
269+
"<div style='display: inline-grid; grid-template-columns: 100%'>" "</div>"
270+
)
271+
272+
def test_one_child(self, childfree_tree, mock_wrap_repr, mock_node_repr):
273+
"""
274+
Test with one child.
275+
276+
Uses a mock of _wrap_repr and node_repr to essentially mock
277+
the inline lambda function "lines_callback".
278+
"""
279+
# Create mapping of children
280+
children = {"a": childfree_tree}
281+
282+
# Expect first line to be produced from the first child, and
283+
# wrapped as the last child
284+
first_line = f"a {id(children['a'])} end//"
285+
286+
assert self.func(children) == (
287+
"<div style='display: inline-grid; grid-template-columns: 100%'>"
288+
f"{first_line}"
289+
"</div>"
290+
)
291+
292+
def test_two_children(self, childfree_tree_factory, mock_wrap_repr, mock_node_repr):
293+
"""
294+
Test with two level deep children.
295+
296+
Uses a mock of _wrap_repr and node_repr to essentially mock
297+
the inline lambda function "lines_callback".
298+
"""
299+
300+
# Create mapping of children
301+
children = {"a": childfree_tree_factory(), "b": childfree_tree_factory()}
302+
303+
# Expect first line to be produced from the first child, and
304+
# wrapped as _not_ the last child
305+
first_line = f"a {id(children['a'])} not end//"
306+
307+
# Expect second line to be produced from the second child, and
308+
# wrapped as the last child
309+
second_line = f"b {id(children['b'])} end//"
310+
311+
assert self.func(children) == (
312+
"<div style='display: inline-grid; grid-template-columns: 100%'>"
313+
f"{first_line}"
314+
f"{second_line}"
315+
"</div>"
316+
)
317+
318+
319+
class Test__wrap_repr:
320+
"""
321+
Unit tests for _wrap_repr.
322+
"""
323+
324+
func = staticmethod(fh._wrap_repr)
325+
326+
def test_end(self, repr):
327+
"""
328+
Test with end=True.
329+
"""
330+
r = self.func(repr, end=True)
331+
assert r == (
332+
"<div style='display: inline-grid;'>"
333+
"<div style='"
334+
"grid-column-start: 1;"
335+
"border-right: 0.2em solid;"
336+
"border-color: var(--xr-border-color);"
337+
"height: 1.2em;"
338+
"width: 0px;"
339+
"'>"
340+
"</div>"
341+
"<div style='"
342+
"grid-column-start: 2;"
343+
"grid-row-start: 1;"
344+
"height: 1em;"
345+
"width: 20px;"
346+
"border-bottom: 0.2em solid;"
347+
"border-color: var(--xr-border-color);"
348+
"'>"
349+
"</div>"
350+
"<div style='"
351+
"grid-column-start: 3;"
352+
"'>"
353+
"<ul class='xr-sections'>"
354+
f"{repr}"
355+
"</ul>"
356+
"</div>"
357+
"</div>"
358+
)
359+
360+
def test_not_end(self, repr):
361+
"""
362+
Test with end=False.
363+
"""
364+
r = self.func(repr, end=False)
365+
assert r == (
366+
"<div style='display: inline-grid;'>"
367+
"<div style='"
368+
"grid-column-start: 1;"
369+
"border-right: 0.2em solid;"
370+
"border-color: var(--xr-border-color);"
371+
"height: 100%;"
372+
"width: 0px;"
373+
"'>"
374+
"</div>"
375+
"<div style='"
376+
"grid-column-start: 2;"
377+
"grid-row-start: 1;"
378+
"height: 1em;"
379+
"width: 20px;"
380+
"border-bottom: 0.2em solid;"
381+
"border-color: var(--xr-border-color);"
382+
"'>"
383+
"</div>"
384+
"<div style='"
385+
"grid-column-start: 3;"
386+
"'>"
387+
"<ul class='xr-sections'>"
388+
f"{repr}"
389+
"</ul>"
390+
"</div>"
391+
"</div>"
392+
)

0 commit comments

Comments
 (0)