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) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 8 | - add base classes to class browser tree |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 9 | - finish removing limitation to x.py files (ModuleBrowserTreeItem) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 10 | """ |
| 11 | |
| 12 | import os |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 13 | import pyclbr |
Terry Jan Reedy | bfbaa6b | 2016-08-31 00:50:55 -0400 | [diff] [blame] | 14 | import sys |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 15 | |
Terry Jan Reedy | 6fa5bdc | 2016-05-28 13:22:31 -0400 | [diff] [blame] | 16 | from idlelib.config import idleConf |
Terry Jan Reedy | bfbaa6b | 2016-08-31 00:50:55 -0400 | [diff] [blame] | 17 | from idlelib import pyshell |
| 18 | from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas |
| 19 | from idlelib.windows import ListedToplevel |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 20 | |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 21 | |
Terry Jan Reedy | cd56736 | 2014-10-17 01:31:35 -0400 | [diff] [blame] | 22 | file_open = None # Method...Item and Class...Item use this. |
Terry Jan Reedy | 6fa5bdc | 2016-05-28 13:22:31 -0400 | [diff] [blame] | 23 | # 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] | 24 | |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 25 | |
| 26 | def 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 | |
Cheryl Sabella | cd99e79 | 2017-09-23 16:46:01 -0400 | [diff] [blame] | 57 | class ModuleBrowser: |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 58 | """Browse module classes and functions in IDLE. |
| 59 | """ |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 60 | # This class is also the base class for pathbrowser.PathBrowser. |
luzpaz | a5293b4 | 2017-11-05 07:37:50 -0600 | [diff] [blame] | 61 | # Init and close are inherited, other methods are overridden. |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 62 | # PathBrowser.__init__ does not call __init__ below. |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 63 | |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 64 | def __init__(self, master, path, *, _htest=False, _utest=False): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 65 | """Create a window for browsing a module's structure. |
| 66 | |
| 67 | Args: |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 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. |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 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. |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 81 | """ |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 82 | self.master = master |
| 83 | self.path = path |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 84 | self._htest = _htest |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 85 | self._utest = _utest |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 86 | self.init() |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 87 | |
| 88 | def close(self, event=None): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 89 | "Dismiss the window and the tree nodes." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 90 | self.top.destroy() |
| 91 | self.node.destroy() |
| 92 | |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 93 | def init(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 94 | "Create browser tkinter widgets, including the tree." |
Cheryl Sabella | 20d48a4 | 2017-11-22 19:05:25 -0500 | [diff] [blame] | 95 | global file_open |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 96 | root = self.master |
Cheryl Sabella | 20d48a4 | 2017-11-22 19:05:25 -0500 | [diff] [blame] | 97 | flist = (pyshell.flist if not (self._htest or self._utest) |
| 98 | else pyshell.PyShellFileList(root)) |
| 99 | file_open = flist.open |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 100 | pyclbr._modules.clear() |
Cheryl Sabella | 20d48a4 | 2017-11-22 19:05:25 -0500 | [diff] [blame] | 101 | |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 102 | # create top |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 103 | self.top = top = ListedToplevel(root) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 104 | top.protocol("WM_DELETE_WINDOW", self.close) |
| 105 | top.bind("<Escape>", self.close) |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 106 | if self._htest: # place dialog below parent if running htest |
| 107 | top.geometry("+%d+%d" % |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 108 | (root.winfo_rootx(), root.winfo_rooty() + 200)) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 109 | self.settitle() |
| 110 | top.focus_set() |
Cheryl Sabella | 20d48a4 | 2017-11-22 19:05:25 -0500 | [diff] [blame] | 111 | |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 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'] |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 115 | sc = ScrolledCanvas(top, bg=background, highlightthickness=0, |
| 116 | takefocus=1) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 117 | sc.frame.pack(expand=1, fill="both") |
| 118 | item = self.rootnode() |
| 119 | self.node = node = TreeNode(sc.canvas, None, item) |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 120 | if not self._utest: |
| 121 | node.update() |
| 122 | node.expand() |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 123 | |
| 124 | def settitle(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 125 | "Set the window title." |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 126 | self.top.wm_title("Module Browser - " + os.path.basename(self.path)) |
Cheryl Sabella | cd99e79 | 2017-09-23 16:46:01 -0400 | [diff] [blame] | 127 | self.top.wm_iconname("Module Browser") |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 128 | |
| 129 | def rootnode(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 130 | "Return a ModuleBrowserTreeItem as the root of the tree." |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 131 | return ModuleBrowserTreeItem(self.path) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 132 | |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 133 | |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 134 | class ModuleBrowserTreeItem(TreeItem): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 135 | """Browser tree for Python module. |
| 136 | |
| 137 | Uses TreeItem as the basis for the structure of the tree. |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 138 | Used by both browsers. |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 139 | """ |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 140 | |
| 141 | def __init__(self, file): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 142 | """Create a TreeItem for the file. |
| 143 | |
| 144 | Args: |
| 145 | file: Full path and module name. |
| 146 | """ |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 147 | self.file = file |
| 148 | |
| 149 | def GetText(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 150 | "Return the module name as the text string to display." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 151 | return os.path.basename(self.file) |
| 152 | |
| 153 | def GetIconName(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 154 | "Return the name of the icon to display." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 155 | return "python" |
| 156 | |
| 157 | def GetSubList(self): |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 158 | "Return ChildBrowserTreeItems for children." |
| 159 | return [ChildBrowserTreeItem(obj) for obj in self.listchildren()] |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 160 | |
| 161 | def OnDoubleClick(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 162 | "Open a module in an editor window when double clicked." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 163 | if os.path.normcase(self.file[-3:]) != ".py": |
| 164 | return |
| 165 | if not os.path.exists(self.file): |
| 166 | return |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 167 | file_open(self.file) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 168 | |
| 169 | def IsExpandable(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 170 | "Return True if Python (.py) file." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 171 | return os.path.normcase(self.file[-3:]) == ".py" |
Kurt B. Kaiser | d6c4c9e | 2001-07-12 23:54:20 +0000 | [diff] [blame] | 172 | |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 173 | def listchildren(self): |
| 174 | "Return sequenced classes and functions in the module." |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 175 | dir, base = os.path.split(self.file) |
| 176 | name, ext = os.path.splitext(base) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 177 | if os.path.normcase(ext) != ".py": |
| 178 | return [] |
| 179 | try: |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 180 | tree = pyclbr.readmodule_ex(name, [dir] + sys.path) |
Terry Jan Reedy | 44f09eb | 2014-07-01 18:52:37 -0400 | [diff] [blame] | 181 | except ImportError: |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 182 | return [] |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 183 | return transform_children(tree, name) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 184 | |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 185 | |
| 186 | class ChildBrowserTreeItem(TreeItem): |
| 187 | """Browser tree for child nodes within the module. |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 188 | |
| 189 | Uses TreeItem as the basis for the structure of the tree. |
| 190 | """ |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 191 | |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 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) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 197 | |
| 198 | def GetText(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 199 | "Return the name of the function/class to display." |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 200 | name = self.name |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 201 | if self.isfunction: |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 202 | return "def " + name + "(...)" |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 203 | else: |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 204 | return "class " + name |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 205 | |
| 206 | def GetIconName(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 207 | "Return the name of the icon to display." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 208 | if self.isfunction: |
| 209 | return "python" |
| 210 | else: |
| 211 | return "folder" |
| 212 | |
| 213 | def IsExpandable(self): |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 214 | "Return True if self.obj has nested objects." |
| 215 | return self.obj.children != {} |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 216 | |
| 217 | def GetSubList(self): |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 218 | "Return ChildBrowserTreeItems for children." |
| 219 | return [ChildBrowserTreeItem(obj) |
| 220 | for obj in transform_children(self.obj.children)] |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 221 | |
| 222 | def OnDoubleClick(self): |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 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 |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 229 | |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 230 | |
Cheryl Sabella | cd99e79 | 2017-09-23 16:46:01 -0400 | [diff] [blame] | 231 | def _module_browser(parent): # htest # |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 232 | if len(sys.argv) > 1: # If pass file on command line. |
| 233 | file = sys.argv[1] |
| 234 | else: |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 235 | file = __file__ |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 236 | # Add nested objects for htest. |
Cheryl Sabella | 058de11 | 2017-09-22 16:08:44 -0400 | [diff] [blame] | 237 | class Nested_in_func(TreeNode): |
| 238 | def nested_in_class(): pass |
| 239 | def closure(): |
| 240 | class Nested_in_closure: pass |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 241 | ModuleBrowser(parent, file, _htest=True) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 242 | |
| 243 | if __name__ == "__main__": |
Terry Jan Reedy | d6bb65f | 2017-09-30 19:54:28 -0400 | [diff] [blame] | 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) |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 247 | from idlelib.idle_test.htest import run |
Cheryl Sabella | cd99e79 | 2017-09-23 16:46:01 -0400 | [diff] [blame] | 248 | run(_module_browser) |