1"""Module browser. 2 3XXX TO DO: 4 5- reparse when source changed (maybe just a button would be OK?) 6 (or recheck on window popup) 7- add popup menu with more options (e.g. doc strings, base classes, imports) 8- add base classes to class browser tree 9- finish removing limitation to x.py files (ModuleBrowserTreeItem) 10""" 11 12import os 13import pyclbr 14import sys 15 16from idlelib.config import idleConf 17from idlelib import pyshell 18from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas 19from idlelib.window import ListedToplevel 20 21 22file_open = None # Method...Item and Class...Item use this. 23# Normally pyshell.flist.open, but there is no pyshell.flist for htest. 24 25 26def transform_children(child_dict, modname=None): 27 """Transform a child dictionary to an ordered sequence of objects. 28 29 The dictionary maps names to pyclbr information objects. 30 Filter out imported objects. 31 Augment class names with bases. 32 The insertion order of the dictionary is assumed to have been in line 33 number order, so sorting is not necessary. 34 35 The current tree only calls this once per child_dict as it saves 36 TreeItems once created. A future tree and tests might violate this, 37 so a check prevents multiple in-place augmentations. 38 """ 39 obs = [] # Use list since values should already be sorted. 40 for key, obj in child_dict.items(): 41 if modname is None or obj.module == modname: 42 if hasattr(obj, 'super') and obj.super and obj.name == key: 43 # If obj.name != key, it has already been suffixed. 44 supers = [] 45 for sup in obj.super: 46 if type(sup) is type(''): 47 sname = sup 48 else: 49 sname = sup.name 50 if sup.module != obj.module: 51 sname = f'{sup.module}.{sname}' 52 supers.append(sname) 53 obj.name += '({})'.format(', '.join(supers)) 54 obs.append(obj) 55 return obs 56 57 58class ModuleBrowser: 59 """Browse module classes and functions in IDLE. 60 """ 61 # This class is also the base class for pathbrowser.PathBrowser. 62 # Init and close are inherited, other methods are overridden. 63 # PathBrowser.__init__ does not call __init__ below. 64 65 def __init__(self, master, path, *, _htest=False, _utest=False): 66 """Create a window for browsing a module's structure. 67 68 Args: 69 master: parent for widgets. 70 path: full path of file to browse. 71 _htest - bool; change box location when running htest. 72 -utest - bool; suppress contents when running unittest. 73 74 Global variables: 75 file_open: Function used for opening a file. 76 77 Instance variables: 78 name: Module name. 79 file: Full path and module with .py extension. Used in 80 creating ModuleBrowserTreeItem as the rootnode for 81 the tree and subsequently in the children. 82 """ 83 self.master = master 84 self.path = path 85 self._htest = _htest 86 self._utest = _utest 87 self.init() 88 89 def close(self, event=None): 90 "Dismiss the window and the tree nodes." 91 self.top.destroy() 92 self.node.destroy() 93 94 def init(self): 95 "Create browser tkinter widgets, including the tree." 96 global file_open 97 root = self.master 98 flist = (pyshell.flist if not (self._htest or self._utest) 99 else pyshell.PyShellFileList(root)) 100 file_open = flist.open 101 pyclbr._modules.clear() 102 103 # create top 104 self.top = top = ListedToplevel(root) 105 top.protocol("WM_DELETE_WINDOW", self.close) 106 top.bind("<Escape>", self.close) 107 if self._htest: # place dialog below parent if running htest 108 top.geometry("+%d+%d" % 109 (root.winfo_rootx(), root.winfo_rooty() + 200)) 110 self.settitle() 111 top.focus_set() 112 113 # create scrolled canvas 114 theme = idleConf.CurrentTheme() 115 background = idleConf.GetHighlight(theme, 'normal')['background'] 116 sc = ScrolledCanvas(top, bg=background, highlightthickness=0, 117 takefocus=1) 118 sc.frame.pack(expand=1, fill="both") 119 item = self.rootnode() 120 self.node = node = TreeNode(sc.canvas, None, item) 121 if not self._utest: 122 node.update() 123 node.expand() 124 125 def settitle(self): 126 "Set the window title." 127 self.top.wm_title("Module Browser - " + os.path.basename(self.path)) 128 self.top.wm_iconname("Module Browser") 129 130 def rootnode(self): 131 "Return a ModuleBrowserTreeItem as the root of the tree." 132 return ModuleBrowserTreeItem(self.path) 133 134 135class ModuleBrowserTreeItem(TreeItem): 136 """Browser tree for Python module. 137 138 Uses TreeItem as the basis for the structure of the tree. 139 Used by both browsers. 140 """ 141 142 def __init__(self, file): 143 """Create a TreeItem for the file. 144 145 Args: 146 file: Full path and module name. 147 """ 148 self.file = file 149 150 def GetText(self): 151 "Return the module name as the text string to display." 152 return os.path.basename(self.file) 153 154 def GetIconName(self): 155 "Return the name of the icon to display." 156 return "python" 157 158 def GetSubList(self): 159 "Return ChildBrowserTreeItems for children." 160 return [ChildBrowserTreeItem(obj) for obj in self.listchildren()] 161 162 def OnDoubleClick(self): 163 "Open a module in an editor window when double clicked." 164 if os.path.normcase(self.file[-3:]) != ".py": 165 return 166 if not os.path.exists(self.file): 167 return 168 file_open(self.file) 169 170 def IsExpandable(self): 171 "Return True if Python (.py) file." 172 return os.path.normcase(self.file[-3:]) == ".py" 173 174 def listchildren(self): 175 "Return sequenced classes and functions in the module." 176 dir, base = os.path.split(self.file) 177 name, ext = os.path.splitext(base) 178 if os.path.normcase(ext) != ".py": 179 return [] 180 try: 181 tree = pyclbr.readmodule_ex(name, [dir] + sys.path) 182 except ImportError: 183 return [] 184 return transform_children(tree, name) 185 186 187class ChildBrowserTreeItem(TreeItem): 188 """Browser tree for child nodes within the module. 189 190 Uses TreeItem as the basis for the structure of the tree. 191 """ 192 193 def __init__(self, obj): 194 "Create a TreeItem for a pyclbr class/function object." 195 self.obj = obj 196 self.name = obj.name 197 self.isfunction = isinstance(obj, pyclbr.Function) 198 199 def GetText(self): 200 "Return the name of the function/class to display." 201 name = self.name 202 if self.isfunction: 203 return "def " + name + "(...)" 204 else: 205 return "class " + name 206 207 def GetIconName(self): 208 "Return the name of the icon to display." 209 if self.isfunction: 210 return "python" 211 else: 212 return "folder" 213 214 def IsExpandable(self): 215 "Return True if self.obj has nested objects." 216 return self.obj.children != {} 217 218 def GetSubList(self): 219 "Return ChildBrowserTreeItems for children." 220 return [ChildBrowserTreeItem(obj) 221 for obj in transform_children(self.obj.children)] 222 223 def OnDoubleClick(self): 224 "Open module with file_open and position to lineno." 225 try: 226 edit = file_open(self.obj.file) 227 edit.gotoline(self.obj.lineno) 228 except (OSError, AttributeError): 229 pass 230 231 232def _module_browser(parent): # htest # 233 if len(sys.argv) > 1: # If pass file on command line. 234 file = sys.argv[1] 235 else: 236 file = __file__ 237 # Add nested objects for htest. 238 class Nested_in_func(TreeNode): 239 def nested_in_class(): pass 240 def closure(): 241 class Nested_in_closure: pass 242 ModuleBrowser(parent, file, _htest=True) 243 244if __name__ == "__main__": 245 if len(sys.argv) == 1: # If pass file on command line, unittest fails. 246 from unittest import main 247 main('idlelib.idle_test.test_browser', verbosity=2, exit=False) 248 from idlelib.idle_test.htest import run 249 run(_module_browser) 250