jpayne@69: """Module browser. jpayne@69: jpayne@69: XXX TO DO: jpayne@69: jpayne@69: - reparse when source changed (maybe just a button would be OK?) jpayne@69: (or recheck on window popup) jpayne@69: - add popup menu with more options (e.g. doc strings, base classes, imports) jpayne@69: - add base classes to class browser tree jpayne@69: - finish removing limitation to x.py files (ModuleBrowserTreeItem) jpayne@69: """ jpayne@69: jpayne@69: import os jpayne@69: import pyclbr jpayne@69: import sys jpayne@69: jpayne@69: from idlelib.config import idleConf jpayne@69: from idlelib import pyshell jpayne@69: from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas jpayne@69: from idlelib.window import ListedToplevel jpayne@69: jpayne@69: jpayne@69: file_open = None # Method...Item and Class...Item use this. jpayne@69: # Normally pyshell.flist.open, but there is no pyshell.flist for htest. jpayne@69: jpayne@69: jpayne@69: def transform_children(child_dict, modname=None): jpayne@69: """Transform a child dictionary to an ordered sequence of objects. jpayne@69: jpayne@69: The dictionary maps names to pyclbr information objects. jpayne@69: Filter out imported objects. jpayne@69: Augment class names with bases. jpayne@69: The insertion order of the dictionary is assumed to have been in line jpayne@69: number order, so sorting is not necessary. jpayne@69: jpayne@69: The current tree only calls this once per child_dict as it saves jpayne@69: TreeItems once created. A future tree and tests might violate this, jpayne@69: so a check prevents multiple in-place augmentations. jpayne@69: """ jpayne@69: obs = [] # Use list since values should already be sorted. jpayne@69: for key, obj in child_dict.items(): jpayne@69: if modname is None or obj.module == modname: jpayne@69: if hasattr(obj, 'super') and obj.super and obj.name == key: jpayne@69: # If obj.name != key, it has already been suffixed. jpayne@69: supers = [] jpayne@69: for sup in obj.super: jpayne@69: if type(sup) is type(''): jpayne@69: sname = sup jpayne@69: else: jpayne@69: sname = sup.name jpayne@69: if sup.module != obj.module: jpayne@69: sname = f'{sup.module}.{sname}' jpayne@69: supers.append(sname) jpayne@69: obj.name += '({})'.format(', '.join(supers)) jpayne@69: obs.append(obj) jpayne@69: return obs jpayne@69: jpayne@69: jpayne@69: class ModuleBrowser: jpayne@69: """Browse module classes and functions in IDLE. jpayne@69: """ jpayne@69: # This class is also the base class for pathbrowser.PathBrowser. jpayne@69: # Init and close are inherited, other methods are overridden. jpayne@69: # PathBrowser.__init__ does not call __init__ below. jpayne@69: jpayne@69: def __init__(self, master, path, *, _htest=False, _utest=False): jpayne@69: """Create a window for browsing a module's structure. jpayne@69: jpayne@69: Args: jpayne@69: master: parent for widgets. jpayne@69: path: full path of file to browse. jpayne@69: _htest - bool; change box location when running htest. jpayne@69: -utest - bool; suppress contents when running unittest. jpayne@69: jpayne@69: Global variables: jpayne@69: file_open: Function used for opening a file. jpayne@69: jpayne@69: Instance variables: jpayne@69: name: Module name. jpayne@69: file: Full path and module with .py extension. Used in jpayne@69: creating ModuleBrowserTreeItem as the rootnode for jpayne@69: the tree and subsequently in the children. jpayne@69: """ jpayne@69: self.master = master jpayne@69: self.path = path jpayne@69: self._htest = _htest jpayne@69: self._utest = _utest jpayne@69: self.init() jpayne@69: jpayne@69: def close(self, event=None): jpayne@69: "Dismiss the window and the tree nodes." jpayne@69: self.top.destroy() jpayne@69: self.node.destroy() jpayne@69: jpayne@69: def init(self): jpayne@69: "Create browser tkinter widgets, including the tree." jpayne@69: global file_open jpayne@69: root = self.master jpayne@69: flist = (pyshell.flist if not (self._htest or self._utest) jpayne@69: else pyshell.PyShellFileList(root)) jpayne@69: file_open = flist.open jpayne@69: pyclbr._modules.clear() jpayne@69: jpayne@69: # create top jpayne@69: self.top = top = ListedToplevel(root) jpayne@69: top.protocol("WM_DELETE_WINDOW", self.close) jpayne@69: top.bind("", self.close) jpayne@69: if self._htest: # place dialog below parent if running htest jpayne@69: top.geometry("+%d+%d" % jpayne@69: (root.winfo_rootx(), root.winfo_rooty() + 200)) jpayne@69: self.settitle() jpayne@69: top.focus_set() jpayne@69: jpayne@69: # create scrolled canvas jpayne@69: theme = idleConf.CurrentTheme() jpayne@69: background = idleConf.GetHighlight(theme, 'normal')['background'] jpayne@69: sc = ScrolledCanvas(top, bg=background, highlightthickness=0, jpayne@69: takefocus=1) jpayne@69: sc.frame.pack(expand=1, fill="both") jpayne@69: item = self.rootnode() jpayne@69: self.node = node = TreeNode(sc.canvas, None, item) jpayne@69: if not self._utest: jpayne@69: node.update() jpayne@69: node.expand() jpayne@69: jpayne@69: def settitle(self): jpayne@69: "Set the window title." jpayne@69: self.top.wm_title("Module Browser - " + os.path.basename(self.path)) jpayne@69: self.top.wm_iconname("Module Browser") jpayne@69: jpayne@69: def rootnode(self): jpayne@69: "Return a ModuleBrowserTreeItem as the root of the tree." jpayne@69: return ModuleBrowserTreeItem(self.path) jpayne@69: jpayne@69: jpayne@69: class ModuleBrowserTreeItem(TreeItem): jpayne@69: """Browser tree for Python module. jpayne@69: jpayne@69: Uses TreeItem as the basis for the structure of the tree. jpayne@69: Used by both browsers. jpayne@69: """ jpayne@69: jpayne@69: def __init__(self, file): jpayne@69: """Create a TreeItem for the file. jpayne@69: jpayne@69: Args: jpayne@69: file: Full path and module name. jpayne@69: """ jpayne@69: self.file = file jpayne@69: jpayne@69: def GetText(self): jpayne@69: "Return the module name as the text string to display." jpayne@69: return os.path.basename(self.file) jpayne@69: jpayne@69: def GetIconName(self): jpayne@69: "Return the name of the icon to display." jpayne@69: return "python" jpayne@69: jpayne@69: def GetSubList(self): jpayne@69: "Return ChildBrowserTreeItems for children." jpayne@69: return [ChildBrowserTreeItem(obj) for obj in self.listchildren()] jpayne@69: jpayne@69: def OnDoubleClick(self): jpayne@69: "Open a module in an editor window when double clicked." jpayne@69: if os.path.normcase(self.file[-3:]) != ".py": jpayne@69: return jpayne@69: if not os.path.exists(self.file): jpayne@69: return jpayne@69: file_open(self.file) jpayne@69: jpayne@69: def IsExpandable(self): jpayne@69: "Return True if Python (.py) file." jpayne@69: return os.path.normcase(self.file[-3:]) == ".py" jpayne@69: jpayne@69: def listchildren(self): jpayne@69: "Return sequenced classes and functions in the module." jpayne@69: dir, base = os.path.split(self.file) jpayne@69: name, ext = os.path.splitext(base) jpayne@69: if os.path.normcase(ext) != ".py": jpayne@69: return [] jpayne@69: try: jpayne@69: tree = pyclbr.readmodule_ex(name, [dir] + sys.path) jpayne@69: except ImportError: jpayne@69: return [] jpayne@69: return transform_children(tree, name) jpayne@69: jpayne@69: jpayne@69: class ChildBrowserTreeItem(TreeItem): jpayne@69: """Browser tree for child nodes within the module. jpayne@69: jpayne@69: Uses TreeItem as the basis for the structure of the tree. jpayne@69: """ jpayne@69: jpayne@69: def __init__(self, obj): jpayne@69: "Create a TreeItem for a pyclbr class/function object." jpayne@69: self.obj = obj jpayne@69: self.name = obj.name jpayne@69: self.isfunction = isinstance(obj, pyclbr.Function) jpayne@69: jpayne@69: def GetText(self): jpayne@69: "Return the name of the function/class to display." jpayne@69: name = self.name jpayne@69: if self.isfunction: jpayne@69: return "def " + name + "(...)" jpayne@69: else: jpayne@69: return "class " + name jpayne@69: jpayne@69: def GetIconName(self): jpayne@69: "Return the name of the icon to display." jpayne@69: if self.isfunction: jpayne@69: return "python" jpayne@69: else: jpayne@69: return "folder" jpayne@69: jpayne@69: def IsExpandable(self): jpayne@69: "Return True if self.obj has nested objects." jpayne@69: return self.obj.children != {} jpayne@69: jpayne@69: def GetSubList(self): jpayne@69: "Return ChildBrowserTreeItems for children." jpayne@69: return [ChildBrowserTreeItem(obj) jpayne@69: for obj in transform_children(self.obj.children)] jpayne@69: jpayne@69: def OnDoubleClick(self): jpayne@69: "Open module with file_open and position to lineno." jpayne@69: try: jpayne@69: edit = file_open(self.obj.file) jpayne@69: edit.gotoline(self.obj.lineno) jpayne@69: except (OSError, AttributeError): jpayne@69: pass jpayne@69: jpayne@69: jpayne@69: def _module_browser(parent): # htest # jpayne@69: if len(sys.argv) > 1: # If pass file on command line. jpayne@69: file = sys.argv[1] jpayne@69: else: jpayne@69: file = __file__ jpayne@69: # Add nested objects for htest. jpayne@69: class Nested_in_func(TreeNode): jpayne@69: def nested_in_class(): pass jpayne@69: def closure(): jpayne@69: class Nested_in_closure: pass jpayne@69: ModuleBrowser(parent, file, _htest=True) jpayne@69: jpayne@69: if __name__ == "__main__": jpayne@69: if len(sys.argv) == 1: # If pass file on command line, unittest fails. jpayne@69: from unittest import main jpayne@69: main('idlelib.idle_test.test_browser', verbosity=2, exit=False) jpayne@69: from idlelib.idle_test.htest import run jpayne@69: run(_module_browser)