Skip to content

Commit 6b1ff6d

Browse files
keewisIllviljanpre-commit-ci[bot]dcherian
authored
join together duplicate entries in the text repr (#7225)
* group the variable names of common indexes * implement option 1: separate with just newlines * implement option 2: list-like presentation of the indexes * implement option 3: light box components in front of the coord names * implement option 4: light box components after the coord names * fix the repr of `Indexes` * only add combine markers for xarray indexes * select option 3 Support for the box components should be common enough that we can use it (and the worst that can happen is that those characters are displayed with a replacement glyph – usually a diamond with a question mark) * improve typing Co-authored-by: Illviljan <14371165+Illviljan@users.noreply.github.com> * whats-new.rst * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * simplify the visual hint construction * improve the existing index repr test * check that the hints are constructed correctly * convert the names to tuples * revert the typing of `index` * more conversion to tuple * type hint the instance variable * whats-new entry * don't type-check the monkeypatching * adjust the typing, again * use a subclass instead of monkeypatching and type-ignores --------- Co-authored-by: Illviljan <14371165+Illviljan@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Deepak Cherian <dcherian@users.noreply.github.com>
1 parent 1d8e92b commit 6b1ff6d

File tree

4 files changed

+95
-31
lines changed

4 files changed

+95
-31
lines changed

doc/whats-new.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ v2023.07.1 (unreleased)
2121

2222
New Features
2323
~~~~~~~~~~~~
24+
25+
- Visually group together coordinates with the same indexes in the index section of the text repr (:pull:`7225`).
26+
By `Justus Magin <https://github.com/keewis>`_.
2427
- Allow creating Xarray objects where a multidimensional variable shares its name
2528
with a dimension. Examples include output from finite volume models like FVCOM.
2629
(:issue:`2233`, :pull:`7989`)

xarray/core/formatting.py

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -424,21 +424,37 @@ def inline_index_repr(index, max_width=None):
424424

425425

426426
def summarize_index(
427-
name: Hashable, index, col_width: int, max_width: int | None = None
428-
):
427+
names: tuple[Hashable, ...],
428+
index,
429+
col_width: int,
430+
max_width: int | None = None,
431+
) -> str:
429432
if max_width is None:
430433
max_width = OPTIONS["display_width"]
431434

432-
preformatted = pretty_print(f" {name} ", col_width)
435+
def prefixes(length: int) -> list[str]:
436+
if length in (0, 1):
437+
return [" "]
438+
439+
return ["┌"] + ["│"] * max(length - 2, 0) + ["└"]
433440

434-
index_width = max_width - len(preformatted)
441+
preformatted = [
442+
pretty_print(f" {prefix} {name}", col_width)
443+
for prefix, name in zip(prefixes(len(names)), names)
444+
]
445+
446+
head, *tail = preformatted
447+
index_width = max_width - len(head)
435448
repr_ = inline_index_repr(index, max_width=index_width)
436-
return preformatted + repr_
449+
return "\n".join([head + repr_] + [line.rstrip() for line in tail])
437450

438451

439-
def nondefault_indexes(indexes):
452+
def filter_nondefault_indexes(indexes, filter_indexes: bool):
440453
from xarray.core.indexes import PandasIndex, PandasMultiIndex
441454

455+
if not filter_indexes:
456+
return indexes
457+
442458
default_indexes = (PandasIndex, PandasMultiIndex)
443459

444460
return {
@@ -448,7 +464,9 @@ def nondefault_indexes(indexes):
448464
}
449465

450466

