mirror of
https://github.com/aykhans/AzSuicideDataVisualization.git
synced 2025-04-21 10:15:45 +00:00
452 lines
15 KiB
Python
452 lines
15 KiB
Python
"""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("<Button-3>", 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()
|