Cheryl Sabella | cd99e79 | 2017-09-23 16:46:01 -0400 | [diff] [blame] | 1 | """Module browser. |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 2 | |
| 3 | XXX 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 | - show function argument list? (have to do pattern matching on source) |
| 9 | - should the classes and methods lists also be in the module's menu bar? |
| 10 | - add base classes to class browser tree |
| 11 | """ |
| 12 | |
| 13 | import os |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 14 | import pyclbr |
Terry Jan Reedy | bfbaa6b | 2016-08-31 00:50:55 -0400 | [diff] [blame] | 15 | import sys |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 16 | |
Terry Jan Reedy | 6fa5bdc | 2016-05-28 13:22:31 -0400 | [diff] [blame] | 17 | from idlelib.config import idleConf |
Terry Jan Reedy | bfbaa6b | 2016-08-31 00:50:55 -0400 | [diff] [blame] | 18 | from idlelib import pyshell |
| 19 | from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas |
| 20 | from idlelib.windows import ListedToplevel |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 21 | |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 22 | |
Terry Jan Reedy | cd56736 | 2014-10-17 01:31:35 -0400 | [diff] [blame] | 23 | file_open = None # Method...Item and Class...Item use this. |
Terry Jan Reedy | 6fa5bdc | 2016-05-28 13:22:31 -0400 | [diff] [blame] | 24 | # Normally pyshell.flist.open, but there is no pyshell.flist for htest. |
Terry Jan Reedy | cd56736 | 2014-10-17 01:31:35 -0400 | [diff] [blame] | 25 | |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 26 | |
| 27 | def transform_children(child_dict, modname=None): |
| 28 | """Transform a child dictionary to an ordered sequence of objects. |
| 29 | |
| 30 | The dictionary maps names to pyclbr information objects. |
| 31 | Filter out imported objects. |
| 32 | Augment class names with bases. |
| 33 | Sort objects by line number. |
| 34 | |
| 35 | The current tree only calls this once per child_dic 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 sorted(obs, key=lambda o: o.lineno) |
| 56 | |
| 57 | |
Cheryl Sabella | cd99e79 | 2017-09-23 16:46:01 -0400 | [diff] [blame] | 58 | class ModuleBrowser: |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 59 | """Browse module classes and functions in IDLE. |
| 60 | """ |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 61 | # This class is the base class for pathbrowser.PathBrowser. |
| 62 | # Init and close are inherited, other methods are overriden. |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 63 | |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 64 | def __init__(self, flist, name, path, _htest=False, _utest=False): |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 65 | # XXX This API should change, if the file doesn't end in ".py" |
| 66 | # XXX the code here is bogus! |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 67 | """Create a window for browsing a module's structure. |
| 68 | |
| 69 | Args: |
| 70 | flist: filelist.FileList instance used as the root for the window. |
| 71 | name: Python module to parse. |
| 72 | path: Module search path. |
| 73 | _htest - bool, change box when location running htest. |
| 74 | |
| 75 | Global variables: |
| 76 | file_open: Function used for opening a file. |
| 77 | |
| 78 | Instance variables: |
| 79 | name: Module name. |
| 80 | file: Full path and module with .py extension. Used in |
| 81 | creating ModuleBrowserTreeItem as the rootnode for |
| 82 | the tree and subsequently in the children. |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 83 | """ |
Terry Jan Reedy | cd56736 | 2014-10-17 01:31:35 -0400 | [diff] [blame] | 84 | global file_open |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 85 | if not (_htest or _utest): |
Terry Jan Reedy | 6fa5bdc | 2016-05-28 13:22:31 -0400 | [diff] [blame] | 86 | file_open = pyshell.flist.open |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 87 | self.name = name |
| 88 | self.file = os.path.join(path[0], self.name + ".py") |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 89 | self._htest = _htest |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 90 | self._utest = _utest |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 91 | self.init(flist) |
| 92 | |
| 93 | def close(self, event=None): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 94 | "Dismiss the window and the tree nodes." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 95 | self.top.destroy() |
| 96 | self.node.destroy() |
| 97 | |
| 98 | def init(self, flist): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 99 | "Create browser tkinter widgets, including the tree." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 100 | self.flist = flist |
| 101 | # reset pyclbr |
| 102 | pyclbr._modules.clear() |
| 103 | # create top |
| 104 | self.top = top = ListedToplevel(flist.root) |
| 105 | top.protocol("WM_DELETE_WINDOW", self.close) |
| 106 | top.bind("<Escape>", self.close) |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 107 | if self._htest: # place dialog below parent if running htest |
| 108 | top.geometry("+%d+%d" % |
| 109 | (flist.root.winfo_rootx(), flist.root.winfo_rooty() + 200)) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 110 | self.settitle() |
| 111 | top.focus_set() |
| 112 | # create scrolled canvas |
Terry Jan Reedy | d0c0f00 | 2015-11-12 15:02:57 -0500 | [diff] [blame] | 113 | theme = idleConf.CurrentTheme() |
Kurt B. Kaiser | 73360a3 | 2004-03-08 18:15:31 +0000 | [diff] [blame] | 114 | background = idleConf.GetHighlight(theme, 'normal')['background'] |
| 115 | sc = ScrolledCanvas(top, bg=background, highlightthickness=0, takefocus=1) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 116 | sc.frame.pack(expand=1, fill="both") |
| 117 | item = self.rootnode() |
| 118 | self.node = node = TreeNode(sc.canvas, None, item) |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 119 | if not self._utest: |
| 120 | node.update() |
| 121 | node.expand() |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 122 | |
| 123 | def settitle(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 124 | "Set the window title." |
Cheryl Sabella | cd99e79 | 2017-09-23 16:46:01 -0400 | [diff] [blame] | 125 | self.top.wm_title("Module Browser - " + self.name) |
| 126 | self.top.wm_iconname("Module Browser") |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 127 | |
| 128 | def rootnode(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 129 | "Return a ModuleBrowserTreeItem as the root of the tree." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 130 | return ModuleBrowserTreeItem(self.file) |
| 131 | |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 132 | |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 133 | class ModuleBrowserTreeItem(TreeItem): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 134 | """Browser tree for Python module. |
| 135 | |
| 136 | Uses TreeItem as the basis for the structure of the tree. |
| 137 | """ |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 138 | |
| 139 | def __init__(self, file): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 140 | """Create a TreeItem for the file. |
| 141 | |
| 142 | Args: |
| 143 | file: Full path and module name. |
| 144 | """ |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 145 | self.file = file |
| 146 | |
| 147 | def GetText(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 148 | "Return the module name as the text string to display." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 149 | return os.path.basename(self.file) |
| 150 | |
| 151 | def GetIconName(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 152 | "Return the name of the icon to display." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 153 | return "python" |
| 154 | |
| 155 | def GetSubList(self): |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 156 | "Return ChildBrowserTreeItems for children." |
| 157 | return [ChildBrowserTreeItem(obj) for obj in self.listchildren()] |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 158 | |
| 159 | def OnDoubleClick(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 160 | "Open a module in an editor window when double clicked." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 161 | if os.path.normcase(self.file[-3:]) != ".py": |
| 162 | return |
| 163 | if not os.path.exists(self.file): |
| 164 | return |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 165 | file_open(self.file) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 166 | |
| 167 | def IsExpandable(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 168 | "Return True if Python (.py) file." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 169 | return os.path.normcase(self.file[-3:]) == ".py" |
Kurt B. Kaiser | d6c4c9e | 2001-07-12 23:54:20 +0000 | [diff] [blame] | 170 | |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 171 | def listchildren(self): |
| 172 | "Return sequenced classes and functions in the module." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 173 | dir, file = os.path.split(self.file) |
| 174 | name, ext = os.path.splitext(file) |
| 175 | if os.path.normcase(ext) != ".py": |
| 176 | return [] |
| 177 | try: |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 178 | tree = pyclbr.readmodule_ex(name, [dir] + sys.path) |
Terry Jan Reedy | 44f09eb | 2014-07-01 18:52:37 -0400 | [diff] [blame] | 179 | except ImportError: |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 180 | return [] |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 181 | return transform_children(tree, name) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 182 | |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 183 | |
| 184 | class ChildBrowserTreeItem(TreeItem): |
| 185 | """Browser tree for child nodes within the module. |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 186 | |
| 187 | Uses TreeItem as the basis for the structure of the tree. |
| 188 | """ |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 189 | |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 190 | def __init__(self, obj): |
| 191 | "Create a TreeItem for a pyclbr class/function object." |
| 192 | self.obj = obj |
| 193 | self.name = obj.name |
| 194 | self.isfunction = isinstance(obj, pyclbr.Function) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 195 | |
| 196 | def GetText(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 197 | "Return the name of the function/class to display." |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 198 | name = self.name |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 199 | if self.isfunction: |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 200 | return "def " + name + "(...)" |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 201 | else: |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 202 | return "class " + name |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 203 | |
| 204 | def GetIconName(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 205 | "Return the name of the icon to display." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 206 | if self.isfunction: |
| 207 | return "python" |
| 208 | else: |
| 209 | return "folder" |
| 210 | |
| 211 | def IsExpandable(self): |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 212 | "Return True if self.obj has nested objects." |
| 213 | return self.obj.children != {} |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 214 | |
| 215 | def GetSubList(self): |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 216 | "Return ChildBrowserTreeItems for children." |
| 217 | return [ChildBrowserTreeItem(obj) |
| 218 | for obj in transform_children(self.obj.children)] |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 219 | |
| 220 | def OnDoubleClick(self): |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 221 | "Open module with file_open and position to lineno." |
| 222 | try: |
| 223 | edit = file_open(self.obj.file) |
| 224 | edit.gotoline(self.obj.lineno) |
| 225 | except (OSError, AttributeError): |
| 226 | pass |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 227 | |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 228 | |
Cheryl Sabella | cd99e79 | 2017-09-23 16:46:01 -0400 | [diff] [blame] | 229 | def _module_browser(parent): # htest # |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 230 | try: |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 231 | file = sys.argv[1] # If pass file on command line |
| 232 | # If this succeeds, unittest will fail. |
| 233 | except IndexError: |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 234 | file = __file__ |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 235 | # Add objects for htest |
| 236 | class Nested_in_func(TreeNode): |
| 237 | def nested_in_class(): pass |
| 238 | def closure(): |
| 239 | class Nested_in_closure: pass |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 240 | dir, file = os.path.split(file) |
| 241 | name = os.path.splitext(file)[0] |
Terry Jan Reedy | 6fa5bdc | 2016-05-28 13:22:31 -0400 | [diff] [blame] | 242 | flist = pyshell.PyShellFileList(parent) |
Terry Jan Reedy | cd56736 | 2014-10-17 01:31:35 -0400 | [diff] [blame] | 243 | global file_open |
| 244 | file_open = flist.open |
Cheryl Sabella | cd99e79 | 2017-09-23 16:46:01 -0400 | [diff] [blame] | 245 | ModuleBrowser(flist, name, [dir], _htest=True) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 246 | |
| 247 | if __name__ == "__main__": |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 248 | from unittest import main |
| 249 | main('idlelib.idle_test.test_browser', verbosity=2, exit=False) |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 250 | from idlelib.idle_test.htest import run |
Cheryl Sabella | cd99e79 | 2017-09-23 16:46:01 -0400 | [diff] [blame] | 251 | run(_module_browser) |