blob: 4cf4744fb0a8edbc53fa8034e9282277475cac28 [file] [log] [blame]
David Scherer7aced172000-08-15 01:13:23 +00001"""Class 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- 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
13import os
David Scherer7aced172000-08-15 01:13:23 +000014import pyclbr
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040015import sys
David Scherer7aced172000-08-15 01:13:23 +000016
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040017from idlelib.config import idleConf
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040018from idlelib import pyshell
19from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas
20from idlelib.windows import ListedToplevel
David Scherer7aced172000-08-15 01:13:23 +000021
Terry Jan Reedycd567362014-10-17 01:31:35 -040022file_open = None # Method...Item and Class...Item use this.
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040023# Normally pyshell.flist.open, but there is no pyshell.flist for htest.
Terry Jan Reedycd567362014-10-17 01:31:35 -040024
David Scherer7aced172000-08-15 01:13:23 +000025class ClassBrowser:
csabellaba352272017-07-11 02:34:01 -040026 """Browse module classes and functions in IDLE.
27 """
David Scherer7aced172000-08-15 01:13:23 +000028
Terry Jan Reedy1b392ff2014-05-24 18:48:18 -040029 def __init__(self, flist, name, path, _htest=False):
David Scherer7aced172000-08-15 01:13:23 +000030 # XXX This API should change, if the file doesn't end in ".py"
31 # XXX the code here is bogus!
csabellaba352272017-07-11 02:34:01 -040032 """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 Reedy1b392ff2014-05-24 18:48:18 -040048 """
Terry Jan Reedycd567362014-10-17 01:31:35 -040049 global file_open
50 if not _htest:
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040051 file_open = pyshell.flist.open
David Scherer7aced172000-08-15 01:13:23 +000052 self.name = name
53 self.file = os.path.join(path[0], self.name + ".py")
Terry Jan Reedy1b392ff2014-05-24 18:48:18 -040054 self._htest = _htest
David Scherer7aced172000-08-15 01:13:23 +000055 self.init(flist)
56
57 def close(self, event=None):
csabellaba352272017-07-11 02:34:01 -040058 "Dismiss the window and the tree nodes."
David Scherer7aced172000-08-15 01:13:23 +000059 self.top.destroy()
60 self.node.destroy()
61
62 def init(self, flist):
csabellaba352272017-07-11 02:34:01 -040063 "Create browser tkinter widgets, including the tree."
David Scherer7aced172000-08-15 01:13:23 +000064 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 Reedy1b392ff2014-05-24 18:48:18 -040071 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 Scherer7aced172000-08-15 01:13:23 +000074 self.settitle()
75 top.focus_set()
76 # create scrolled canvas
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -050077 theme = idleConf.CurrentTheme()
Kurt B. Kaiser73360a32004-03-08 18:15:31 +000078 background = idleConf.GetHighlight(theme, 'normal')['background']
79 sc = ScrolledCanvas(top, bg=background, highlightthickness=0, takefocus=1)
David Scherer7aced172000-08-15 01:13:23 +000080 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):
csabellaba352272017-07-11 02:34:01 -040087 "Set the window title."
David Scherer7aced172000-08-15 01:13:23 +000088 self.top.wm_title("Class Browser - " + self.name)
89 self.top.wm_iconname("Class Browser")
90
91 def rootnode(self):
csabellaba352272017-07-11 02:34:01 -040092 "Return a ModuleBrowserTreeItem as the root of the tree."
David Scherer7aced172000-08-15 01:13:23 +000093 return ModuleBrowserTreeItem(self.file)
94
95class ModuleBrowserTreeItem(TreeItem):
csabellaba352272017-07-11 02:34:01 -040096 """Browser tree for Python module.
97
98 Uses TreeItem as the basis for the structure of the tree.
99 """
David Scherer7aced172000-08-15 01:13:23 +0000100
101 def __init__(self, file):
csabellaba352272017-07-11 02:34:01 -0400102 """Create a TreeItem for the file.
103
104 Args:
105 file: Full path and module name.
106 """
David Scherer7aced172000-08-15 01:13:23 +0000107 self.file = file
108
109 def GetText(self):
csabellaba352272017-07-11 02:34:01 -0400110 "Return the module name as the text string to display."
David Scherer7aced172000-08-15 01:13:23 +0000111 return os.path.basename(self.file)
112
113 def GetIconName(self):
csabellaba352272017-07-11 02:34:01 -0400114 "Return the name of the icon to display."
David Scherer7aced172000-08-15 01:13:23 +0000115 return "python"
116
117 def GetSubList(self):
csabellaba352272017-07-11 02:34:01 -0400118 """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 Scherer7aced172000-08-15 01:13:23 +0000123 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):
csabellaba352272017-07-11 02:34:01 -0400130 "Open a module in an editor window when double clicked."
David Scherer7aced172000-08-15 01:13:23 +0000131 if os.path.normcase(self.file[-3:]) != ".py":
132 return
133 if not os.path.exists(self.file):
134 return
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400135 pyshell.flist.open(self.file)
David Scherer7aced172000-08-15 01:13:23 +0000136
137 def IsExpandable(self):
csabellaba352272017-07-11 02:34:01 -0400138 "Return True if Python (.py) file."
David Scherer7aced172000-08-15 01:13:23 +0000139 return os.path.normcase(self.file[-3:]) == ".py"
Kurt B. Kaiserd6c4c9e2001-07-12 23:54:20 +0000140
David Scherer7aced172000-08-15 01:13:23 +0000141 def listclasses(self):
csabellaba352272017-07-11 02:34:01 -0400142 """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 Scherer7aced172000-08-15 01:13:23 +0000152 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 Reedy44f09eb2014-07-01 18:52:37 -0400158 except ImportError:
David Scherer7aced172000-08-15 01:13:23 +0000159 return []
160 items = []
161 self.classes = {}
162 for key, cl in dict.items():
163 if cl.module == name:
164 s = key
Raymond Hettinger65500512003-01-19 02:37:41 +0000165 if hasattr(cl, 'super') and cl.super:
David Scherer7aced172000-08-15 01:13:23 +0000166 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. Kaisera2876442002-09-15 22:09:16 +0000175 s = s + "(%s)" % ", ".join(supers)
David Scherer7aced172000-08-15 01:13:23 +0000176 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
184class ClassBrowserTreeItem(TreeItem):
csabellaba352272017-07-11 02:34:01 -0400185 """Browser tree for classes within a module.
186
187 Uses TreeItem as the basis for the structure of the tree.
188 """
David Scherer7aced172000-08-15 01:13:23 +0000189
190 def __init__(self, name, classes, file):
csabellaba352272017-07-11 02:34:01 -0400191 """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 Scherer7aced172000-08-15 01:13:23 +0000202 self.name = name
csabellaba352272017-07-11 02:34:01 -0400203 # XXX - Does classes need to be an instance variable?
David Scherer7aced172000-08-15 01:13:23 +0000204 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):
csabellaba352272017-07-11 02:34:01 -0400213 "Return the name of the function/class to display."
David Scherer7aced172000-08-15 01:13:23 +0000214 if self.isfunction:
215 return "def " + self.name + "(...)"
216 else:
217 return "class " + self.name
218
219 def GetIconName(self):
csabellaba352272017-07-11 02:34:01 -0400220 "Return the name of the icon to display."
David Scherer7aced172000-08-15 01:13:23 +0000221 if self.isfunction:
222 return "python"
223 else:
224 return "folder"
225
226 def IsExpandable(self):
csabellaba352272017-07-11 02:34:01 -0400227 "Return True if this class has methods."
David Scherer7aced172000-08-15 01:13:23 +0000228 if self.cl:
Kurt B. Kaiser0b743442003-01-20 04:49:37 +0000229 try:
230 return not not self.cl.methods
231 except AttributeError:
232 return False
csabellaba352272017-07-11 02:34:01 -0400233 return None
David Scherer7aced172000-08-15 01:13:23 +0000234
235 def GetSubList(self):
csabellaba352272017-07-11 02:34:01 -0400236 """Return Class methods as a list of MethodBrowserTreeItem items.
237
238 Each item is a method within the class.
239 """
David Scherer7aced172000-08-15 01:13:23 +0000240 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):
csabellaba352272017-07-11 02:34:01 -0400249 "Open module with file_open and position to lineno, if it exists."
David Scherer7aced172000-08-15 01:13:23 +0000250 if not os.path.exists(self.file):
251 return
Terry Jan Reedycd567362014-10-17 01:31:35 -0400252 edit = file_open(self.file)
David Scherer7aced172000-08-15 01:13:23 +0000253 if hasattr(self.cl, 'lineno'):
254 lineno = self.cl.lineno
255 edit.gotoline(lineno)
256
257 def listmethods(self):
csabellaba352272017-07-11 02:34:01 -0400258 "Return list of methods within a class sorted by lineno."
David Scherer7aced172000-08-15 01:13:23 +0000259 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
270class MethodBrowserTreeItem(TreeItem):
csabellaba352272017-07-11 02:34:01 -0400271 """Browser tree for methods within a class.
272
273 Uses TreeItem as the basis for the structure of the tree.
274 """
David Scherer7aced172000-08-15 01:13:23 +0000275
276 def __init__(self, name, cl, file):
csabellaba352272017-07-11 02:34:01 -0400277 """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 Scherer7aced172000-08-15 01:13:23 +0000284 self.name = name
285 self.cl = cl
286 self.file = file
287
288 def GetText(self):
csabellaba352272017-07-11 02:34:01 -0400289 "Return the method name to display."
David Scherer7aced172000-08-15 01:13:23 +0000290 return "def " + self.name + "(...)"
291
292 def GetIconName(self):
csabellaba352272017-07-11 02:34:01 -0400293 "Return the name of the icon to display."
294 return "python"
David Scherer7aced172000-08-15 01:13:23 +0000295
296 def IsExpandable(self):
csabellaba352272017-07-11 02:34:01 -0400297 "Return False as there are no tree items after methods."
298 return False
David Scherer7aced172000-08-15 01:13:23 +0000299
300 def OnDoubleClick(self):
csabellaba352272017-07-11 02:34:01 -0400301 "Open module with file_open and position at the method start."
David Scherer7aced172000-08-15 01:13:23 +0000302 if not os.path.exists(self.file):
303 return
Terry Jan Reedycd567362014-10-17 01:31:35 -0400304 edit = file_open(self.file)
David Scherer7aced172000-08-15 01:13:23 +0000305 edit.gotoline(self.cl.methods[self.name])
306
terryjreedy20001502017-07-04 22:41:12 -0400307def _class_browser(parent): # htest #
David Scherer7aced172000-08-15 01:13:23 +0000308 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 Reedy6fa5bdc2016-05-28 13:22:31 -0400318 flist = pyshell.PyShellFileList(parent)
Terry Jan Reedycd567362014-10-17 01:31:35 -0400319 global file_open
320 file_open = flist.open
Terry Jan Reedy1b392ff2014-05-24 18:48:18 -0400321 ClassBrowser(flist, name, [dir], _htest=True)
David Scherer7aced172000-08-15 01:13:23 +0000322
323if __name__ == "__main__":
Terry Jan Reedy1b392ff2014-05-24 18:48:18 -0400324 from idlelib.idle_test.htest import run
325 run(_class_browser)