190 lines
5.6 KiB
Python
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()
|
|
|
|
|
|
|