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