From 6db62d25f28e9561cca4659efd03af0feed92d53 Mon Sep 17 00:00:00 2001 From: Thomas Teisberg Date: Wed, 20 Aug 2025 17:02:44 -0700 Subject: [PATCH 1/3] display nested attribute dictionaries collapsibly in notebook output --- xarray/core/formatting_html.py | 22 +++++++++++++--- xarray/static/css/style.css | 47 ++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/xarray/core/formatting_html.py b/xarray/core/formatting_html.py index 46c6709d118..b53255f3cb5 100644 --- a/xarray/core/formatting_html.py +++ b/xarray/core/formatting_html.py @@ -62,10 +62,24 @@ def format_dims(dim_sizes, dims_with_index) -> str: def summarize_attrs(attrs) -> str: - attrs_dl = "".join( - f"
{escape(str(k))} :
{escape(str(v))}
" - for k, v in attrs.items() - ) + attrs_dl = "" + for k, v in attrs.items(): + if isinstance(v, dict): + attr_id = "attrs-" + str(uuid.uuid4()) + + attrs_dl += "
" + attrs_dl += f"" + attrs_dl += ( + f"" + attrs_dl += "" + attrs_dl += summarize_attrs(v) + attrs_dl += "
" + else: + attrs_dl += ( + f"
{escape(str(k))} :
{escape(str(v))}
" + ) return f"
{attrs_dl}
" diff --git a/xarray/static/css/style.css b/xarray/static/css/style.css index 10c41cfc6d2..622473ff7e7 100644 --- a/xarray/static/css/style.css +++ b/xarray/static/css/style.css @@ -407,6 +407,53 @@ dl.xr-attrs { word-break: break-all; } +.xr-attr-item { + grid-column: 1/-1; +} + +.xr-attr-item input { + display: none; +} + +.xr-attr-item label.xr-attr-nested { + border: 0 !important; +} + +.xr-attr label > span { + display: inline-block; + padding-left: 0.5em; +} + +.xr-attr-item input + label.xr-attr-nested { + color: var(--xr-font-color0); +} + +.xr-attr-in + label::before { + display: inline-block; + content: "►"; + font-size: 11px; + width: 15px; + margin-left: -15px; + text-align: center; +} + +.xr-attr-in:checked + label:before { + content: "▼"; +} + +.xr-attr-in:checked + label > span { + display: none; +} + +.xr-attr-nested-inner { + display: none; + padding-left: 1em; +} + +.xr-attr-in:checked ~ .xr-attr-nested-inner { + display: block; +} + .xr-icon-database, .xr-icon-file-text2, .xr-no-icon { From f9b4e8c052482977abee279593283afb7a84c9e4 Mon Sep 17 00:00:00 2001 From: Thomas Teisberg Date: Tue, 26 Aug 2025 18:20:09 -0700 Subject: [PATCH 2/3] Preview attrs by _repr_html_inline_ and allow expanding --- xarray/core/formatting_html.py | 69 +++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/xarray/core/formatting_html.py b/xarray/core/formatting_html.py index b53255f3cb5..01fb29430f1 100644 --- a/xarray/core/formatting_html.py +++ b/xarray/core/formatting_html.py @@ -64,26 +64,67 @@ def format_dims(dim_sizes, dims_with_index) -> str: def summarize_attrs(attrs) -> str: attrs_dl = "" for k, v in attrs.items(): - if isinstance(v, dict): - attr_id = "attrs-" + str(uuid.uuid4()) + # Supply default repr values for certain built-in type (so far, just dict) - attrs_dl += "
" - attrs_dl += f"" - attrs_dl += ( - f"" - attrs_dl += "" - attrs_dl += summarize_attrs(v) - attrs_dl += "
" + # Check for a _repr_html_inline_ method + if hasattr(v, "_repr_html_inline_"): + repr_html_inline_fn = v._repr_html_inline_ + elif isinstance(v, dict): + repr_html_inline_fn = lambda: _default_repr_html_inline_dict(v) # noqa: B023 else: - attrs_dl += ( - f"
{escape(str(k))} :
{escape(str(v))}
" - ) + repr_html_inline_fn = None + # Check for a _repr_html_ method + if hasattr(v, "_repr_html_"): + repr_html_fn = v._repr_html_ + elif isinstance(v, dict): + repr_html_fn = lambda: _default_repr_html_dict(v) # noqa: B023 + else: + repr_html_fn = None + + if repr_html_inline_fn and repr_html_fn: + # If we have both, then consider if we need to include an expand option + inline_repr = repr_html_inline_fn() + full_repr = repr_html_fn() + if inline_repr == full_repr: + value_representation = inline_repr + else: + # Show the inline, with an option to expand and show the full + value_representation = _collapsible_repr(inline_repr, full_repr) + elif repr_html_inline_fn: + value_representation = escape(repr_html_inline_fn()) + else: + value_representation = escape(str(v)) + + attrs_dl += ( + f"
{escape(str(k))} :
{value_representation}
" + ) return f"
{attrs_dl}
" +def _default_repr_html_inline_dict(d: dict) -> str: + return f"dict : ({len(d)})" + + +def _default_repr_html_dict(d: dict) -> str: + return summarize_attrs(d) + + +def _collapsible_repr(inline_html: str, full_html: str) -> str: + r_id = "attrs-" + str(uuid.uuid4()) + + r = "
" + r += f"" + r += f"" + r += "" + r += full_html + r += "
" + + return r + + def _icon(icon_name) -> str: # icon_name should be defined in xarray/static/html/icon-svg-inline.html return ( From 3edc0ea8a2c1b1531f28b613cdff48226fe24bad Mon Sep 17 00:00:00 2001 From: Thomas Teisberg Date: Tue, 26 Aug 2025 18:21:08 -0700 Subject: [PATCH 3/3] Add temporary test notebook --- Test Notebook.ipynb | 714 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 714 insertions(+) create mode 100644 Test Notebook.ipynb diff --git a/Test Notebook.ipynb b/Test Notebook.ipynb new file mode 100644 index 00000000000..68f1067189a --- /dev/null +++ b/Test Notebook.ipynb @@ -0,0 +1,714 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "7b616f68", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "159b238e", + "metadata": {}, + "outputs": [], + "source": [ + "import dask.array as da\n", + "from IPython.display import HTML, display\n", + "\n", + "import xarray as xr" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "90cd35af", + "metadata": {}, + "outputs": [], + "source": [ + "class TestDuckArray(da.Array):\n", + " def _repr_html_inline_(self):\n", + " return f\"TestDuckArray of shape {self.shape}\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f83ba9d1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "TestDuckArray of shape (20,)" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "a = da.linspace(0, 1, 20, chunks=2)\n", + "\n", + "test_array = TestDuckArray(\n", + " dask=a.__dask_graph__(), name=a.name, chunks=a.chunks, dtype=a.dtype, meta=a._meta\n", + ")\n", + "display(HTML(test_array._repr_html_inline_()))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "16d2676f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 0B\n",
+       "Dimensions:  ()\n",
+       "Data variables:\n",
+       "    *empty*\n",
+       "Attributes:\n",
+       "    test:           This is a test dataset\n",
+       "    nested_dict:    {'key1': 'value1', 'key2': 'value2', 'nested': {'subkey1'...\n",
+       "    nested_dict_2:  {'key3': 'value3', 'key4': 'value4'}\n",
+       "    duck_array:     dask.array<linspace, shape=(20,), dtype=float64, chunksiz...\n",
+       "    regular_key:    regular_value
" + ], + "text/plain": [ + " Size: 0B\n", + "Dimensions: ()\n", + "Data variables:\n", + " *empty*\n", + "Attributes:\n", + " test: This is a test dataset\n", + " nested_dict: {'key1': 'value1', 'key2': 'value2', 'nested': {'subkey1'...\n", + " nested_dict_2: {'key3': 'value3', 'key4': 'value4'}\n", + " duck_array: dask.array