diff --git a/doc/internals.rst b/doc/internals.rst index 46c117e312b..b158f12ac6d 100644 --- a/doc/internals.rst +++ b/doc/internals.rst @@ -42,6 +42,38 @@ xarray objects via the (readonly) :py:attr:`Dataset.variables ` and :py:attr:`DataArray.variable ` attributes. +Duck arrays +----------- + +.. warning:: + + This is a experimental feature. + +xarray can wrap custom `duck array`_ objects as long as they define numpy's +``shape``, ``dtype`` and ``ndim`` properties and the ``__array__``, +``__array_ufunc__`` and ``__array_function__`` methods. + +In certain situations (e.g. when printing the collapsed preview of +variables of a ``Dataset``), xarray will display the repr of a `duck array`_ +in a single line, truncating it to a certain number of characters. If that +would drop too much information, the `duck array`_ may define a +``_repr_inline_`` method that takes ``max_width`` (number of characters) as an +argument: + +.. code:: python + + class MyDuckArray: + ... + + def _repr_inline_(self, max_width): + """ format to a single line with at most max_width characters """ + ... + + ... + +.. _duck array: https://numpy.org/neps/nep-0022-ndarray-duck-typing-overview.html + + Extending xarray ---------------- diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 81ef2cd9e17..573583f0de3 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -29,6 +29,10 @@ New Features property for :py:class:`CFTimeIndex` and show ``calendar`` and ``length`` in :py:meth:`CFTimeIndex.__repr__` (:issue:`2416`, :pull:`4092`) `Aaron Spring `_. +- Use a wrapped array's ``_repr_inline_`` method to construct the collapsed ``repr`` + of :py:class:`DataArray` and :py:class:`Dataset` objects and + document the new method in :doc:`internals`. (:pull:`4248`). + By `Justus Magin `_. Bug fixes diff --git a/xarray/core/formatting.py b/xarray/core/formatting.py index 28eaae5f05b..9aa20f2b87e 100644 --- a/xarray/core/formatting.py +++ b/xarray/core/formatting.py @@ -261,6 +261,8 @@ def inline_variable_array_repr(var, max_width): return inline_dask_repr(var.data) elif isinstance(var._data, sparse_array_type): return inline_sparse_repr(var.data) + elif hasattr(var._data, "_repr_inline_"): + return var._data._repr_inline_(max_width) elif hasattr(var._data, "__array_function__"): return maybe_truncate(repr(var._data).replace("\n", " "), max_width) else: diff --git a/xarray/tests/test_formatting.py b/xarray/tests/test_formatting.py index 82de8080c80..1cc91266421 100644 --- a/xarray/tests/test_formatting.py +++ b/xarray/tests/test_formatting.py @@ -7,6 +7,7 @@ import xarray as xr from xarray.core import formatting +from xarray.core.npcompat import IS_NEP18_ACTIVE from . import raises_regex @@ -391,6 +392,44 @@ def test_array_repr(self): assert actual == expected +@pytest.mark.skipif(not IS_NEP18_ACTIVE, reason="requires __array_function__") +def test_inline_variable_array_repr_custom_repr(): + class CustomArray: + def __init__(self, value, attr): + self.value = value + self.attr = attr + + def _repr_inline_(self, width): + formatted = f"({self.attr}) {self.value}" + if len(formatted) > width: + formatted = f"({self.attr}) ..." + + return formatted + + def __array_function__(self, *args, **kwargs): + return NotImplemented + + @property + def shape(self): + return self.value.shape + + @property + def dtype(self): + return self.value.dtype + + @property + def ndim(self): + return self.value.ndim + + value = CustomArray(np.array([20, 40]), "m") + variable = xr.Variable("x", value) + + max_width = 10 + actual = formatting.inline_variable_array_repr(variable, max_width=10) + + assert actual == value._repr_inline_(max_width) + + def test_set_numpy_options(): original_options = np.get_printoptions() with formatting.set_numpy_options(threshold=10):