blob: 447dafcc515e6c591fb3dfd7da1f2f8d0efbc7f8 [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)
David Scherer7aced172000-08-15 01:13:23 +00008- add base classes to class browser tree
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -04009- finish removing limitation to x.py files (ModuleBrowserTreeItem)
David Scherer7aced172000-08-15 01:13:23 +000010"""
11
12import os
David Scherer7aced172000-08-15 01:13:23 +000013import pyclbr
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040014import sys
David Scherer7aced172000-08-15 01:13:23 +000015
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040016from idlelib.config import idleConf
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040017from idlelib import pyshell
18from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas
19from idlelib.windows import ListedToplevel
David Scherer7aced172000-08-15 01:13:23 +000020
Cheryl Sabella058de112017-09-22 16:08:44 -040021
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
Cheryl Sabella058de112017-09-22 16:08:44 -040025
26def transform_children(child_dict, modname=None):
27 """Transform a child dictionary to an ordered sequence of objects.
28
29 The dictionary maps names to pyclbr information objects.
30 Filter out imported objects.
31 Augment class names with bases.
32 Sort objects by line number.
33
34 The current tree only calls this once per child_dic as it saves
35 TreeItems once created. A future tree and tests might violate this,
36 so a check prevents multiple in-place augmentations.
37 """
38 obs = [] # Use list since values should already be sorted.
39 for key, obj in child_dict.items():
40 if modname is None or obj.module == modname:
41 if hasattr(obj, 'super') and obj.super and obj.name == key:
42 # If obj.name != key, it has already been suffixed.
43 supers = []
44 for sup in obj.super:
45 if type(sup) is type(''):
46 sname = sup
47 else:
48 sname = sup.name
49 if sup.module != obj.module:
50 sname = f'{sup.module}.{sname}'
51 supers.append(sname)
52 obj.name += '({})'.format(', '.join(supers))
53 obs.append(obj)
54 return sorted(obs, key=lambda o: o.lineno)
55
56
Cheryl Sabellacd99e792017-09-23 16:46:01 -040057class ModuleBrowser:
csabellaba352272017-07-11 02:34:01 -040058 """Browse module classes and functions in IDLE.
59 """
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -040060 # This class is also the base class for pathbrowser.PathBrowser.
luzpaza5293b42017-11-05 07:37:50 -060061 # Init and close are inherited, other methods are overridden.
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -040062 # PathBrowser.__init__ does not call __init__ below.
David Scherer7aced172000-08-15 01:13:23 +000063
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -040064 def __init__(self, master, path, *, _htest=False, _utest=False):
csabellaba352272017-07-11 02:34:01 -040065 """Create a window for browsing a module's structure.
66
67 Args:
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -040068 master: parent for widgets.
69 path: full path of file to browse.
70 _htest - bool; change box location when running htest.
71 -utest - bool; suppress contents when running unittest.
csabellaba352272017-07-11 02:34:01 -040072
73 Global variables:
74 file_open: Function used for opening a file.
75
76 Instance variables:
77 name: Module name.
78 file: Full path and module with .py extension. Used in
79 creating ModuleBrowserTreeItem as the rootnode for
80 the tree and subsequently in the children.
Terry Jan Reedy1b392ff2014-05-24 18:48:18 -040081 """
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -040082 self.master = master
83 self.path = path
Terry Jan Reedy1b392ff2014-05-24 18:48:18 -040084 self._htest = _htest
Cheryl Sabella058de112017-09-22 16:08:44 -040085 self._utest = _utest
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -040086 self.init()
David Scherer7aced172000-08-15 01:13:23 +000087
88 def close(self, event=None):
csabellaba352272017-07-11 02:34:01 -040089 "Dismiss the window and the tree nodes."
David Scherer7aced172000-08-15 01:13:23 +000090 self.top.destroy()
91 self.node.destroy()
92
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -040093 def init(self):
csabellaba352272017-07-11 02:34:01 -040094 "Create browser tkinter widgets, including the tree."
Cheryl Sabella20d48a42017-11-22 19:05:25 -050095 global file_open
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -040096 root = self.master
Cheryl Sabella20d48a42017-11-22 19:05:25 -050097 flist = (pyshell.flist if not (self._htest or self._utest)
98 else pyshell.PyShellFileList(root))
99 file_open = flist.open
David Scherer7aced172000-08-15 01:13:23 +0000100 pyclbr._modules.clear()
Cheryl Sabella20d48a42017-11-22 19:05:25 -0500101
David Scherer7aced172000-08-15 01:13:23 +0000102 # create top
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400103 self.top = top = ListedToplevel(root)
David Scherer7aced172000-08-15 01:13:23 +0000104 top.protocol("WM_DELETE_WINDOW", self.close)
105 top.bind("<Escape>", self.close)
Terry Jan Reedy1b392ff2014-05-24 18:48:18 -0400106 if self._htest: # place dialog below parent if running htest
107 top.geometry("+%d+%d" %
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400108 (root.winfo_rootx(), root.winfo_rooty() + 200))
David Scherer7aced172000-08-15 01:13:23 +0000109 self.settitle()
110 top.focus_set()
Cheryl Sabella20d48a42017-11-22 19:05:25 -0500111
David Scherer7aced172000-08-15 01:13:23 +0000112 # 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']
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400115 sc = ScrolledCanvas(top, bg=background, highlightthickness=0,
116 takefocus=1)
David Scherer7aced172000-08-15 01:13:23 +0000117 sc.frame.pack(expand=1, fill="both")
118 item = self.rootnode()
119 self.node = node = TreeNode(sc.canvas, None, item)
Cheryl Sabella058de112017-09-22 16:08:44 -0400120 if not self._utest:
121 node.update()
122 node.expand()
David Scherer7aced172000-08-15 01:13:23 +0000123
124 def settitle(self):
csabellaba352272017-07-11 02:34:01 -0400125 "Set the window title."
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400126 self.top.wm_title("Module Browser - " + os.path.basename(self.path))
Cheryl Sabellacd99e792017-09-23 16:46:01 -0400127 self.top.wm_iconname("Module Browser")
David Scherer7aced172000-08-15 01:13:23 +0000128
129 def rootnode(self):
csabellaba352272017-07-11 02:34:01 -0400130 "Return a ModuleBrowserTreeItem as the root of the tree."
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400131 return ModuleBrowserTreeItem(self.path)
David Scherer7aced172000-08-15 01:13:23 +0000132
Cheryl Sabella058de112017-09-22 16:08:44 -0400133
David Scherer7aced172000-08-15 01:13:23 +0000134class ModuleBrowserTreeItem(TreeItem):
csabellaba352272017-07-11 02:34:01 -0400135 """Browser tree for Python module.
136
137 Uses TreeItem as the basis for the structure of the tree.
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400138 Used by both browsers.
csabellaba352272017-07-11 02:34:01 -0400139 """
David Scherer7aced172000-08-15 01:13:23 +0000140
141 def __init__(self, file):
csabellaba352272017-07-11 02:34:01 -0400142 """Create a TreeItem for the file.
143
144 Args:
145 file: Full path and module name.
146 """
David Scherer7aced172000-08-15 01:13:23 +0000147 self.file = file
148
149 def GetText(self):
csabellaba352272017-07-11 02:34:01 -0400150 "Return the module name as the text string to display."
David Scherer7aced172000-08-15 01:13:23 +0000151 return os.path.basename(self.file)
152
153 def GetIconName(self):
csabellaba352272017-07-11 02:34:01 -0400154 "Return the name of the icon to display."
David Scherer7aced172000-08-15 01:13:23 +0000155 return "python"
156
157 def GetSubList(self):
Cheryl Sabella058de112017-09-22 16:08:44 -0400158 "Return ChildBrowserTreeItems for children."
159 return [ChildBrowserTreeItem(obj) for obj in self.listchildren()]
David Scherer7aced172000-08-15 01:13:23 +0000160
161 def OnDoubleClick(self):
csabellaba352272017-07-11 02:34:01 -0400162 "Open a module in an editor window when double clicked."
David Scherer7aced172000-08-15 01:13:23 +0000163 if os.path.normcase(self.file[-3:]) != ".py":
164 return
165 if not os.path.exists(self.file):
166 return
Cheryl Sabella058de112017-09-22 16:08:44 -0400167 file_open(self.file)
David Scherer7aced172000-08-15 01:13:23 +0000168
169 def IsExpandable(self):
csabellaba352272017-07-11 02:34:01 -0400170 "Return True if Python (.py) file."
David Scherer7aced172000-08-15 01:13:23 +0000171 return os.path.normcase(self.file[-3:]) == ".py"
Kurt B. Kaiserd6c4c9e2001-07-12 23:54:20 +0000172
Cheryl Sabella058de112017-09-22 16:08:44 -0400173 def listchildren(self):
174 "Return sequenced classes and functions in the module."
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400175 dir, base = os.path.split(self.file)
176 name, ext = os.path.splitext(base)
David Scherer7aced172000-08-15 01:13:23 +0000177 if os.path.normcase(ext) != ".py":
178 return []
179 try:
Cheryl Sabella058de112017-09-22 16:08:44 -0400180 tree = pyclbr.readmodule_ex(name, [dir] + sys.path)
Terry Jan Reedy44f09eb2014-07-01 18:52:37 -0400181 except ImportError:
David Scherer7aced172000-08-15 01:13:23 +0000182 return []
Cheryl Sabella058de112017-09-22 16:08:44 -0400183 return transform_children(tree, name)
David Scherer7aced172000-08-15 01:13:23 +0000184
Cheryl Sabella058de112017-09-22 16:08:44 -0400185
186class ChildBrowserTreeItem(TreeItem):
187 """Browser tree for child nodes within the module.
csabellaba352272017-07-11 02:34:01 -0400188
189 Uses TreeItem as the basis for the structure of the tree.
190 """
David Scherer7aced172000-08-15 01:13:23 +0000191
Cheryl Sabella058de112017-09-22 16:08:44 -0400192 def __init__(self, obj):
193 "Create a TreeItem for a pyclbr class/function object."
194 self.obj = obj
195 self.name = obj.name
196 self.isfunction = isinstance(obj, pyclbr.Function)
David Scherer7aced172000-08-15 01:13:23 +0000197
198 def GetText(self):
csabellaba352272017-07-11 02:34:01 -0400199 "Return the name of the function/class to display."
Cheryl Sabella058de112017-09-22 16:08:44 -0400200 name = self.name
David Scherer7aced172000-08-15 01:13:23 +0000201 if self.isfunction:
Cheryl Sabella058de112017-09-22 16:08:44 -0400202 return "def " + name + "(...)"
David Scherer7aced172000-08-15 01:13:23 +0000203 else:
Cheryl Sabella058de112017-09-22 16:08:44 -0400204 return "class " + name
David Scherer7aced172000-08-15 01:13:23 +0000205
206 def GetIconName(self):
csabellaba352272017-07-11 02:34:01 -0400207 "Return the name of the icon to display."
David Scherer7aced172000-08-15 01:13:23 +0000208 if self.isfunction:
209 return "python"
210 else:
211 return "folder"
212
213 def IsExpandable(self):
Cheryl Sabella058de112017-09-22 16:08:44 -0400214 "Return True if self.obj has nested objects."
215 return self.obj.children != {}
David Scherer7aced172000-08-15 01:13:23 +0000216
217 def GetSubList(self):
Cheryl Sabella058de112017-09-22 16:08:44 -0400218 "Return ChildBrowserTreeItems for children."
219 return [ChildBrowserTreeItem(obj)
220 for obj in transform_children(self.obj.children)]
David Scherer7aced172000-08-15 01:13:23 +0000221
222 def OnDoubleClick(self):
Cheryl Sabella058de112017-09-22 16:08:44 -0400223 "Open module with file_open and position to lineno."
224 try:
225 edit = file_open(self.obj.file)
226 edit.gotoline(self.obj.lineno)
227 except (OSError, AttributeError):
228 pass
David Scherer7aced172000-08-15 01:13:23 +0000229
David Scherer7aced172000-08-15 01:13:23 +0000230
Cheryl Sabellacd99e792017-09-23 16:46:01 -0400231def _module_browser(parent): # htest #
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400232 if len(sys.argv) > 1: # If pass file on command line.
233 file = sys.argv[1]
234 else:
David Scherer7aced172000-08-15 01:13:23 +0000235 file = __file__
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400236 # Add nested objects for htest.
Cheryl Sabella058de112017-09-22 16:08:44 -0400237 class Nested_in_func(TreeNode):
238 def nested_in_class(): pass
239 def closure():
240 class Nested_in_closure: pass
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400241 ModuleBrowser(parent, file, _htest=True)
David Scherer7aced172000-08-15 01:13:23 +0000242
243if __name__ == "__main__":
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400244 if len(sys.argv) == 1: # If pass file on command line, unittest fails.
245 from unittest import main
246 main('idlelib.idle_test.test_browser', verbosity=2, exit=False)
Terry Jan Reedy1b392ff2014-05-24 18:48:18 -0400247 from idlelib.idle_test.htest import run
Cheryl Sabellacd99e792017-09-23 16:46:01 -0400248 run(_module_browser)