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