451-
def indexes_repr(indexes, col_width=None, max_rows=None):
467+
def indexes_repr(indexes, max_rows: int | None = None) -> str:
468+
col_width = _calculate_col_width(chain.from_iterable(indexes))
469+
452470
return _mapping_repr(
453471
indexes,
454472
"Indexes",
@@ -599,6 +617,12 @@ def short_data_repr(array):
599617
return f"[{array.size} values with dtype={array.dtype}]"
600618

601619

620+
def _get_indexes_dict(indexes):
621+
return {
622+
tuple(index_vars.keys()): idx for idx, index_vars in indexes.group_by_index()
623+
}
624+
625+
602626
@recursive_repr("<recursive array>")
603627
def array_repr(arr):
604628
from xarray.core.variable import Variable
@@ -643,15 +667,13 @@ def array_repr(arr):
643667
display_default_indexes = _get_boolean_with_default(
644668
"display_default_indexes", False
645669
)
646-
if display_default_indexes:
647-
xindexes = arr.xindexes
648-
else:
649-
xindexes = nondefault_indexes(arr.xindexes)
670+
671+
xindexes = filter_nondefault_indexes(
672+
_get_indexes_dict(arr.xindexes), not display_default_indexes
673+
)
650674

651675
if xindexes:
652-
summary.append(
653-
indexes_repr(xindexes, col_width=col_width, max_rows=max_rows)
654-
)
676+
summary.append(indexes_repr(xindexes, max_rows=max_rows))
655677

656678
if arr.attrs:
657679
summary.append(attrs_repr(arr.attrs, max_rows=max_rows))
@@ -682,12 +704,11 @@ def dataset_repr(ds):
682704
display_default_indexes = _get_boolean_with_default(
683705
"display_default_indexes", False
684706
)
685-
if display_default_indexes:
686-
xindexes = ds.xindexes
687-
else:
688-
xindexes = nondefault_indexes(ds.xindexes)
707+
xindexes = filter_nondefault_indexes(
708+
_get_indexes_dict(ds.xindexes), not display_default_indexes
709+
)
689710
if xindexes:
690-
summary.append(indexes_repr(xindexes, col_width=col_width, max_rows=max_rows))
711+
summary.append(indexes_repr(xindexes, max_rows=max_rows))
691712

692713
if ds.attrs:
693714
summary.append(attrs_repr(ds.attrs, max_rows=max_rows))

xarray/core/indexes.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1621,7 +1621,8 @@ def __getitem__(self, key) -> T_PandasOrXarrayIndex:
16211621
return self._indexes[key]
16221622

16231623
def __repr__(self):
1624-
return formatting.indexes_repr(self)
1624+
indexes = formatting._get_indexes_dict(self)
1625+
return formatting.indexes_repr(indexes)
16251626

16261627

16271628
def default_indexes(

xarray/tests/test_formatting.py

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -218,31 +218,70 @@ def test_attribute_repr(self) -> None:
218218
assert "\n" not in newlines
219219
assert "\t" not in tabs
220220

221-
def test_index_repr(self):
221+
def test_index_repr(self) -> None:
222222
from xarray.core.indexes import Index
223223

224224
class CustomIndex(Index):
225-
def __init__(self, names):
225+
names: tuple[str, ...]
226+
227+
def __init__(self, names: tuple[str, ...]):
226228
self.names = names
227229

228230
def __repr__(self):
229231
return f"CustomIndex(coords={self.names})"
230232

231-
coord_names = ["x", "y"]
233+
coord_names = ("x", "y")
232234
index = CustomIndex(coord_names)
233-
name = "x"
235+
names = ("x",)
234236

235-
normal = formatting.summarize_index(name, index, col_width=20)
236-
assert name in normal
237+
normal = formatting.summarize_index(names, index, col_width=20)
238+
assert names[0] in normal
239+
assert len(normal.splitlines()) == len(names)
237240
assert "CustomIndex" in normal
238241

239-
CustomIndex._repr_inline_ = (
240-
lambda self, max_width: f"CustomIndex[{', '.join(self.names)}]"
241-
)
242-
inline = formatting.summarize_index(name, index, col_width=20)
243-
assert name in inline
242+
class IndexWithInlineRepr(CustomIndex):
243+
def _repr_inline_(self, max_width: int):
244+
return f"CustomIndex[{', '.join(self.names)}]"
245+
246+
index = IndexWithInlineRepr(coord_names)
247+
inline = formatting.summarize_index(names, index, col_width=20)
248+
assert names[0] in inline
244249
assert index._repr_inline_(max_width=40) in inline
245250

251+
@pytest.mark.parametrize(
252+
"names",
253+
(
254+
("x",),
255+
("x", "y"),
256+
("x", "y", "z"),
257+
("x", "y", "z", "a"),
258+
),
259+
)
260+
def test_index_repr_grouping(self, names) -> None:
261+
from xarray.core.indexes import Index
262+
263+
class CustomIndex(Index):
264+
def __init__(self, names):
265+
self.names = names
266+
267+
def __repr__(self):
268+
return f"CustomIndex(coords={self.names})"
269+
270+
index = CustomIndex(names)
271+
272+
normal = formatting.summarize_index(names, index, col_width=20)
273+
assert all(name in normal for name in names)
274+
assert len(normal.splitlines()) == len(names)
275+
assert "CustomIndex" in normal
276+
277+
hint_chars = [line[2] for line in normal.splitlines()]
278+
279+
if len(names) <= 1:
280+
assert hint_chars == [" "]
281+
else:
282+
assert hint_chars[0] == "┌" and hint_chars[-1] == "└"
283+
assert len(names) == 2 or hint_chars[1:-1] == ["│"] * (len(names) - 2)
284+
246285
def test_diff_array_repr(self) -> None:
247286
da_a = xr.DataArray(
248287
np.array([[1, 2, 3], [4, 5, 6]], dtype="int64"),

0 commit comments

Comments
 (0)