Repository: zarr-developers/zarr-python

Issue:
Implement ipython repr methods on Group to prevent expensive lookups
### Zarr version

2.16.1

### Numcodecs version

...

### Python Version

...

### Operating System

...

### Installation

conda

### Description

`ipython` will look for 11 custom repr methods (https://ipython.readthedocs.io/en/stable/config/integrating.html#custom-methods) in a zarr group.

Zarr chooses to map any `hasattr` query to a `__getitem__` call so sach of these queries becomes a `__getitem__` call for (e.g.) `_repr_mimebundle_.array.json` and `_repr_mimebundle_.array.json` and is interleaved with a request for `_ipython_canary_method_should_not_exist_.[array|group].json`.

I count **44** useless requests for a simple `zarr.open_group`.

I think a good solution would be to implement `_ipython_display_` or `_repr_mimebundle_`.

It would also be nice to raise `AttributeError` for `_ipython_canary_method_should_not_exist_`.

xref https://github.com/jupyter/notebook/issues/2014

### Steps to reproduce

1. Create a zarr group
2. Open it with some way of recording __getitem__ requests.

### Additional output

_No response_

Discussion:
MSanKeys963: Thanks, @dcherian.
I've launched the workflows.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/zarr-developers/zarr-python/pull/1724?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zarr-developers) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.99%. Comparing base [(`8d0d910`)](https://app.codecov.io/gh/zarr-developers/zarr-python/commit/8d0d910f27453157558d201e7a6a84bb565c87ad?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zarr-developers) to head [(`dc762cc`)](https://app.codecov.io/gh/zarr-developers/zarr-python/pull/1724?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zarr-developers).

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #1724   +/-   ##
=======================================
  Coverage   99.99%   99.99%           
=======================================
  Files          38       38           
  Lines       14580    14611   +31     
=======================================
+ Hits        14579    14610   +31     
  Misses          1        1           
```

| [Files](https://app.codecov.io/gh/zarr-developers/zarr-python/pull/1724?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zarr-developers) | Coverage Δ | |
|---|---|---|
| [zarr/hierarchy.py](https://app.codecov.io/gh/zarr-developers/zarr-python/pull/1724?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zarr-developers#diff-emFyci9oaWVyYXJjaHkucHk=) | `100.00% <100.00%> (ø)` | |
| [zarr/tests/test\_hierarchy.py](https://app.codecov.io/gh/zarr-developers/zarr-python/pull/1724?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zarr-developers#diff-emFyci90ZXN0cy90ZXN0X2hpZXJhcmNoeS5weQ==) | `100.00% <100.00%> (ø)` | |

</details>

Patch:
diff --git a/docs/release.rst b/docs/release.rst
index 116393d4..fd48a53b 100644
--- a/docs/release.rst
+++ b/docs/release.rst
@@ -18,6 +18,12 @@ Release notes
 Unreleased
 ----------
 
+Enhancements
+~~~~~~~~~~~~
+
+* Override IPython ``_repr_*_`` methods to avoid expensive lookups against object stores.
+  By :user:`Deepak Cherian <dcherian>` :issue:`1716`.
+
 Maintenance
 ~~~~~~~~~~~
 
diff --git a/zarr/hierarchy.py b/zarr/hierarchy.py
index 44af1d63..c88892c9 100644
--- a/zarr/hierarchy.py
+++ b/zarr/hierarchy.py
@@ -515,6 +515,13 @@ class Group(MutableMapping):
             raise KeyError(item)
 
     def __getattr__(self, item):
+        # https://github.com/jupyter/notebook/issues/2014
+        # Save a possibly expensive lookup (for e.g. against cloud stores)
+        # Note: The _ipython_display_ method is required to display the right info as a side-effect.
+        # It is simpler to pretend it doesn't exist.
+        if item in ["_ipython_canary_method_should_not_exist_", "_ipython_display_"]:
+            raise AttributeError
+
         # allow access to group members via dot notation
         try:
             return self.__getitem__(item)
@@ -1331,6 +1338,40 @@ class Group(MutableMapping):
 
         self._write_op(self._move_nosync, source, dest)
 
+    # Override ipython repr methods, GH1716
+    # https://ipython.readthedocs.io/en/stable/config/integrating.html#custom-methods
+    #     " If the methods don’t exist, the standard repr() is used. If a method exists and
+    #       returns None, it is treated the same as if it does not exist."
+    def _repr_html_(self):
+        return None
+
+    def _repr_latex_(self):
+        return None
+
+    def _repr_mimebundle_(self, **kwargs):
+        return None
+
+    def _repr_svg_(self):
+        return None
+
+    def _repr_png_(self):
+        return None
+
+    def _repr_jpeg_(self):
+        return None
+
+    def _repr_markdown_(self):
+        return None
+
+    def _repr_javascript_(self):
+        return None
+
+    def _repr_pdf_(self):
+        return None
+
+    def _repr_json_(self):
+        return None
+
 
 def _normalize_store_arg(store, *, storage_options=None, mode="r", zarr_version=None):
     if zarr_version is None:


Test patch:
diff --git a/zarr/tests/test_hierarchy.py b/zarr/tests/test_hierarchy.py
index 6c08d7b8..161e1eb8 100644
--- a/zarr/tests/test_hierarchy.py
+++ b/zarr/tests/test_hierarchy.py
@@ -1,4 +1,5 @@
 import atexit
+import operator
 import os
 import sys
 import pickle
@@ -87,6 +88,26 @@ class TestGroup(unittest.TestCase):
         )
         return g
 
+    def test_ipython_repr_methods(self):
+        g = self.create_group()
+        for method in [
+            "html",
+            "json",
+            "javascript",
+            "markdown",
+            "svg",
+            "png",
+            "jpeg",
+            "latex",
+            "pdf",
+            "mimebundle",
+        ]:
+            assert operator.methodcaller(f"_repr_{method}_")(g) is None
+        with pytest.raises(AttributeError):
+            g._ipython_display_()
+        with pytest.raises(AttributeError):
+            g._ipython_canary_method_should_not_exist_()
+
     def test_group_init_1(self):
         store, chunk_store = self.create_store()
         g = self.create_group(store, chunk_store=chunk_store)