Files
ipynb-variable-inspector/inspector.py

190 lines
5.6 KiB
Python

import inspect
import re
import types
import weakref
from types import SimpleNamespace
import ipywidgets
import numpy as np
from matplotlib.colors import TABLEAU_COLORS
HEADER = '<div class="rendered_html jp-RenderedHTMLCommon"><table><thead><tr><th>Name</th><th>Type</th><th>Size</th><th>Value</th></tr></thead><tr><td>'
FOOTER = '</td></tr></table></div>'
SEP = '</td></tr><tr><td>'
LINE = '{0}</td><td style="background-color:{4};color:{5};">{1}</td><td>{2}</td><td>{3}'
IGNORE_NAMES = ("In", "Out", "exit", "quit", "get_ipython")
SNIP = " ...✀... "
RE_DIGITS = re.compile("([0-9]+)")
colors = SimpleNamespace(**{
name.split(":")[1]: color for name, color in TABLEAU_COLORS.items()
})
cmap = {
bool: colors.pink, # bool must be tested first, otherwise it's treated as int
int: colors.purple,
float: colors.cyan,
str: colors.red,
list: colors.green,
tuple: colors.olive,
set: colors.orange,
dict: colors.blue,
np.ndarray: colors.brown
}
class Singleton(type):
# inspired by https://stackoverflow.com/a/6798042/655404
def __init__(cls, name, bases, namespace):
super().__init__(name, bases, namespace) # creates the class
cls.__signature__ = inspect.signature(cls.__init__) # restore the constructor signature (instead of that of __call__ below)
cls.__instance__ = lambda: None # fake dead weakref
def __call__(cls, *args, **kwargs):
inst = cls.__instance__()
if not inst:
inst = super().__call__(*args, **kwargs) # creates the instance (calls __new__ and __init__ methods)
cls.__instance__ = weakref.ref(inst)
return inst
class VariableInspector(object, metaclass=Singleton):
# inspired by https://github.com/jupyter-widgets/ipywidgets/blob/7.x/docs/source/examples/Variable%20Inspector.ipynb
def __init__(self, ipython=None):
self._box = ipywidgets.Box()
self._box.layout.overflow_y = "scroll"
self._table = ipywidgets.HTML(value="Loading...")
self._box.children = [self._table]
self._ipython = ipython or get_ipython()
self.start()
def start(self):
"""Add update callback if not already registered"""
if self.is_running:
raise RuntimeError("Cannot start. Update callback is already registered")
self._ipython.events.register("post_run_cell", self._update)
def stop(self):
"""Remove update callback if registered"""
if not self.is_running:
raise RuntimeError("Cannot stop. Update callback is not registered")
self._ipython.events.unregister("post_run_cell", self._update)
@property
def is_running(self):
"""Test if update callback is registered"""
return self._update in self._ipython.events.callbacks["post_run_cell"]
def _update(self, _):
"""Fill table with variable information"""
namespace = self._ipython.user_ns.items()
lines = (format_line(k, v) for k, v in sorted_naturally(namespace) if is_good_entry(k, v))
self._table.value = HEADER + SEP.join(lines) + FOOTER
def _ipython_display_(self):
"""Called when display() or pyout is used to display the Variable Inspector"""
try:
self._box._ipython_display_()
except:
pass
def format_line(k, v):
return LINE.format(k, format_type(v), format_size(v), format_value(v), *format_colors(v))
def sorted_naturally(iterable, reverse=False):
natural = lambda item: [int(c) if c.isdigit() else c.casefold() for c in RE_DIGITS.split(str(item))]
return sorted(iterable, key=natural, reverse=reverse)
def is_good_entry(k, v):
# #TODO: completely ignore objects with errors here? or use error as value (see format_value below)
# try:
# str(v) # have to be able to create a string representation for the object
# except Exception:
# return False
ignore_types = (VariableInspector, types.ModuleType, type) # hide modules and classes
return not k.startswith("_") and k not in IGNORE_NAMES and not isinstance(v, ignore_types)
def format_type(obj):
tn = typename(obj)
try:
dtype = obj.dtype
except Exception: # Exception just in case something besides AttributeError is raised
return tn
else:
return f"{dtype} {tn}"
def format_size(obj):
try:
return obj.shape
except Exception: # Exception just in case something besides AttributeError is raised
try:
return len(obj)
except TypeError:
return ""
def format_value(obj): #TODO: make magic numbers configurable
# if string representation cannot be created, use error as value
#TODO: cf. is_good_entry for alternative way of dealing with this
try:
res = str(obj)
except Exception as e:
res = printable_error(e)
# try if separate lines can be used to shorten
splitted = res.split("\n")
if len(splitted) > 4:
res = splitted[0] + SNIP + splitted[-1]
# if still too long (or no lines), cut the middle part
if len(res) < 120:
return res
res = res[:50] + SNIP + res[-50:]
return res
def format_colors(obj):
bkg = format_bkg_color(obj)
if bkg:
return bkg, "black" # use black font on colored background
return "", "" # use the default colors
def format_bkg_color(obj):
for typ, col in cmap.items():
if isinstance(obj, typ):
return col
return None
def typename(obj):
return type(obj).__name__
def printable_error(exc):
tn = typename(exc)
res = f"caused a {tn}"
if str(exc): # exc has a message
res += f": {exc}"
return res
inspector = VariableInspector()