jupyterlab / jupyterlab

JupyterLab computational environment.
https://jupyterlab.readthedocs.io/
Other
14.12k stars 3.34k forks source link

Python object inspector for the Jupyter Notebook #7873

Open allefeld opened 4 years ago

allefeld commented 4 years ago

I'm not entirely sure this is the right place, if not sorry & please redirect me. It is a mixture between a feature suggestion and a request for comments.

When working with JavaScript, I very much appreciate the object inspector that comes with Chrome's Developer Tools Console: image

I think it would be great to have something like that for use within a Python notebook. So I started to make a very rough draft: inspector

This draft implementation creates HTML for the nested list and shows it using IPython.display.HTML. It also injects some CSS for styling and JavaScript to make list items expandable / collapsable. So far so good.

The problem is that Python object hierarchies are huge (actually infinite, because every object's __class__ is also an object with a __class__, etc.), so I have to limit the hierarchy to a few levels.

A better approach would be to dynamically generate subtrees when items are expanded. For that I would need to make the Python implementation aware of JavaScript click events. I tried ipyevents, but it does not give event target information, and the required ipywidgets.HTML (instead of IPython.display.HTML) apparently messes with my CSS & JS.

Questions:

Thanks!

jasongrout commented 4 years ago
* Just to be sure, no such interactive object inspector exists yet, right? I checked but I may have missed something. There is the variable inspector extension, but it doesn't allow exploring object hierarchies in the way I have in mind.

Good, you saw the existing variable explorer extension. There is also a lot of work going into the debugger extension, which has a variable inspector: https://github.com/jupyterlab/debugger

What would be the most straightforward way to send detailed JS event information to Python, or to request additional HTML elements from Python? I know that an extension could do this, but is there a way short of that complexity?

I would have suggested ipyevents, but apparently you already tried that. Can ipyevents be extended to give target information?

If you do go custom, writing a custom widget extension has the advantage that it may work in other frontends

allefeld commented 4 years ago

I've asked @mwcraig to comment.

The debugger looks promising. When does it come out of beta? ;)

jasongrout commented 4 years ago

When does it come out of beta? ;)

Not sure. They are working hard on it.

blois commented 4 years ago

+1 to a structured representation of objects, Polynote does a decent job of this for Scala.

I played around with a library to do this in Colab a while back, an example is: https://colab.research.google.com/gist/blois/e432e3aa45de92a09baf9a6644269a0e/copy-of-inspector.ipynb (need to execute the notebook to expand the objects).

All of the code for that one is https://github.com/blois/colab_inspector/tree/master/source/inspector.

allefeld commented 4 years ago

@blois, thanks, I'll have a look.

jtpio commented 4 years ago

Thanks @allefeld for sharing that, it looks good.

For now the variable inspector in the debugger extension (which can be tested on Binder) is still being iterated on. The goal is to keep it language agnostic and use standard calls from the Debug Adapter Protocol (see this JEP) to request variables from the kernel.

There is also a table view, which can be more convenient to inspect other types of variables and for other categories of users.

allefeld commented 4 years ago

I tried ipyevents, but it does not give event target information

I've asked @mwcraig to comment.

After thinking about this some more, I realized it's not enough to be able to listen to mouse events and get their target. To expand a subtree in response to a click I would need to be able to manipulate the DOM of an ipywidgets.HTML widget to fill in the subtree elements. As far as I can tell, there is no API to do so, and it is impossible to execute JavaScript immediately in a notebook (as opposed to creating a JavaScript cell).

So as far as I can tell, there is no way around creating a new widget, which would need an extension. I tried to follow Building a Custom Widget - Email widget, but I can't even get this example to work (chokes on cell [3]).

Please tell me if I'm wrong.

allefeld commented 4 years ago

Sorry, for the incessant commenting, I hope this stuff is interesting for someone beside me, at least for future reference.

