"""Tree-like exploration of object referrers. This module provides a base implementation for tree-like referrers browsing. The two non-interactive classes ConsoleBrowser and FileBrowser output a tree to the console or a file. One graphical user interface for referrers browsing is provided as well. Further types can be subclassed. All types share a similar initialisation. That is, you provide a root object and may specify further settings such as the initial depth of the tree or an output function. Afterwards you can print the tree which will be arranged based on your previous settings. The interactive browser is based on a TreeWidget implemented in IDLE. It is available only if you have Tcl/Tk installed. If you try to instantiate the interactive browser without having Tkinter installed, an ImportError will be raised. """ import gc import inspect import sys from pympler import muppy from pympler import summary from pympler.util.compat import tkinter class _Node(object): """A node as it is used in the tree structure. Each node contains the object it represents and a list of children. Children can be other nodes or arbitrary other objects. Any object in a tree which is not of the type _Node is considered a leaf. """ def __init__(self, o, str_func=None): """You have to define the object this node represents. Also you can define an output function which will be used to represent this node. If no function is defined, the default str representation is used. keyword arguments str_func -- output function """ self.o = o self.children = [] self.str_func = str_func def __str__(self): """Override str(self.o) if str_func is defined.""" if self.str_func is not None: return self.str_func(self.o) else: return str(self.o) class RefBrowser(object): """Base class to other RefBrowser implementations. This base class provides means to extract a tree from a given root object and holds information on already known objects (to avoid repetition if requested). """ def __init__(self, rootobject, maxdepth=3, str_func=summary._repr, repeat=True, stream=None): """You have to provide the root object used in the refbrowser. keyword arguments maxdepth -- maximum depth of the initial tree str_func -- function used when calling str(node) repeat -- should nodes appear repeatedly in the tree, or should be referred to existing nodes stream -- output stream (used in derived classes) """ self.root = rootobject self.maxdepth = maxdepth self.str_func = str_func self.repeat = repeat self.stream = stream # objects which should be ignored while building the tree # e.g. the current frame self.ignore = [] # set of object ids which are already included self.already_included = set() self.ignore.append(self.already_included) def get_tree(self): """Get a tree of referrers of the root object.""" self.ignore.append(inspect.currentframe()) return self._get_tree(self.root, self.maxdepth) def _get_tree(self, root, maxdepth): """Workhorse of the get_tree implementation. This is a recursive method which is why we have a wrapper method. root is the current root object of the tree which should be returned. Note that root is not of the type _Node. maxdepth defines how much further down the from the root the tree should be build. """ objects = gc.get_referrers(root) res = _Node(root, self.str_func) self.already_included.add(id(root)) if maxdepth == 0: return res self.ignore.append(inspect.currentframe()) self.ignore.append(objects) for o in objects: # Ignore dict of _Node and RefBrowser objects if isinstance(o, dict): if any(isinstance(ref, (_Node, RefBrowser)) for ref in gc.get_referrers(o)): continue _id = id(o) if not self.repeat and (_id in self.already_included): s = self.str_func(o) res.children.append("%s (already included, id %s)" % (s, _id)) continue if (not isinstance(o, _Node)) and (o not in self.ignore): res.children.append(self._get_tree(o, maxdepth - 1)) return res class StreamBrowser(RefBrowser): """RefBrowser implementation which prints the tree to the console. If you don't like the looks, you can change it a little bit. The class attributes 'hline', 'vline', 'cross', and 'space' can be modified to your needs. """ hline = '-' vline = '|' cross = '+' space = ' ' def print_tree(self, tree=None): """ Print referrers tree to console. keyword arguments tree -- if not None, the passed tree will be printed. Otherwise it is based on the rootobject. """ if tree is None: tree = self.get_tree() self._print(tree, '', '') def _print(self, tree, prefix, carryon): """Compute and print a new line of the tree. This is a recursive function. arguments tree -- tree to print prefix -- prefix to the current line to print carryon -- prefix which is used to carry on the vertical lines """ level = prefix.count(self.cross) + prefix.count(self.vline) len_children = 0 if isinstance(tree, _Node): len_children = len(tree.children) # add vertex prefix += str(tree) # and as many spaces as the vertex is long carryon += self.space * len(str(tree)) if (level == self.maxdepth) or (not isinstance(tree, _Node)) or\ (len_children == 0): self.stream.write(prefix + '\n') return else: # add in between connections prefix += self.hline carryon += self.space # if there is more than one branch, add a cross if len(tree.children) > 1: prefix += self.cross carryon += self.vline prefix += self.hline carryon += self.space if len_children > 0: # print the first branch (on the same line) self._print(tree.children[0], prefix, carryon) for b in range(1, len_children): # the carryon becomes the prefix for all following children prefix = carryon[:-2] + self.cross + self.hline # remove the vlines for any children of last branch if b == (len_children - 1): carryon = carryon[:-2] + 2 * self.space self._print(tree.children[b], prefix, carryon) # leave a free line before the next branch if b == (len_children - 1): if len(carryon.strip(' ')) == 0: return self.stream.write(carryon[:-2].rstrip() + '\n') class ConsoleBrowser(StreamBrowser): """RefBrowser that prints to the console (stdout).""" def __init__(self, *args, **kwargs): super(ConsoleBrowser, self).__init__(*args, **kwargs) if not self.stream: self.stream = sys.stdout class FileBrowser(StreamBrowser): """RefBrowser implementation which prints the tree to a file.""" def print_tree(self, filename, tree=None): """ Print referrers tree to file (in text format). keyword arguments tree -- if not None, the passed tree will be printed. """ old_stream = self.stream self.stream = open(filename, 'w') try: super(FileBrowser, self).print_tree(tree=tree) finally: self.stream.close() self.stream = old_stream # Code for interactive browser (GUI) # ================================== # The interactive browser requires Tkinter which is not always available. To # avoid an import error when loading the module, we encapsulate most of the # code in the following try-except-block. The InteractiveBrowser itself # remains outside this block. If you try to instantiate it without having # Tkinter installed, the import error will be raised. try: if sys.version_info < (3, 5, 2): from idlelib import TreeWidget as _TreeWidget else: from idlelib import tree as _TreeWidget class _TreeNode(_TreeWidget.TreeNode): """TreeNode used by the InteractiveBrowser. Not to be confused with _Node. This one is used in the GUI context. """ def reload_referrers(self): """Reload all referrers for this _TreeNode.""" self.item.node = self.item.reftree._get_tree(self.item.node.o, 1) self.item._clear_children() self.expand() self.update() def print_object(self): """Print object which this _TreeNode represents to console.""" print(self.item.node.o) def drawtext(self): """Override drawtext from _TreeWidget.TreeNode. This seems to be a good place to add the popup menu. """ _TreeWidget.TreeNode.drawtext(self) # create a menu menu = tkinter.Menu(self.canvas, tearoff=0) menu.add_command(label="reload referrers", command=self.reload_referrers) menu.add_command(label="print", command=self.print_object) menu.add_separator() menu.add_command(label="expand", command=self.expand) menu.add_separator() # the popup only disappears when to click on it menu.add_command(label="Close Popup Menu") def do_popup(event): menu.post(event.x_root, event.y_root) self.label.bind("", do_popup) # override, i.e. disable the editing of items # disable editing of TreeNodes def edit(self, event=None): pass # see comment above def edit_finish(self, event=None): pass # see comment above def edit_cancel(self, event=None): pass # see comment above class _ReferrerTreeItem(_TreeWidget.TreeItem, tkinter.Label): """Tree item wrapper around _Node object.""" def __init__(self, parentwindow, node, reftree): # constr calls """You need to provide the parent window, the node this TreeItem represents, as well as the tree (_Node) which the node belongs to. """ _TreeWidget.TreeItem.__init__(self) tkinter.Label.__init__(self, parentwindow) self.node = node self.parentwindow = parentwindow self.reftree = reftree def _clear_children(self): """Clear children list from any TreeNode instances. Normally these objects are not required for memory profiling, as they are part of the profiler. """ new_children = [] for child in self.node.children: if not isinstance(child, _TreeNode): new_children.append(child) self.node.children = new_children def GetText(self): return str(self.node) def GetIconName(self): """Different icon when object cannot be expanded, i.e. has no referrers. """ if not self.IsExpandable(): return "python" def IsExpandable(self): """An object is expandable when it is a node which has children and is a container object. """ if not isinstance(self.node, _Node): return False else: if len(self.node.children) > 0: return True else: return muppy._is_containerobject(self.node.o) def GetSubList(self): """This method is the point where further referrers are computed. Thus, the computation is done on-demand and only when needed. """ sublist = [] children = self.node.children if (len(children) == 0) and\ (muppy._is_containerobject(self.node.o)): self.node = self.reftree._get_tree(self.node.o, 1) self._clear_children() children = self.node.children for child in children: item = _ReferrerTreeItem(self.parentwindow, child, self.reftree) sublist.append(item) return sublist except ImportError: _TreeWidget = None def gui_default_str_function(o): """Default str function for InteractiveBrowser.""" return summary._repr(o) + '(id=%s)' % id(o) class InteractiveBrowser(RefBrowser): """Interactive referrers browser. The interactive browser is based on a TreeWidget implemented in IDLE. It is available only if you have Tcl/Tk installed. If you try to instantiate the interactive browser without having Tkinter installed, an ImportError will be raised. """ def __init__(self, rootobject, maxdepth=3, str_func=gui_default_str_function, repeat=True): """You have to provide the root object used in the refbrowser. keyword arguments maxdepth -- maximum depth of the initial tree str_func -- function used when calling str(node) repeat -- should nodes appear repeatedly in the tree, or should be referred to existing nodes """ if tkinter is None: raise ImportError( "InteractiveBrowser requires Tkinter to be installed.") RefBrowser.__init__(self, rootobject, maxdepth, str_func, repeat) def main(self, standalone=False): """Create interactive browser window. keyword arguments standalone -- Set to true, if the browser is not attached to other windows """ window = tkinter.Tk() sc = _TreeWidget.ScrolledCanvas(window, bg="white", highlightthickness=0, takefocus=1) sc.frame.pack(expand=1, fill="both") item = _ReferrerTreeItem(window, self.get_tree(), self) node = _TreeNode(sc.canvas, None, item) node.expand() if standalone: window.mainloop() # list to hold to referrers superlist = [] root = "root" for i in range(3): tmp = [root] superlist.append(tmp) def foo(o): return str(type(o)) def print_sample(): cb = ConsoleBrowser(root, str_func=foo) cb.print_tree() def write_sample(): fb = FileBrowser(root, str_func=foo) fb.print_tree('sample.txt') if __name__ == "__main__": write_sample()