Skip to content

Commit 058de11

Browse files
csabellaterryjreedy
authored andcommitted
bpo-1612262: IDLE: Class Browser shows nested functions, classes (#2573)
Original patches for code and tests by Guilherme Polo and Cheryl Sabella, respectively.
1 parent 0a1ff24 commit 058de11

File tree

4 files changed

+329
-157
lines changed

4 files changed

+329
-157
lines changed

Lib/idlelib/browser.py

Lines changed: 82 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,49 @@
1919
from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas
2020
from idlelib.windows import ListedToplevel
2121

22+
2223
file_open = None # Method...Item and Class...Item use this.
2324
# Normally pyshell.flist.open, but there is no pyshell.flist for htest.
2425

26+
27+
def 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+
2558
class ClassBrowser:
2659
"""Browse module classes and functions in IDLE.
2760
"""
61+
# This class is the base class for pathbrowser.PathBrowser.
62+
# Init and close are inherited, other methods are overriden.
2863

29-
def __init__(self, flist, name, path, _htest=False):
64+
def __init__(self, flist, name, path, _htest=False, _utest=False):
3065
# XXX This API should change, if the file doesn't end in ".py"
3166
# XXX the code here is bogus!
3267
"""Create a window for browsing a module's structure.
@@ -47,11 +82,12 @@ def __init__(self, flist, name, path, _htest=False):
4782
the tree and subsequently in the children.
4883
"""
4984
global file_open
50-
if not _htest:
85+
if not (_htest or _utest):
5186
file_open = pyshell.flist.open
5287
self.name = name
5388
self.file = os.path.join(path[0], self.name + ".py")
5489
self._htest = _htest
90+
self._utest = _utest
5591
self.init(flist)
5692

5793
def close(self, event=None):
@@ -80,8 +116,9 @@ def init(self, flist):
80116
sc.frame.pack(expand=1, fill="both")
81117
item = self.rootnode()
82118
self.node = node = TreeNode(sc.canvas, None, item)
83-
node.update()
84-
node.expand()
119+
if not self._utest:
120+
node.update()
121+
node.expand()
85122

86123
def settitle(self):
87124
"Set the window title."
@@ -92,6 +129,7 @@ def rootnode(self):
92129
"Return a ModuleBrowserTreeItem as the root of the tree."
93130
return ModuleBrowserTreeItem(self.file)
94131

132+
95133
class ModuleBrowserTreeItem(TreeItem):
96134
"""Browser tree for Python module.
97135
@@ -115,106 +153,53 @@ def GetIconName(self):
115153
return "python"
116154

117155
def GetSubList(self):
118-
"""Return the list of ClassBrowserTreeItem items.
119-
120-
Each item returned from listclasses is the first level of
121-
classes/functions within the module.
122-
"""
123-
sublist = []
124-
for name in self.listclasses():
125-
item = ClassBrowserTreeItem(name, self.classes, self.file)
126-
sublist.append(item)
127-
return sublist
156+
"Return ChildBrowserTreeItems for children."
157+
return [ChildBrowserTreeItem(obj) for obj in self.listchildren()]
128158

129159
def OnDoubleClick(self):
130160
"Open a module in an editor window when double clicked."
131161
if os.path.normcase(self.file[-3:]) != ".py":
132162
return
133163
if not os.path.exists(self.file):
134164
return
135-
pyshell.flist.open(self.file)
165+
file_open(self.file)
136166

137167
def IsExpandable(self):
138168
"Return True if Python (.py) file."
139169
return os.path.normcase(self.file[-3:]) == ".py"
140170

141-
def listclasses(self):
142-
"""Return list of classes and functions in the module.
143-
144-
The dictionary output from pyclbr is re-written as a
145-
list of tuples in the form (lineno, name) and
146-
then sorted so that the classes and functions are
147-
processed in line number order. The returned list only
148-
contains the name and not the line number. An instance
149-
variable self.classes contains the pyclbr dictionary values,
150-
which are instances of Class and Function.
151-
"""
171+
def listchildren(self):
172+
"Return sequenced classes and functions in the module."
152173
dir, file = os.path.split(self.file)
153174
name, ext = os.path.splitext(file)
154175
if os.path.normcase(ext) != ".py":
155176
return []
156177
try:
157-
dict = pyclbr.readmodule_ex(name, [dir] + sys.path)
178+
tree = pyclbr.readmodule_ex(name, [dir] + sys.path)
158179
except ImportError:
159180
return []
160-
items = []
161-
self.classes = {}
162-
for key, cl in dict.items():
163-
if cl.module == name:
164-
s = key
165-
if hasattr(cl, 'super') and cl.super:
166-
supers = []
167-
for sup in cl.super:
168-
if type(sup) is type(''):
169-
sname = sup
170-
else:
171-
sname = sup.name
172-
if sup.module != cl.module:
173-
sname = "%s.%s" % (sup.module, sname)
174-
supers.append(sname)
175-
s = s + "(%s)" % ", ".join(supers)
176-
items.append((cl.lineno, s))
177-
self.classes[s] = cl
178-
items.sort()
179-
list = []
180-
for item, s in items:
181-
list.append(s)
182-
return list
183-
184-
class ClassBrowserTreeItem(TreeItem):
185-
"""Browser tree for classes within a module.
181+
return transform_children(tree, name)
186182

187-
Uses TreeItem as the basis for the structure of the tree.
188-
"""
189183

190-
def __init__(self, name, classes, file):
191-
"""Create a TreeItem for the class/function.
184+
class ChildBrowserTreeItem(TreeItem):
185+
"""Browser tree for child nodes within the module.
192186
193-
Args:
194-
name: Name of the class/function.
195-
classes: Dictonary of Class/Function instances from pyclbr.
196-
file: Full path and module name.
187+
Uses TreeItem as the basis for the structure of the tree.
188+
"""
197189

198-
Instance variables:
199-
self.cl: Class/Function instance for the class/function name.
200-
self.isfunction: True if self.cl is a Function.
201-
"""
202-
self.name = name
203-
# XXX - Does classes need to be an instance variable?
204-
self.classes = classes
205-
self.file = file
206-
try:
207-
self.cl = self.classes[self.name]
208-
except (IndexError, KeyError):
209-
self.cl = None
210-
self.isfunction = isinstance(self.cl, pyclbr.Function)
190+
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)
211195

212196
def GetText(self):
213197
"Return the name of the function/class to display."
198+
name = self.name
214199
if self.isfunction:
215-
return "def " + self.name + "(...)"
200+
return "def " + name + "(...)"
216201
else:
217-
return "class " + self.name
202+
return "class " + name
218203

219204
def GetIconName(self):
220205
"Return the name of the icon to display."
@@ -224,95 +209,34 @@ def GetIconName(self):
224209
return "folder"
225210

226211
def IsExpandable(self):
227-
"Return True if this class has methods."
228-
if self.cl:
229-
try:
230-
return not not self.cl.methods
231-
except AttributeError:
232-
return False
233-
return None
212+
"Return True if self.obj has nested objects."
213+
return self.obj.children != {}
234214

235215
def GetSubList(self):
236-
"""Return Class methods as a list of MethodBrowserTreeItem items.
237-
238-
Each item is a method within the class.
239-
"""
240-
if not self.cl:
241-
return []
242-
sublist = []
243-
for name in self.listmethods():
244-
item = MethodBrowserTreeItem(name, self.cl, self.file)
245-
sublist.append(item)
246-
return sublist
216+
"Return ChildBrowserTreeItems for children."
217+
return [ChildBrowserTreeItem(obj)
218+
for obj in transform_children(self.obj.children)]
247219

248220
def OnDoubleClick(self):
249-
"Open module with file_open and position to lineno, if it exists."
250-
if not os.path.exists(self.file):
251-
return
252-
edit = file_open(self.file)
253-
if hasattr(self.cl, 'lineno'):
254-
lineno = self.cl.lineno
255-
edit.gotoline(lineno)
256-
257-
def listmethods(self):
258-
"Return list of methods within a class sorted by lineno."
259-
if not self.cl:
260-
return []
261-
items = []
262-
for name, lineno in self.cl.methods.items():
263-
items.append((lineno, name))
264-
items.sort()
265-
list = []
266-
for item, name in items:
267-
list.append(name)
268-
return list
269-
270-
class MethodBrowserTreeItem(TreeItem):
271-
"""Browser tree for methods within a class.
272-
273-
Uses TreeItem as the basis for the structure of the tree.
274-
"""
275-
276-
def __init__(self, name, cl, file):
277-
"""Create a TreeItem for the methods.
278-
279-
Args:
280-
name: Name of the class/function.
281-
cl: pyclbr.Class instance for name.
282-
file: Full path and module name.
283-
"""
284-
self.name = name
285-
self.cl = cl
286-
self.file = file
287-
288-
def GetText(self):
289-
"Return the method name to display."
290-
return "def " + self.name + "(...)"
291-
292-
def GetIconName(self):
293-
"Return the name of the icon to display."
294-
return "python"
295-
296-
def IsExpandable(self):
297-
"Return False as there are no tree items after methods."
298-
return False
221+
"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
299227

300-
def OnDoubleClick(self):
301-
"Open module with file_open and position at the method start."
302-
if not os.path.exists(self.file):
303-
return
304-
edit = file_open(self.file)
305-
edit.gotoline(self.cl.methods[self.name])
306228

307229
def _class_browser(parent): # htest #
308230
try:
231+
file = sys.argv[1] # If pass file on command line
232+
# If this succeeds, unittest will fail.
233+
except IndexError:
309234
file = __file__
310-
except NameError:
311-
file = sys.argv[0]
312-
if sys.argv[1:]:
313-
file = sys.argv[1]
314-
else:
315-
file = sys.argv[0]
235+
# 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
316240
dir, file = os.path.split(file)
317241
name = os.path.splitext(file)[0]
318242
flist = pyshell.PyShellFileList(parent)
@@ -321,5 +245,7 @@ def _class_browser(parent): # htest #
321245
ClassBrowser(flist, name, [dir], _htest=True)
322246

323247
if __name__ == "__main__":
248+
from unittest import main
249+
main('idlelib.idle_test.test_browser', verbosity=2, exit=False)
324250
from idlelib.idle_test.htest import run
325251
run(_class_browser)

0 commit comments

Comments
 (0)