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 = '
NameTypeSizeValue
' FOOTER = '
' SEP = '' LINE = '{0}{1}{2}{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()