David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 1 | """Class browser. |
| 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 | |
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 | |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 25 | class ClassBrowser: |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 26 | """Browse module classes and functions in IDLE. |
| 27 | """ |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 28 | |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 29 | def __init__(self, flist, name, path, _htest=False): |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 30 | # XXX This API should change, if the file doesn't end in ".py" |
| 31 | # XXX the code here is bogus! |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 32 | """Create a window for browsing a module's structure. |
| 33 | |
| 34 | Args: |
| 35 | flist: filelist.FileList instance used as the root for the window. |
| 36 | name: Python module to parse. |
| 37 | path: Module search path. |
| 38 | _htest - bool, change box when location running htest. |
| 39 | |
| 40 | Global variables: |
| 41 | file_open: Function used for opening a file. |
| 42 | |
| 43 | Instance variables: |
| 44 | name: Module name. |
| 45 | file: Full path and module with .py extension. Used in |
| 46 | creating ModuleBrowserTreeItem as the rootnode for |
| 47 | the tree and subsequently in the children. |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 48 | """ |
Terry Jan Reedy | cd56736 | 2014-10-17 01:31:35 -0400 | [diff] [blame] | 49 | global file_open |
| 50 | if not _htest: |
Terry Jan Reedy | 6fa5bdc | 2016-05-28 13:22:31 -0400 | [diff] [blame] | 51 | file_open = pyshell.flist.open |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 52 | self.name = name |
| 53 | self.file = os.path.join(path[0], self.name + ".py") |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 54 | self._htest = _htest |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 55 | self.init(flist) |
| 56 | |
| 57 | def close(self, event=None): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 58 | "Dismiss the window and the tree nodes." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 59 | self.top.destroy() |
| 60 | self.node.destroy() |
| 61 | |
| 62 | def init(self, flist): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 63 | "Create browser tkinter widgets, including the tree." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 64 | self.flist = flist |
| 65 | # reset pyclbr |
| 66 | pyclbr._modules.clear() |
| 67 | # create top |
| 68 | self.top = top = ListedToplevel(flist.root) |
| 69 | top.protocol("WM_DELETE_WINDOW", self.close) |
| 70 | top.bind("<Escape>", self.close) |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 71 | if self._htest: # place dialog below parent if running htest |
| 72 | top.geometry("+%d+%d" % |
| 73 | (flist.root.winfo_rootx(), flist.root.winfo_rooty() + 200)) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 74 | self.settitle() |
| 75 | top.focus_set() |
| 76 | # create scrolled canvas |
Terry Jan Reedy | d0c0f00 | 2015-11-12 15:02:57 -0500 | [diff] [blame] | 77 | theme = idleConf.CurrentTheme() |
Kurt B. Kaiser | 73360a3 | 2004-03-08 18:15:31 +0000 | [diff] [blame] | 78 | background = idleConf.GetHighlight(theme, 'normal')['background'] |
| 79 | sc = ScrolledCanvas(top, bg=background, highlightthickness=0, takefocus=1) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 80 | sc.frame.pack(expand=1, fill="both") |
| 81 | item = self.rootnode() |
| 82 | self.node = node = TreeNode(sc.canvas, None, item) |
| 83 | node.update() |
| 84 | node.expand() |
| 85 | |
| 86 | def settitle(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 87 | "Set the window title." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 88 | self.top.wm_title("Class Browser - " + self.name) |
| 89 | self.top.wm_iconname("Class Browser") |
| 90 | |
| 91 | def rootnode(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 92 | "Return a ModuleBrowserTreeItem as the root of the tree." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 93 | return ModuleBrowserTreeItem(self.file) |
| 94 | |
| 95 | class ModuleBrowserTreeItem(TreeItem): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 96 | """Browser tree for Python module. |
| 97 | |
| 98 | Uses TreeItem as the basis for the structure of the tree. |
| 99 | """ |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 100 | |
| 101 | def __init__(self, file): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 102 | """Create a TreeItem for the file. |
| 103 | |
| 104 | Args: |
| 105 | file: Full path and module name. |
| 106 | """ |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 107 | self.file = file |
| 108 | |
| 109 | def GetText(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 110 | "Return the module name as the text string to display." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 111 | return os.path.basename(self.file) |
| 112 | |
| 113 | def GetIconName(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 114 | "Return the name of the icon to display." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 115 | return "python" |
| 116 | |
| 117 | def GetSubList(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 118 | """Return the list of ClassBrowserTreeItem items. |
| 119 | |
| 120 | Each item returned from listclasses is the first level of |
| 121 | classes/functions within the module. |
| 122 | """ |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 123 | sublist = [] |
| 124 | for name in self.listclasses(): |
| 125 | item = ClassBrowserTreeItem(name, self.classes, self.file) |
| 126 | sublist.append(item) |
| 127 | return sublist |
| 128 | |
| 129 | def OnDoubleClick(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 130 | "Open a module in an editor window when double clicked." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 131 | if os.path.normcase(self.file[-3:]) != ".py": |
| 132 | return |
| 133 | if not os.path.exists(self.file): |
| 134 | return |
Terry Jan Reedy | 6fa5bdc | 2016-05-28 13:22:31 -0400 | [diff] [blame] | 135 | pyshell.flist.open(self.file) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 136 | |
| 137 | def IsExpandable(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 138 | "Return True if Python (.py) file." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 139 | return os.path.normcase(self.file[-3:]) == ".py" |
Kurt B. Kaiser | d6c4c9e | 2001-07-12 23:54:20 +0000 | [diff] [blame] | 140 | |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 141 | def listclasses(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 142 | """Return list of classes and functions in the module. |
| 143 | |
| 144 | The dictionary output from pyclbr is re-written as a |
| 145 | list of tuples in the form (lineno, name) and |
| 146 | then sorted so that the classes and functions are |
| 147 | processed in line number order. The returned list only |
| 148 | contains the name and not the line number. An instance |
| 149 | variable self.classes contains the pyclbr dictionary values, |
| 150 | which are instances of Class and Function. |
| 151 | """ |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 152 | dir, file = os.path.split(self.file) |
| 153 | name, ext = os.path.splitext(file) |
| 154 | if os.path.normcase(ext) != ".py": |
| 155 | return [] |
| 156 | try: |
| 157 | dict = pyclbr.readmodule_ex(name, [dir] + sys.path) |
Terry Jan Reedy | 44f09eb | 2014-07-01 18:52:37 -0400 | [diff] [blame] | 158 | except ImportError: |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 159 | return [] |
| 160 | items = [] |
| 161 | self.classes = {} |
| 162 | for key, cl in dict.items(): |
| 163 | if cl.module == name: |
| 164 | s = key |
Raymond Hettinger | 6550051 | 2003-01-19 02:37:41 +0000 | [diff] [blame] | 165 | if hasattr(cl, 'super') and cl.super: |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 166 | supers = [] |
| 167 | for sup in cl.super: |
| 168 | if type(sup) is type(''): |
| 169 | sname = sup |
| 170 | else: |
| 171 | sname = sup.name |
| 172 | if sup.module != cl.module: |
| 173 | sname = "%s.%s" % (sup.module, sname) |
| 174 | supers.append(sname) |
Kurt B. Kaiser | a287644 | 2002-09-15 22:09:16 +0000 | [diff] [blame] | 175 | s = s + "(%s)" % ", ".join(supers) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 176 | items.append((cl.lineno, s)) |
| 177 | self.classes[s] = cl |
| 178 | items.sort() |
| 179 | list = [] |
| 180 | for item, s in items: |
| 181 | list.append(s) |
| 182 | return list |
| 183 | |
| 184 | class ClassBrowserTreeItem(TreeItem): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 185 | """Browser tree for classes within a module. |
| 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 | |
| 190 | def __init__(self, name, classes, file): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 191 | """Create a TreeItem for the class/function. |
| 192 | |
| 193 | Args: |
| 194 | name: Name of the class/function. |
| 195 | classes: Dictonary of Class/Function instances from pyclbr. |
| 196 | file: Full path and module name. |
| 197 | |
| 198 | Instance variables: |
| 199 | self.cl: Class/Function instance for the class/function name. |
| 200 | self.isfunction: True if self.cl is a Function. |
| 201 | """ |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 202 | self.name = name |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 203 | # XXX - Does classes need to be an instance variable? |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 204 | self.classes = classes |
| 205 | self.file = file |
| 206 | try: |
| 207 | self.cl = self.classes[self.name] |
| 208 | except (IndexError, KeyError): |
| 209 | self.cl = None |
| 210 | self.isfunction = isinstance(self.cl, pyclbr.Function) |
| 211 | |
| 212 | def GetText(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 213 | "Return the name of the function/class to display." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 214 | if self.isfunction: |
| 215 | return "def " + self.name + "(...)" |
| 216 | else: |
| 217 | return "class " + self.name |
| 218 | |
| 219 | def GetIconName(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 220 | "Return the name of the icon to display." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 221 | if self.isfunction: |
| 222 | return "python" |
| 223 | else: |
| 224 | return "folder" |
| 225 | |
| 226 | def IsExpandable(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 227 | "Return True if this class has methods." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 228 | if self.cl: |
Kurt B. Kaiser | 0b74344 | 2003-01-20 04:49:37 +0000 | [diff] [blame] | 229 | try: |
| 230 | return not not self.cl.methods |
| 231 | except AttributeError: |
| 232 | return False |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 233 | return None |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 234 | |
| 235 | def GetSubList(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 236 | """Return Class methods as a list of MethodBrowserTreeItem items. |
| 237 | |
| 238 | Each item is a method within the class. |
| 239 | """ |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 240 | if not self.cl: |
| 241 | return [] |
| 242 | sublist = [] |
| 243 | for name in self.listmethods(): |
| 244 | item = MethodBrowserTreeItem(name, self.cl, self.file) |
| 245 | sublist.append(item) |
| 246 | return sublist |
| 247 | |
| 248 | def OnDoubleClick(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 249 | "Open module with file_open and position to lineno, if it exists." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 250 | if not os.path.exists(self.file): |
| 251 | return |
Terry Jan Reedy | cd56736 | 2014-10-17 01:31:35 -0400 | [diff] [blame] | 252 | edit = file_open(self.file) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 253 | if hasattr(self.cl, 'lineno'): |
| 254 | lineno = self.cl.lineno |
| 255 | edit.gotoline(lineno) |
| 256 | |
| 257 | def listmethods(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 258 | "Return list of methods within a class sorted by lineno." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 259 | if not self.cl: |
| 260 | return [] |
| 261 | items = [] |
| 262 | for name, lineno in self.cl.methods.items(): |
| 263 | items.append((lineno, name)) |
| 264 | items.sort() |
| 265 | list = [] |
| 266 | for item, name in items: |
| 267 | list.append(name) |
| 268 | return list |
| 269 | |
| 270 | class MethodBrowserTreeItem(TreeItem): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 271 | """Browser tree for methods within a class. |
| 272 | |
| 273 | Uses TreeItem as the basis for the structure of the tree. |
| 274 | """ |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 275 | |
| 276 | def __init__(self, name, cl, file): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 277 | """Create a TreeItem for the methods. |
| 278 | |
| 279 | Args: |
| 280 | name: Name of the class/function. |
| 281 | cl: pyclbr.Class instance for name. |
| 282 | file: Full path and module name. |
| 283 | """ |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 284 | self.name = name |
| 285 | self.cl = cl |
| 286 | self.file = file |
| 287 | |
| 288 | def GetText(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 289 | "Return the method name to display." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 290 | return "def " + self.name + "(...)" |
| 291 | |
| 292 | def GetIconName(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 293 | "Return the name of the icon to display." |
| 294 | return "python" |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 295 | |
| 296 | def IsExpandable(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 297 | "Return False as there are no tree items after methods." |
| 298 | return False |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 299 | |
| 300 | def OnDoubleClick(self): |
csabella | ba35227 | 2017-07-11 02:34:01 -0400 | [diff] [blame] | 301 | "Open module with file_open and position at the method start." |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 302 | if not os.path.exists(self.file): |
| 303 | return |
Terry Jan Reedy | cd56736 | 2014-10-17 01:31:35 -0400 | [diff] [blame] | 304 | edit = file_open(self.file) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 305 | edit.gotoline(self.cl.methods[self.name]) |
| 306 | |
terryjreedy | 2000150 | 2017-07-04 22:41:12 -0400 | [diff] [blame] | 307 | def _class_browser(parent): # htest # |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 308 | try: |
| 309 | file = __file__ |
| 310 | except NameError: |
| 311 | file = sys.argv[0] |
| 312 | if sys.argv[1:]: |
| 313 | file = sys.argv[1] |
| 314 | else: |
| 315 | file = sys.argv[0] |
| 316 | dir, file = os.path.split(file) |
| 317 | name = os.path.splitext(file)[0] |
Terry Jan Reedy | 6fa5bdc | 2016-05-28 13:22:31 -0400 | [diff] [blame] | 318 | flist = pyshell.PyShellFileList(parent) |
Terry Jan Reedy | cd56736 | 2014-10-17 01:31:35 -0400 | [diff] [blame] | 319 | global file_open |
| 320 | file_open = flist.open |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 321 | ClassBrowser(flist, name, [dir], _htest=True) |
David Scherer | 7aced17 | 2000-08-15 01:13:23 +0000 | [diff] [blame] | 322 | |
| 323 | if __name__ == "__main__": |
Terry Jan Reedy | 1b392ff | 2014-05-24 18:48:18 -0400 | [diff] [blame] | 324 | from idlelib.idle_test.htest import run |
| 325 | run(_class_browser) |