class ObjectBrowser: """Introspect an object with a tree-browser GUI. Click on the [+] or double-click on the folder icons to expand 1 level. At the top of this GUI 3 Radiobuttons are visible. Default is 'Attributes' which means we do not include callable methods. 'Methods' will only display callable methods. 'All' will display everything. Required constructor argument: the objetc to be browsed. Optional constructor arguments: rootName: a string used as the name for the root node. Default: 'root' title: a string used as window title. Default: 'Python Introspector' refresh: this has to be either None or a callable method that returns an object to be introspected. If refresh is not None a new button is added to the left of the Dismiss button. Clicking this button calls the callable method (which must return a new object) and rebuilds the tree. If refresh is None, we do not add the 'Refresh' button. Default: None API: show() displays the GUI hide() hides the GUI (this does not destroy the window) rebuild() destroys the current tree and rebuilds it To get an idea of how this thing works, start Python, import this class, then type: browser = ObjectBrowser( __builtins__ ) """ def __init__(self, object, rootName='root', title='Python Introspector', refresh=None): self.object = object # the object to be introspeced self.rootName = rootName # name the root node self.title = title # title of the GUI assert refresh is None or callable(refresh) self.refresh = refresh # used to rebuild tree with new data self.busyRebuilding = False self.buildGUI() def buildGUI(self): self.root = tkinter.Toplevel() self.root.title(self.title) self.root.protocol('WM_DELETE_WINDOW', self.hide ) self.topFrame = tkinter.Frame(self.root, relief='raised', bd=4) self.topFrame.pack(fill="x", side='top', ) choices = ['All', 'Attributes', 'Methods'] self.choicesVarTk = tkinter.StringVar() self.choicesVarTk.set('Attributes') for c in choices: b = tkinter.Radiobutton( self.topFrame, variable = self.choicesVarTk, text=c, value=c, command=self.rebuild) b.pack(side='left', expand=1, fill='x') self.frame = tkinter.Frame(self.root) self.frame.pack(expand=1, fill="both")#, side='bottom') frame = tkinter.Frame(self.root, bd=3) frame.pack(fill="x", side='bottom') # add Refresh button if specified if self.refresh is not None: button1 = tkinter.Button(frame, text='Refresh', command=self.refresh_cb) button1.pack(expand=1, fill="x", side='left') button2 = tkinter.Button(frame, text='Dismiss', command=self.hide) button2.pack(expand=1, fill="x", side='left') else: button2 = tkinter.Button(frame, text='Dismiss', command=self.hide) button2.pack(expand=1, fill="x") # add root node self.createTree() def show(self, event=None): """show GUI""" self.root.deiconify() if self.refresh is not None: data = self.refresh() if data != self.object: self.object = data self.rebuild() def hide(self, event=None): """hide GUI, note: this does not destroy the GUI""" self.root.withdraw() def createTree(self): """build a TreeView object, add it to the GUI""" self.tree = TreeView(master=self.frame, nohistory=True, displayValue=True) # I want white background, so: self.tree.canvas.configure(bg='white') hasChildren = not self.isLeafNode(self.object) node = self.tree.addNode(parent=None, name=self.rootName, object=self.object, hasChildren=hasChildren, firstExpand_cb=self.expandTreeNode_cb) def expandTreeNode_cb(self, node=None, object=None): """expand the given object by 1 level (i.e. all its children)""" if type(object) in [list, tuple]: children = [] i = 0 for o in object: children.append( (str(i), o) ) i = i + 1 elif type(object) == dict: children = [] for k, v in list(object.items()): children.append( (k, v) ) elif type(object) in [ bool, float, int, int, type(None), bytes ]: children = [] else: if self.choicesVarTk.get() == 'All': children = inspect.getmembers(object) elif self.choicesVarTk.get() == 'Attributes': children = inspect.getmembers(object, lambda x: type(x) is not types.MethodType) elif self.choicesVarTk.get() == 'Methods': if node.parent is None: # only at the first level do we # distinguish between methods, attributes, etc children = inspect.getmembers(object, inspect.ismethod) else: #second and deeper levels: here we want to see everything children = inspect.getmembers(object) from time import time t1 = time() nameList=[] objList=[] hasChildrenList=[] for (name, data) in children: hasChildren = not self.isLeafNode(data) hasChildrenList.append(hasChildren) nameList.append(str(name)) objList.append(data) n = self.tree.addNodeSet(parent=node, name=nameList, object=objList, hasChildren=hasChildrenList, firstExpand_cb=self.expandTreeNode_cb) # self.tree.canvas.update() #print "firstExpand_cb :", time()-t1 def isLeafNode(self, x): """Helper method: returns True if this object does not have children (e.g., int, float, boolean, etc)""" if type(x) in [ bool, float, int, int, type(None), bytes]: return True elif type(x) in [dict, list] and \ len(x)==0: return True else: return False def rebuild(self, event=None): """destroy old tree, delete frame, create new tree and add it to the GUI""" self.tree.destroy() self.tree = None self.createTree() self.busyRebuilding = False def refresh_cb(self, event=None): """rebuild tree with new data (calls rebuild)""" # Note: on slow machines, if one repeatetly clicks the 'Refresh' # button very, very fast, we can reach a state where the canvas has # been destroyed while we try to add an icon on it if self.busyRebuilding is True: return if self.refresh is not None: self.busyRebuilding = True # call the callback to get a new object to introspect self.object = self.refresh() # and rebuild tree self.rebuild()