In the meantime I found ipytree, which provides a customizable tree widget based on jsTree. It allows to listen to events, but unfortunately not to "item open/close" events but only to "item select" events. That is enough to allow me to implement my inspector, albeit in a slightly hacky way.

I also found vdom, which at first sight appeared to be perfect: create a display from HTML elements, and listen to pretty much arbitrary DOM events. Unfortunately I then found that vdom elements are immutable, which means every little change in the display necessitates recreating the whole element hierarchy.

I believe vdom done right, i.e. with modifiable objects, would be a way to allow users to create interactive HTML-based displays with relatively little effort, especially without creating an extension, but which does not involve executing arbitrary JavaScript:

Anyone care to create ipydom? ;)

dhirschfeld commented 4 years ago

@allefeld - I don't know that it does what you're after but it's kind of neat: https://github.com/nteract/vdom

rmorshea commented 4 years ago

As mentioned in my response from https://github.com/nteract/vdom/issues/83, my project idom (inspired byvdom) is probably the closest you're going to get to arbitrary DOM manipulation. You can try it out in this notebook.

allefeld commented 4 years ago

I created a first draft of a package for an interactive Python object and data inspector for the Jupyter Notebook.

https://github.com/allefeld/ipyinspector

Subnodes are created as parent items are expanded, based on the functionality of ipytree.

It also uses inspect.getattr_static to avoid activating code, and therefore shows properties as descriptors.

Comments welcome! Optimally as issues on the repository.

I'm not sure it makes sense to create a PyPI package for such a small amount of code (~170 lines), so I invite comments on how to best publish this.

Kreijstal commented 3 years ago

@blois I think google colab removed some functions on the frontend so this example doesn't work anymore. (too bad, I really enjoyed it when it did.)

rmorshea commented 3 years ago

I'm having trouble getting idom_jupyter to work in Jupyterlab right now, but here's a quick example of an inspector implemented with idom running in the classic notebook:

edit: doesn't work in google colab either :(

import idom
import idom_jupyter
from typing import Mapping, Collection

DEFAULT_STYLE = """
.idom-inspector-key-repr {
    font-weight: bold;
    padding-right: 10px;
    margin-right: 5px;
    border-right: 1px solid black;
}
"""

@idom.component
def Inspector(value, style=DEFAULT_STYLE):
    return idom.html.div(
        {"class": "idom-inspector"},
        idom.html.style(style),
        idom.html.ul(Node("root", value))
    )

@idom.component
def Node(key, value):
    is_open, set_is_open = idom.hooks.use_state(False)

    if not is_open:
        fields = {}
    else:
        if isinstance(value, Mapping):
            fields = dict(value)
        elif isinstance(value, Collection) and not isinstance(value, str):
            fields = {i: v for i, v in enumerate(value)}
        elif hasattr(value, "__dict__"):
            fields = {k: v for k, v in value.__dict__.items() if not k.startswith("_")}
        elif hasattr(value, "__slots__"):
            slots = [value.__slots__] if isinstance(value.__slots__, str) else value.__slots__
            fields = {k: getattr(value, k) for k in slots}
        else:
            fields = {}

    if is_open:
        disabled = not fields
    else:
        disabled = False

    return idom.html.li(
        idom.html.input(
            {
                "class": "idom-inspector-open-button",
                "type": "checkbox",
                "onClick": lambda event: set_is_open(not is_open),
                "disabled": disabled,
            }
        ),
        idom.html.label(
            {"class": "idom-inspector-repr-container"},
            idom.html.span({"class": "idom-inspector-key-repr"}, str(key)),
            idom.html.span({"class": "idom-inspector-value-repr"}, str(value))
        ),
        idom.html.ul(
            [Node(k, v) for k, v in fields.items()]
        )
    )

image

blois commented 3 years ago

@Kreijstal I just updated my code- it was using a browser feature which has since been removed (custom elements v0).

rmorshea commented 3 years ago

Update: idom works in JupyterLab now.