blob: 603d299518d63ca0a3af41ac0c74b1520cf892d8 [file] [log] [blame]
Cheryl Sabellacd99e792017-09-23 16:46:01 -04001"""Module browser.
David Scherer7aced172000-08-15 01:13:23 +00002
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
Cheryl Sabella058de112017-09-22 16:08:44 -040022
Terry Jan Reedycd567362014-10-17 01:31:35 -040023file_open = None # Method...Item and Class...Item use this.
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040024# Normally pyshell.flist.open, but there is no pyshell.flist for htest.
Terry Jan Reedycd567362014-10-17 01:31:35 -040025
Cheryl Sabella058de112017-09-22 16:08:44 -040026
27def 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 Sabellacd99e792017-09-23 16:46:01 -040058class ModuleBrowser:
csabellaba352272017-07-11 02:34:01 -040059 """Browse module classes and functions in IDLE.
60 """
Cheryl Sabella058de112017-09-22 16:08:44 -040061 # This class is the base class for pathbrowser.PathBrowser.
62 # Init and close are inherited, other methods are overriden.
David Scherer7aced172000-08-15 01:13:23 +000063
Cheryl Sabella058de112017-09-22 16:08:44 -040064 def __init__(self, flist, name, path, _htest=False, _utest=False):
David Scherer7aced172000-08-15 01:13:23 +000065 # XXX This API should change, if the file doesn't end in ".py"
66 # XXX the code here is bogus!
csabellaba352272017-07-11 02:34:01 -040067 """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 Reedy1b392ff2014-05-24 18:48:18 -040083 """
Terry Jan Reedycd567362014-10-17 01:31:35 -040084 global file_open
Cheryl Sabella058de112017-09-22 16:08:44 -040085 if not (_htest or _utest):
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040086 file_open = pyshell.flist.open
David Scherer7aced172000-08-15 01:13:23 +000087 self.name = name
88 self.file = os.path.join(path[0], self.name + ".py")
Terry Jan Reedy1b392ff2014-05-24 18:48:18 -040089 self._htest = _htest
Cheryl Sabella058de112017-09-22 16:08:44 -040090 self._utest = _utest
David Scherer7aced172000-08-15 01:13:23 +000091 self.init(flist)
92
93 def close(self, event=None):
csabellaba352272017-07-11 02:34:01 -040094 "Dismiss the window and the tree nodes."
David Scherer7aced172000-08-15 01:13:23 +000095 self.top.destroy()
96 self.node.destroy()
97
98 def init(self, flist):
csabellaba352272017-07-11 02:34:01 -040099 "Create browser tkinter widgets, including the tree."
David Scherer7aced172000-08-15 01:13:23 +0000100 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 Reedy1b392ff2014-05-24 18:48:18 -0400107 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 Scherer7aced172000-08-15 01:13:23 +0000110 self.settitle()
111 top.focus_set()
112 # create scrolled canvas
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -0500113 theme = idleConf.CurrentTheme()
Kurt B. Kaiser73360a32004-03-08 18:15:31 +0000114 background = idleConf.GetHighlight(theme, 'normal')['background']
115 sc = ScrolledCanvas(top, bg=background, highlightthickness=0, takefocus=1)
David Scherer7aced172000-08-15 01:13:23 +0000116 sc.frame.pack(expand=1, fill="both")
117 item = self.rootnode()
118 self.node = node = TreeNode(sc.canvas, None, item)
Cheryl Sabella058de112017-09-22 16:08:44 -0400119 if not self._utest:
120 node.update()
121 node.expand()
David Scherer7aced172000-08-15 01:13:23 +0000122
123 def settitle(self):
csabellaba352272017-07-11 02:34:01 -0400124 "Set the window title."
Cheryl Sabellacd99e792017-09-23 16:46:01 -0400125 self.top.wm_title("Module Browser - " + self.name)
126 self.top.wm_iconname("Module Browser")
David Scherer7aced172000-08-15 01:13:23 +0000127
128 def rootnode(self):
csabellaba352272017-07-11 02:34:01 -0400129 "Return a ModuleBrowserTreeItem as the root of the tree."
David Scherer7aced172000-08-15 01:13:23 +0000130 return ModuleBrowserTreeItem(self.file)
131
Cheryl Sabella058de112017-09-22 16:08:44 -0400132
David Scherer7aced172000-08-15 01:13:23 +0000133class ModuleBrowserTreeItem(TreeItem):
csabellaba352272017-07-11 02:34:01 -0400134 """Browser tree for Python module.
135
136 Uses TreeItem as the basis for the structure of the tree.
137 """
David Scherer7aced172000-08-15 01:13:23 +0000138
139 def __init__(self, file):
csabellaba352272017-07-11 02:34:01 -0400140 """Create a TreeItem for the file.
141
142 Args:
143 file: Full path and module name.
144 """
David Scherer7aced172000-08-15 01:13:23 +0000145 self.file = file
146
147 def GetText(self):
csabellaba352272017-07-11 02:34:01 -0400148 "Return the module name as the text string to display."
David Scherer7aced172000-08-15 01:13:23 +0000149 return os.path.basename(self.file)
150
151 def GetIconName(self):
csabellaba352272017-07-11 02:34:01 -0400152 "Return the name of the icon to display."
David Scherer7aced172000-08-15 01:13:23 +0000153 return "python"
154
155 def GetSubList(self):
Cheryl Sabella058de112017-09-22 16:08:44 -0400156 "Return ChildBrowserTreeItems for children."
157 return [ChildBrowserTreeItem(obj) for obj in self.listchildren()]
David Scherer7aced172000-08-15 01:13:23 +0000158
159 def OnDoubleClick(self):
csabellaba352272017-07-11 02:34:01 -0400160 "Open a module in an editor window when double clicked."
David Scherer7aced172000-08-15 01:13:23 +0000161 if os.path.normcase(self.file[-3:]) != ".py":
162 return
163 if not os.path.exists(self.file):
164 return
Cheryl Sabella058de112017-09-22 16:08:44 -0400165 file_open(self.file)
David Scherer7aced172000-08-15 01:13:23 +0000166
167 def IsExpandable(self):
csabellaba352272017-07-11 02:34:01 -0400168 "Return True if Python (.py) file."
David Scherer7aced172000-08-15 01:13:23 +0000169 return os.path.normcase(self.file[-3:]) == ".py"
Kurt B. Kaiserd6c4c9e2001-07-12 23:54:20 +0000170
Cheryl Sabella058de112017-09-22 16:08:44 -0400171 def listchildren(self):
172 "Return sequenced classes and functions in the module."
David Scherer7aced172000-08-15 01:13:23 +0000173 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 Sabella058de112017-09-22 16:08:44 -0400178 tree = pyclbr.readmodule_ex(name, [dir] + sys.path)
Terry Jan Reedy44f09eb2014-07-01 18:52:37 -0400179 except ImportError:
David Scherer7aced172000-08-15 01:13:23 +0000180 return []
Cheryl Sabella058de112017-09-22 16:08:44 -0400181 return transform_children(tree, name)
David Scherer7aced172000-08-15 01:13:23 +0000182
Cheryl Sabella058de112017-09-22 16:08:44 -0400183
184class ChildBrowserTreeItem(TreeItem):
185 """Browser tree for child nodes within the module.
csabellaba352272017-07-11 02:34:01 -0400186
187 Uses TreeItem as the basis for the structure of the tree.
188 """
David Scherer7aced172000-08-15 01:13:23 +0000189
Cheryl Sabella058de112017-09-22 16:08:44 -0400190 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 Scherer7aced172000-08-15 01:13:23 +0000195
196 def GetText(self):
csabellaba352272017-07-11 02:34:01 -0400197 "Return the name of the function/class to display."
Cheryl Sabella058de112017-09-22 16:08:44 -0400198 name = self.name
David Scherer7aced172000-08-15 01:13:23 +0000199 if self.isfunction:
Cheryl Sabella058de112017-09-22 16:08:44 -0400200 return "def " + name + "(...)"
David Scherer7aced172000-08-15 01:13:23 +0000201 else:
Cheryl Sabella058de112017-09-22 16:08:44 -0400202 return "class " + name
David Scherer7aced172000-08-15 01:13:23 +0000203
204 def GetIconName(self):
csabellaba352272017-07-11 02:34:01 -0400205 "Return the name of the icon to display."
David Scherer7aced172000-08-15 01:13:23 +0000206 if self.isfunction:
207 return "python"
208 else:
209 return "folder"
210
211 def IsExpandable(self):
Cheryl Sabella058de112017-09-22 16:08:44 -0400212 "Return True if self.obj has nested objects."
213 return self.obj.children != {}
David Scherer7aced172000-08-15 01:13:23 +0000214
215 def GetSubList(self):
Cheryl Sabella058de112017-09-22 16:08:44 -0400216 "Return ChildBrowserTreeItems for children."
217 return [ChildBrowserTreeItem(obj)
218 for obj in transform_children(self.obj.children)]
David Scherer7aced172000-08-15 01:13:23 +0000219
220 def OnDoubleClick(self):
Cheryl Sabella058de112017-09-22 16:08:44 -0400221 "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 Scherer7aced172000-08-15 01:13:23 +0000227
David Scherer7aced172000-08-15 01:13:23 +0000228
Cheryl Sabellacd99e792017-09-23 16:46:01 -0400229def _module_browser(parent): # htest #
David Scherer7aced172000-08-15 01:13:23 +0000230 try:
Cheryl Sabella058de112017-09-22 16:08:44 -0400231 file = sys.argv[1] # If pass file on command line
232 # If this succeeds, unittest will fail.
233 except IndexError:
David Scherer7aced172000-08-15 01:13:23 +0000234 file = __file__
Cheryl Sabella058de112017-09-22 16:08:44 -0400235 # 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 Scherer7aced172000-08-15 01:13:23 +0000240 dir, file = os.path.split(file)
241 name = os.path.splitext(file)[0]
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400242 flist = pyshell.PyShellFileList(parent)
Terry Jan Reedycd567362014-10-17 01:31:35 -0400243 global file_open
244 file_open = flist.open
Cheryl Sabellacd99e792017-09-23 16:46:01 -0400245 ModuleBrowser(flist, name, [dir], _htest=True)
David Scherer7aced172000-08-15 01:13:23 +0000246
247if __name__ == "__main__":
Cheryl Sabella058de112017-09-22 16:08:44 -0400248 from unittest import main
249 main('idlelib.idle_test.test_browser', verbosity=2, exit=False)
Terry Jan Reedy1b392ff2014-05-24 18:48:18 -0400250 from idlelib.idle_test.htest import run
Cheryl Sabellacd99e792017-09-23 16:46:01 -0400251 run(_module_browser)