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