I'm trying to implement _repr_html_
on a python class (docs).
The class is a read-only facade for navigating a JSON-like object using attribute notation (based on example 19-5 from Fluent Python, Rahmalho (O'Reilly)). It has a custom __getatrr__
method to achieve this behavior:
from collections import abc
class FrozenJSON:
def __init__(self, mapping):
self._data = dict(mapping)
def __repr__(self):
return "FrozenJSON({})".format(repr(self._data))
def _repr_html_(self):
return (
"<ul>"
+ "\n".join(
f"<li><strong>{k}:</strong> {v}</li>"
for k, v in self._data.items()
)
+ "</ul>"
)
def __getattr__(self, name):
if hasattr(self._data, name):
return getattr(self._data, name)
else:
return FrozenJSON.build(self._data[name])
@classmethod
def build(cls, obj):
if isinstance(obj, abc.Mapping):
return cls(obj)
elif isinstance(obj, abc.MutableSequence):
return [cls.build(item) for item in obj]
else:
return obj
def __dir__(self):
return list(self._data.keys())
The class behaves like this:
>>> record = FrozenJSON({"name": "waldo", "age": 32, "occupation": "lost"})
>>> record.occupation
'lost'
However the _repr_html_
doesn't get displayed in an IPython environment (I've tried vscode and a jupyter lab).
Commenting out the __getattr__
method causes the HTML representation to be displayed, so I'm fairly confident the issue is something to do with that.
(_repr_html_
on other objects work fine in my environments (e.g. pandas DataFrames).)
The following doesn't help:
def __getattr__(self, name):
if hasattr(self._data, name):
return getattr(self._data, name)
elif name == "_repr_html_":
return self._repr_html_
else:
return FrozenJSON.build(self._data[name])
I don't know enough about how vscode / juptyer lab knows to call _repr_html_
rather than __repr__
, and how this __getattr__
is breaking that.
Thanks in advance for any help!
IPython checks that a non-existing attribute raises an AttributeError
for a class. If that happens, apparently everything is okay and _repr_html_
is used. If no AttributeError
is raised for a non-existing attribute, __repr__
is used.
(You can find out what attribute, by printing name
inside __getattr__
; it's called _ipython_canary_method_should_not_exist_
.)
The exact how, and more importantly, why, IPython does this, I don't really know. The answer here suggests it's to verify that a class/object doesn't lie about the attributes it possesses, for example, that it doesn't says "yes, I've got this attribute" to any and all attribute request. That way, IPython is (better) guaranteed that if an attribute exists, it can use it properly.
In your case, a non-existing attribute will raise a KeyError
instead(*), because of the line
return FrozenJSON.build(self._data[name])
So make sure __getattr__
raises an actual AttributeError
when name
is not found, for example as follows:
def __getattr__(self, name):
if hasattr(self._data, name):
return getattr(self._data, name)
else:
try:
return FrozenJSON.build(self._data[name])
except KeyError:
raise AttributeError('no such attribute')
Now IPython is happy, and your HTML will be shown in a notebook.
(*) raising a KeyError
changes the behaviour of __getattr__
; that can cause all kind of problems. The first paragraph, last line, of the __getattr__
documentation states:
This method should either return the (computed) attribute value or raise an AttributeError exception.