blob: a185ed584aba68c5e19dface4cafee833b324e0a [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.
Cheryl Sabella058de112017-09-22 16:08:44 -040061 # Init and close are inherited, other methods are overriden.
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 Reedycd567362014-10-17 01:31:35 -040082 global file_open
Cheryl Sabella058de112017-09-22 16:08:44 -040083 if not (_htest or _utest):
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040084 file_open = pyshell.flist.open
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -040085 self.master = master
86 self.path = path
Terry Jan Reedy1b392ff2014-05-24 18:48:18 -040087 self._htest = _htest
Cheryl Sabella058de112017-09-22 16:08:44 -040088 self._utest = _utest
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -040089 self.init()
David Scherer7aced172000-08-15 01:13:23 +000090
91 def close(self, event=None):
csabellaba352272017-07-11 02:34:01 -040092 "Dismiss the window and the tree nodes."
David Scherer7aced172000-08-15 01:13:23 +000093 self.top.destroy()
94 self.node.destroy()
95
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -040096 def init(self):
csabellaba352272017-07-11 02:34:01 -040097 "Create browser tkinter widgets, including the tree."
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -040098 root = self.master
David Scherer7aced172000-08-15 01:13:23 +000099 # reset pyclbr
100 pyclbr._modules.clear()
101 # create top
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400102 self.top = top = ListedToplevel(root)
David Scherer7aced172000-08-15 01:13:23 +0000103 top.protocol("WM_DELETE_WINDOW", self.close)
104 top.bind("<Escape>", self.close)
Terry Jan Reedy1b392ff2014-05-24 18:48:18 -0400105 if self._htest: # place dialog below parent if running htest
106 top.geometry("+%d+%d" %
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400107 (root.winfo_rootx(), root.winfo_rooty() + 200))
David Scherer7aced172000-08-15 01:13:23 +0000108 self.settitle()
109 top.focus_set()
110 # create scrolled canvas
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -0500111 theme = idleConf.CurrentTheme()
Kurt B. Kaiser73360a32004-03-08 18:15:31 +0000112 background = idleConf.GetHighlight(theme, 'normal')['background']
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400113 sc = ScrolledCanvas(top, bg=background, highlightthickness=0,
114 takefocus=1)
David Scherer7aced172000-08-15 01:13:23 +0000115 sc.frame.pack(expand=1, fill="both")
116 item = self.rootnode()
117 self.node = node = TreeNode(sc.canvas, None, item)
Cheryl Sabella058de112017-09-22 16:08:44 -0400118 if not self._utest:
119 node.update()
120 node.expand()
David Scherer7aced172000-08-15 01:13:23 +0000121
122 def settitle(self):
csabellaba352272017-07-11 02:34:01 -0400123 "Set the window title."
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400124 self.top.wm_title("Module Browser - " + os.path.basename(self.path))
Cheryl Sabellacd99e792017-09-23 16:46:01 -0400125 self.top.wm_iconname("Module Browser")
David Scherer7aced172000-08-15 01:13:23 +0000126
127 def rootnode(self):
csabellaba352272017-07-11 02:34:01 -0400128 "Return a ModuleBrowserTreeItem as the root of the tree."
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400129 return ModuleBrowserTreeItem(self.path)
David Scherer7aced172000-08-15 01:13:23 +0000130
Cheryl Sabella058de112017-09-22 16:08:44 -0400131
David Scherer7aced172000-08-15 01:13:23 +0000132class ModuleBrowserTreeItem(TreeItem):
csabellaba352272017-07-11 02:34:01 -0400133 """Browser tree for Python module.
134
135 Uses TreeItem as the basis for the structure of the tree.
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400136 Used by both browsers.
csabellaba352272017-07-11 02:34:01 -0400137 """
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."
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400173 dir, base = os.path.split(self.file)
174 name, ext = os.path.splitext(base)
David Scherer7aced172000-08-15 01:13:23 +0000175 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 #
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400230 if len(sys.argv) > 1: # If pass file on command line.
231 file = sys.argv[1]
232 else:
David Scherer7aced172000-08-15 01:13:23 +0000233 file = __file__
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400234 # Add nested objects for htest.
Cheryl Sabella058de112017-09-22 16:08:44 -0400235 class Nested_in_func(TreeNode):
236 def nested_in_class(): pass
237 def closure():
238 class Nested_in_closure: pass
Terry Jan Reedycd567362014-10-17 01:31:35 -0400239 global file_open
Terry Jan Reedyd6bb65f2017-09-30 19:54:28 -0400240 file_open = pyshell.PyShellFileList(parent).open
241 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)