Search code examples
pythonvisual-studio-codeipythongetattrrepr

_repr_html_ not showing when custom __getattr__ implemented


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!


Solution

  • 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.