jpayne@69
|
1 """Module browser.
|
jpayne@69
|
2
|
jpayne@69
|
3 XXX TO DO:
|
jpayne@69
|
4
|
jpayne@69
|
5 - reparse when source changed (maybe just a button would be OK?)
|
jpayne@69
|
6 (or recheck on window popup)
|
jpayne@69
|
7 - add popup menu with more options (e.g. doc strings, base classes, imports)
|
jpayne@69
|
8 - add base classes to class browser tree
|
jpayne@69
|
9 - finish removing limitation to x.py files (ModuleBrowserTreeItem)
|
jpayne@69
|
10 """
|
jpayne@69
|
11
|
jpayne@69
|
12 import os
|
jpayne@69
|
13 import pyclbr
|
jpayne@69
|
14 import sys
|
jpayne@69
|
15
|
jpayne@69
|
16 from idlelib.config import idleConf
|
jpayne@69
|
17 from idlelib import pyshell
|
jpayne@69
|
18 from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas
|
jpayne@69
|
19 from idlelib.window import ListedToplevel
|
jpayne@69
|
20
|
jpayne@69
|
21
|
jpayne@69
|
22 file_open = None # Method...Item and Class...Item use this.
|
jpayne@69
|
23 # Normally pyshell.flist.open, but there is no pyshell.flist for htest.
|
jpayne@69
|
24
|
jpayne@69
|
25
|
jpayne@69
|
26 def transform_children(child_dict, modname=None):
|
jpayne@69
|
27 """Transform a child dictionary to an ordered sequence of objects.
|
jpayne@69
|
28
|
jpayne@69
|
29 The dictionary maps names to pyclbr information objects.
|
jpayne@69
|
30 Filter out imported objects.
|
jpayne@69
|
31 Augment class names with bases.
|
jpayne@69
|
32 The insertion order of the dictionary is assumed to have been in line
|
jpayne@69
|
33 number order, so sorting is not necessary.
|
jpayne@69
|
34
|
jpayne@69
|
35 The current tree only calls this once per child_dict as it saves
|
jpayne@69
|
36 TreeItems once created. A future tree and tests might violate this,
|
jpayne@69
|
37 so a check prevents multiple in-place augmentations.
|
jpayne@69
|
38 """
|
jpayne@69
|
39 obs = [] # Use list since values should already be sorted.
|
jpayne@69
|
40 for key, obj in child_dict.items():
|
jpayne@69
|
41 if modname is None or obj.module == modname:
|
jpayne@69
|
42 if hasattr(obj, 'super') and obj.super and obj.name == key:
|
jpayne@69
|
43 # If obj.name != key, it has already been suffixed.
|
jpayne@69
|
44 supers = []
|
jpayne@69
|
45 for sup in obj.super:
|
jpayne@69
|
46 if type(sup) is type(''):
|
jpayne@69
|
47 sname = sup
|
jpayne@69
|
48 else:
|
jpayne@69
|
49 sname = sup.name
|
jpayne@69
|
50 if sup.module != obj.module:
|
jpayne@69
|
51 sname = f'{sup.module}.{sname}'
|
jpayne@69
|
52 supers.append(sname)
|
jpayne@69
|
53 obj.name += '({})'.format(', '.join(supers))
|
jpayne@69
|
54 obs.append(obj)
|
jpayne@69
|
55 return obs
|
jpayne@69
|
56
|
jpayne@69
|
57
|
jpayne@69
|
58 class ModuleBrowser:
|
jpayne@69
|
59 """Browse module classes and functions in IDLE.
|
jpayne@69
|
60 """
|
jpayne@69
|
61 # This class is also the base class for pathbrowser.PathBrowser.
|
jpayne@69
|
62 # Init and close are inherited, other methods are overridden.
|
jpayne@69
|
63 # PathBrowser.__init__ does not call __init__ below.
|
jpayne@69
|
64
|
jpayne@69
|
65 def __init__(self, master, path, *, _htest=False, _utest=False):
|
jpayne@69
|
66 """Create a window for browsing a module's structure.
|
jpayne@69
|
67
|
jpayne@69
|
68 Args:
|
jpayne@69
|
69 master: parent for widgets.
|
jpayne@69
|
70 path: full path of file to browse.
|
jpayne@69
|
71 _htest - bool; change box location when running htest.
|
jpayne@69
|
72 -utest - bool; suppress contents when running unittest.
|
jpayne@69
|
73
|
jpayne@69
|
74 Global variables:
|
jpayne@69
|
75 file_open: Function used for opening a file.
|
jpayne@69
|
76
|
jpayne@69
|
77 Instance variables:
|
jpayne@69
|
78 name: Module name.
|
jpayne@69
|
79 file: Full path and module with .py extension. Used in
|
jpayne@69
|
80 creating ModuleBrowserTreeItem as the rootnode for
|
jpayne@69
|
81 the tree and subsequently in the children.
|
jpayne@69
|
82 """
|
jpayne@69
|
83 self.master = master
|
jpayne@69
|
84 self.path = path
|
jpayne@69
|
85 self._htest = _htest
|
jpayne@69
|
86 self._utest = _utest
|
jpayne@69
|
87 self.init()
|
jpayne@69
|
88
|
jpayne@69
|
89 def close(self, event=None):
|
jpayne@69
|
90 "Dismiss the window and the tree nodes."
|
jpayne@69
|
91 self.top.destroy()
|
jpayne@69
|
92 self.node.destroy()
|
jpayne@69
|
93
|
jpayne@69
|
94 def init(self):
|
jpayne@69
|
95 "Create browser tkinter widgets, including the tree."
|
jpayne@69
|
96 global file_open
|
jpayne@69
|
97 root = self.master
|
jpayne@69
|
98 flist = (pyshell.flist if not (self._htest or self._utest)
|
jpayne@69
|
99 else pyshell.PyShellFileList(root))
|
jpayne@69
|
100 file_open = flist.open
|
jpayne@69
|
101 pyclbr._modules.clear()
|
jpayne@69
|
102
|
jpayne@69
|
103 # create top
|
jpayne@69
|
104 self.top = top = ListedToplevel(root)
|
jpayne@69
|
105 top.protocol("WM_DELETE_WINDOW", self.close)
|
jpayne@69
|
106 top.bind("<Escape>", self.close)
|
jpayne@69
|
107 if self._htest: # place dialog below parent if running htest
|
jpayne@69
|
108 top.geometry("+%d+%d" %
|
jpayne@69
|
109 (root.winfo_rootx(), root.winfo_rooty() + 200))
|
jpayne@69
|
110 self.settitle()
|
jpayne@69
|
111 top.focus_set()
|
jpayne@69
|
112
|
jpayne@69
|
113 # create scrolled canvas
|
jpayne@69
|
114 theme = idleConf.CurrentTheme()
|
jpayne@69
|
115 background = idleConf.GetHighlight(theme, 'normal')['background']
|
jpayne@69
|
116 sc = ScrolledCanvas(top, bg=background, highlightthickness=0,
|
jpayne@69
|
117 takefocus=1)
|
jpayne@69
|
118 sc.frame.pack(expand=1, fill="both")
|
jpayne@69
|
119 item = self.rootnode()
|
jpayne@69
|
120 self.node = node = TreeNode(sc.canvas, None, item)
|
jpayne@69
|
121 if not self._utest:
|
jpayne@69
|
122 node.update()
|
jpayne@69
|
123 node.expand()
|
jpayne@69
|
124
|
jpayne@69
|
125 def settitle(self):
|
jpayne@69
|
126 "Set the window title."
|
jpayne@69
|
127 self.top.wm_title("Module Browser - " + os.path.basename(self.path))
|
jpayne@69
|
128 self.top.wm_iconname("Module Browser")
|
jpayne@69
|
129
|
jpayne@69
|
130 def rootnode(self):
|
jpayne@69
|
131 "Return a ModuleBrowserTreeItem as the root of the tree."
|
jpayne@69
|
132 return ModuleBrowserTreeItem(self.path)
|
jpayne@69
|
133
|
jpayne@69
|
134
|
jpayne@69
|
135 class ModuleBrowserTreeItem(TreeItem):
|
jpayne@69
|
136 """Browser tree for Python module.
|
jpayne@69
|
137
|
jpayne@69
|
138 Uses TreeItem as the basis for the structure of the tree.
|
jpayne@69
|
139 Used by both browsers.
|
jpayne@69
|
140 """
|
jpayne@69
|
141
|
jpayne@69
|
142 def __init__(self, file):
|
jpayne@69
|
143 """Create a TreeItem for the file.
|
jpayne@69
|
144
|
jpayne@69
|
145 Args:
|
jpayne@69
|
146 file: Full path and module name.
|
jpayne@69
|
147 """
|
jpayne@69
|
148 self.file = file
|
jpayne@69
|
149
|
jpayne@69
|
150 def GetText(self):
|
jpayne@69
|
151 "Return the module name as the text string to display."
|
jpayne@69
|
152 return os.path.basename(self.file)
|
jpayne@69
|
153
|
jpayne@69
|
154 def GetIconName(self):
|
jpayne@69
|
155 "Return the name of the icon to display."
|
jpayne@69
|
156 return "python"
|
jpayne@69
|
157
|
jpayne@69
|
158 def GetSubList(self):
|
jpayne@69
|
159 "Return ChildBrowserTreeItems for children."
|
jpayne@69
|
160 return [ChildBrowserTreeItem(obj) for obj in self.listchildren()]
|
jpayne@69
|
161
|
jpayne@69
|
162 def OnDoubleClick(self):
|
jpayne@69
|
163 "Open a module in an editor window when double clicked."
|
jpayne@69
|
164 if os.path.normcase(self.file[-3:]) != ".py":
|
jpayne@69
|
165 return
|
jpayne@69
|
166 if not os.path.exists(self.file):
|
jpayne@69
|
167 return
|
jpayne@69
|
168 file_open(self.file)
|
jpayne@69
|
169
|
jpayne@69
|
170 def IsExpandable(self):
|
jpayne@69
|
171 "Return True if Python (.py) file."
|
jpayne@69
|
172 return os.path.normcase(self.file[-3:]) == ".py"
|
jpayne@69
|
173
|
jpayne@69
|
174 def listchildren(self):
|
jpayne@69
|
175 "Return sequenced classes and functions in the module."
|
jpayne@69
|
176 dir, base = os.path.split(self.file)
|
jpayne@69
|
177 name, ext = os.path.splitext(base)
|
jpayne@69
|
178 if os.path.normcase(ext) != ".py":
|
jpayne@69
|
179 return []
|
jpayne@69
|
180 try:
|
jpayne@69
|
181 tree = pyclbr.readmodule_ex(name, [dir] + sys.path)
|
jpayne@69
|
182 except ImportError:
|
jpayne@69
|
183 return []
|
jpayne@69
|
184 return transform_children(tree, name)
|
jpayne@69
|
185
|
jpayne@69
|
186
|
jpayne@69
|
187 class ChildBrowserTreeItem(TreeItem):
|
jpayne@69
|
188 """Browser tree for child nodes within the module.
|
jpayne@69
|
189
|
jpayne@69
|
190 Uses TreeItem as the basis for the structure of the tree.
|
jpayne@69
|
191 """
|
jpayne@69
|
192
|
jpayne@69
|
193 def __init__(self, obj):
|
jpayne@69
|
194 "Create a TreeItem for a pyclbr class/function object."
|
jpayne@69
|
195 self.obj = obj
|
jpayne@69
|
196 self.name = obj.name
|
jpayne@69
|
197 self.isfunction = isinstance(obj, pyclbr.Function)
|
jpayne@69
|
198
|
jpayne@69
|
199 def GetText(self):
|
jpayne@69
|
200 "Return the name of the function/class to display."
|
jpayne@69
|
201 name = self.name
|
jpayne@69
|
202 if self.isfunction:
|
jpayne@69
|
203 return "def " + name + "(...)"
|
jpayne@69
|
204 else:
|
jpayne@69
|
205 return "class " + name
|
jpayne@69
|
206
|
jpayne@69
|
207 def GetIconName(self):
|
jpayne@69
|
208 "Return the name of the icon to display."
|
jpayne@69
|
209 if self.isfunction:
|
jpayne@69
|
210 return "python"
|
jpayne@69
|
211 else:
|
jpayne@69
|
212 return "folder"
|
jpayne@69
|
213
|
jpayne@69
|
214 def IsExpandable(self):
|
jpayne@69
|
215 "Return True if self.obj has nested objects."
|
jpayne@69
|
216 return self.obj.children != {}
|
jpayne@69
|
217
|
jpayne@69
|
218 def GetSubList(self):
|
jpayne@69
|
219 "Return ChildBrowserTreeItems for children."
|
jpayne@69
|
220 return [ChildBrowserTreeItem(obj)
|
jpayne@69
|
221 for obj in transform_children(self.obj.children)]
|
jpayne@69
|
222
|
jpayne@69
|
223 def OnDoubleClick(self):
|
jpayne@69
|
224 "Open module with file_open and position to lineno."
|
jpayne@69
|
225 try:
|
jpayne@69
|
226 edit = file_open(self.obj.file)
|
jpayne@69
|
227 edit.gotoline(self.obj.lineno)
|
jpayne@69
|
228 except (OSError, AttributeError):
|
jpayne@69
|
229 pass
|
jpayne@69
|
230
|
jpayne@69
|
231
|
jpayne@69
|
232 def _module_browser(parent): # htest #
|
jpayne@69
|
233 if len(sys.argv) > 1: # If pass file on command line.
|
jpayne@69
|
234 file = sys.argv[1]
|
jpayne@69
|
235 else:
|
jpayne@69
|
236 file = __file__
|
jpayne@69
|
237 # Add nested objects for htest.
|
jpayne@69
|
238 class Nested_in_func(TreeNode):
|
jpayne@69
|
239 def nested_in_class(): pass
|
jpayne@69
|
240 def closure():
|
jpayne@69
|
241 class Nested_in_closure: pass
|
jpayne@69
|
242 ModuleBrowser(parent, file, _htest=True)
|
jpayne@69
|
243
|
jpayne@69
|
244 if __name__ == "__main__":
|
jpayne@69
|
245 if len(sys.argv) == 1: # If pass file on command line, unittest fails.
|
jpayne@69
|
246 from unittest import main
|
jpayne@69
|
247 main('idlelib.idle_test.test_browser', verbosity=2, exit=False)
|
jpayne@69
|
248 from idlelib.idle_test.htest import run
|
jpayne@69
|
249 run(_module_browser)
|