From a7b6411e0953a4fb5f3b7be8e995e5582374c406 Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Tue, 4 Jul 2017 19:37:13 -0400 Subject: [PATCH 01/20] bpo-1612262: IDLE: Class Browser for nested functions/classes --- Lib/idlelib/browser.py | 119 +++++++++++++++-------------------------- 1 file changed, 44 insertions(+), 75 deletions(-) diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py index 4cf4744fb0a8ed..7efca273d9fe25 100644 --- a/Lib/idlelib/browser.py +++ b/Lib/idlelib/browser.py @@ -19,9 +19,34 @@ from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas from idlelib.windows import ListedToplevel +__all__ = ['ClassBrowser'] + file_open = None # Method...Item and Class...Item use this. # Normally pyshell.flist.open, but there is no pyshell.flist for htest. + +def collect_children(d, name=None): + items = [] + children = {} + for key, cl in d.items(): + if name is None or cl.module == name: + s = key + if hasattr(cl, 'super') and cl.super: + supers = [] + for sup in cl.super: + if type(sup) is type(''): + sname = sup + else: + sname = sup.name + if sup.module != cl.module: + sname = "%s.%s" % (sup.module, sname) + supers.append(sname) + s = s + "(%s)" % ", ".join(supers) + items.append((cl.lineno, s)) + children[s] = cl + return children, items + + class ClassBrowser: """Browse module classes and functions in IDLE. """ @@ -121,8 +146,8 @@ def GetSubList(self): classes/functions within the module. """ sublist = [] - for name in self.listclasses(): - item = ClassBrowserTreeItem(name, self.classes, self.file) + for name in self.listchildren(): + item = ChildBrowserTreeItem(name, self.classes, self.file) sublist.append(item) return sublist @@ -138,7 +163,7 @@ def IsExpandable(self): "Return True if Python (.py) file." return os.path.normcase(self.file[-3:]) == ".py" - def listclasses(self): + def listchildren(self): """Return list of classes and functions in the module. The dictionary output from pyclbr is re-written as a @@ -154,34 +179,15 @@ def listclasses(self): if os.path.normcase(ext) != ".py": return [] try: - dict = pyclbr.readmodule_ex(name, [dir] + sys.path) + tree = pyclbr.readmodule_ex(name, [dir] + sys.path) except ImportError: return [] - items = [] - self.classes = {} - for key, cl in dict.items(): - if cl.module == name: - s = key - if hasattr(cl, 'super') and cl.super: - supers = [] - for sup in cl.super: - if type(sup) is type(''): - sname = sup - else: - sname = sup.name - if sup.module != cl.module: - sname = "%s.%s" % (sup.module, sname) - supers.append(sname) - s = s + "(%s)" % ", ".join(supers) - items.append((cl.lineno, s)) - self.classes[s] = cl + self.classes, items = collect_children(tree, name) items.sort() - list = [] - for item, s in items: - list.append(s) - return list + return [s for item, s in items] -class ClassBrowserTreeItem(TreeItem): + +class ChildBrowserTreeItem(TreeItem): """Browser tree for classes within a module. Uses TreeItem as the basis for the structure of the tree. @@ -227,7 +233,7 @@ def IsExpandable(self): "Return True if this class has methods." if self.cl: try: - return not not self.cl.methods + return not not self.cl.children except AttributeError: return False return None @@ -240,8 +246,9 @@ def GetSubList(self): if not self.cl: return [] sublist = [] - for name in self.listmethods(): - item = MethodBrowserTreeItem(name, self.cl, self.file) + for obj in self.listchildren(): + classes, item_name = obj + item = ChildBrowserTreeItem(item_name, classes, self.file) sublist.append(item) return sublist @@ -254,55 +261,17 @@ def OnDoubleClick(self): lineno = self.cl.lineno edit.gotoline(lineno) - def listmethods(self): + def listchildren(self): "Return list of methods within a class sorted by lineno." if not self.cl: return [] - items = [] - for name, lineno in self.cl.methods.items(): - items.append((lineno, name)) - items.sort() - list = [] - for item, name in items: - list.append(name) - return list - -class MethodBrowserTreeItem(TreeItem): - """Browser tree for methods within a class. - - Uses TreeItem as the basis for the structure of the tree. - """ - - def __init__(self, name, cl, file): - """Create a TreeItem for the methods. - - Args: - name: Name of the class/function. - cl: pyclbr.Class instance for name. - file: Full path and module name. - """ - self.name = name - self.cl = cl - self.file = file - - def GetText(self): - "Return the method name to display." - return "def " + self.name + "(...)" + result = [] + for name, ob in self.cl.children.items(): + classes, items = collect_children({name: ob}) + result.append((ob.lineno, classes, items[0][1])) + result.sort() + return [item[1:] for item in result] - def GetIconName(self): - "Return the name of the icon to display." - return "python" - - def IsExpandable(self): - "Return False as there are no tree items after methods." - return False - - def OnDoubleClick(self): - "Open module with file_open and position at the method start." - if not os.path.exists(self.file): - return - edit = file_open(self.file) - edit.gotoline(self.cl.methods[self.name]) def _class_browser(parent): # htest # try: From 1f4c5479033982cb7ea3486581badadffe550c09 Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Wed, 12 Jul 2017 11:30:07 -0400 Subject: [PATCH 02/20] Initial test for TreeItem structure --- Lib/idlelib/browser.py | 29 +++++--- Lib/idlelib/idle_test/test_browser.py | 98 +++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 Lib/idlelib/idle_test/test_browser.py diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py index 7efca273d9fe25..f2b57c7e88ef29 100644 --- a/Lib/idlelib/browser.py +++ b/Lib/idlelib/browser.py @@ -25,10 +25,17 @@ # Normally pyshell.flist.open, but there is no pyshell.flist for htest. -def collect_children(d, name=None): +def traverse_node(node, name=None): + """Return the immediate children for a node. + + Node is the current node being traversed. The return value is + a tuple with the first value being a dictionary of the + Class/Function instances of the node and the second is + a list of tuples with (lineno, name). + """ items = [] children = {} - for key, cl in d.items(): + for key, cl in node.items(): if name is None or cl.module == name: s = key if hasattr(cl, 'super') and cl.super: @@ -117,6 +124,7 @@ def rootnode(self): "Return a ModuleBrowserTreeItem as the root of the tree." return ModuleBrowserTreeItem(self.file) + class ModuleBrowserTreeItem(TreeItem): """Browser tree for Python module. @@ -140,7 +148,7 @@ def GetIconName(self): return "python" def GetSubList(self): - """Return the list of ClassBrowserTreeItem items. + """Return the list of ChildBrowserTreeItem items. Each item returned from listclasses is the first level of classes/functions within the module. @@ -182,13 +190,13 @@ def listchildren(self): tree = pyclbr.readmodule_ex(name, [dir] + sys.path) except ImportError: return [] - self.classes, items = collect_children(tree, name) + self.classes, items = traverse_node(tree, name) items.sort() return [s for item, s in items] class ChildBrowserTreeItem(TreeItem): - """Browser tree for classes within a module. + """Browser tree for child nodes within the module. Uses TreeItem as the basis for the structure of the tree. """ @@ -239,10 +247,7 @@ def IsExpandable(self): return None def GetSubList(self): - """Return Class methods as a list of MethodBrowserTreeItem items. - - Each item is a method within the class. - """ + "Return recursive list of ChildBrowserTreeItem items." if not self.cl: return [] sublist = [] @@ -262,12 +267,12 @@ def OnDoubleClick(self): edit.gotoline(lineno) def listchildren(self): - "Return list of methods within a class sorted by lineno." + "Return list of nested classes/functions sorted by lineno." if not self.cl: return [] result = [] for name, ob in self.cl.children.items(): - classes, items = collect_children({name: ob}) + classes, items = traverse_node({name: ob}) result.append((ob.lineno, classes, items[0][1])) result.sort() return [item[1:] for item in result] @@ -290,5 +295,7 @@ def _class_browser(parent): # htest # ClassBrowser(flist, name, [dir], _htest=True) if __name__ == "__main__": + from unittest import main + main('idlelib.idle_test.test_browser', verbosity=2, exit=False) from idlelib.idle_test.htest import run run(_class_browser) diff --git a/Lib/idlelib/idle_test/test_browser.py b/Lib/idlelib/idle_test/test_browser.py new file mode 100644 index 00000000000000..44a8796d5102be --- /dev/null +++ b/Lib/idlelib/idle_test/test_browser.py @@ -0,0 +1,98 @@ +""" Test idlelib.browser. +""" + +from idlelib import browser +from test.support import requires +requires('gui') +import unittest +from unittest import mock +from tkinter import Tk +from collections import deque +import pyclbr +from textwrap import dedent + + +class ClassBrowserTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.root = Tk() + cls.root.withdraw() + + @classmethod + def tearDownClass(cls): + cls.root.destroy() + del cls.root + + +class BrowserTreeItemTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + mb = pyclbr + # Set arguments for descriptor creation and _create_tree call. + # Same as test_nested in test_pyclbr with the addition of a super + # for C0. + m, p, cls.f, t, i = 'test', '', 'test.py', {}, None + source = dedent("""\ + def f0: + def f1(a,b,c): + def f2(a=1, b=2, c=3): pass + return f1(a,b,d) + class c1: pass + class C0(base): + "Test class." + def F1(): + "Method." + return 'return' + class C1(): + class C2: + "Class nested within nested class." + def F3(): return 1+1 + """) + mock_pyclbr_tree = mb._create_tree(m, p, cls.f, source, t, i) + + cls.save_readmodule_ex = browser.pyclbr.readmodule_ex + browser.pyclbr.readmodule_ex = mock.Mock(return_value=mock_pyclbr_tree) + cls.mbt = browser.ModuleBrowserTreeItem(cls.f) + cls.sublist = cls.mbt.GetSubList() + + @classmethod + def tearDownClass(cls): + del cls.sublist, cls.mbt + browser.pyclbr.readmodule_ex = cls.save_readmodule_ex + del cls.save_readmodule_ex + + def test_ModuleBrowserTreeItem(self): + self.assertEqual(self.mbt.GetText(), self.f) + self.assertEqual(self.mbt.GetIconName(), 'python') + self.assertTrue(self.mbt.IsExpandable()) + + self.assertEqual('f0', self.sublist[0].name) + self.assertEqual('C0(base)', self.sublist[1].name) + for s in self.sublist: + self.assertIsInstance(s, browser.ChildBrowserTreeItem) + + def test_ChildBrowserTreeItem(self): + queue = deque() + actual_names = [] + # The tree items are processed in breadth first order. + # Verify that processing each sublist hits every node and + # in the right order. + expected_names = ['f0', 'C0(base)', + 'f1', 'c1', 'F1', 'C1()', + 'f2', 'C2', + 'F3'] + queue.extend(self.sublist) + while queue: + cb = queue.popleft() + sublist = cb.GetSubList() + queue.extend(sublist) + self.assertIn(cb.name, cb.GetText()) + self.assertIn(cb.GetIconName(), ('python', 'folder')) + self.assertIs(cb.IsExpandable(), sublist != []) + actual_names.append(cb.name) + self.assertEqual(actual_names, expected_names) + +if __name__ == '__main__': + unittest.main(verbosity=2) From 6763dfcea45ad12c5a061f507fba1a18442f73d7 Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Sat, 5 Aug 2017 16:31:55 -0400 Subject: [PATCH 03/20] More complete unit tests. --- Lib/idlelib/browser.py | 23 ++- Lib/idlelib/idle_test/test_browser.py | 287 ++++++++++++++++++++++---- 2 files changed, 254 insertions(+), 56 deletions(-) diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py index f2b57c7e88ef29..8de12bacf8876d 100644 --- a/Lib/idlelib/browser.py +++ b/Lib/idlelib/browser.py @@ -19,13 +19,12 @@ from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas from idlelib.windows import ListedToplevel -__all__ = ['ClassBrowser'] file_open = None # Method...Item and Class...Item use this. # Normally pyshell.flist.open, but there is no pyshell.flist for htest. -def traverse_node(node, name=None): +def _traverse_node(node, name=None): """Return the immediate children for a node. Node is the current node being traversed. The return value is @@ -46,9 +45,9 @@ def traverse_node(node, name=None): else: sname = sup.name if sup.module != cl.module: - sname = "%s.%s" % (sup.module, sname) + sname = f'{sup.module}.{sname}' supers.append(sname) - s = s + "(%s)" % ", ".join(supers) + s += '({})'.format(', '.join(supers)) items.append((cl.lineno, s)) children[s] = cl return children, items @@ -58,7 +57,7 @@ class ClassBrowser: """Browse module classes and functions in IDLE. """ - def __init__(self, flist, name, path, _htest=False): + def __init__(self, flist, name, path, _htest=False, _utest=False): # XXX This API should change, if the file doesn't end in ".py" # XXX the code here is bogus! """Create a window for browsing a module's structure. @@ -79,11 +78,12 @@ def __init__(self, flist, name, path, _htest=False): the tree and subsequently in the children. """ global file_open - if not _htest: + if not (_htest or _utest): file_open = pyshell.flist.open self.name = name self.file = os.path.join(path[0], self.name + ".py") self._htest = _htest + self._utest = _utest self.init(flist) def close(self, event=None): @@ -112,8 +112,9 @@ def init(self, flist): sc.frame.pack(expand=1, fill="both") item = self.rootnode() self.node = node = TreeNode(sc.canvas, None, item) - node.update() - node.expand() + if not self._utest: + node.update() + node.expand() def settitle(self): "Set the window title." @@ -165,7 +166,7 @@ def OnDoubleClick(self): return if not os.path.exists(self.file): return - pyshell.flist.open(self.file) + file_open(self.file) def IsExpandable(self): "Return True if Python (.py) file." @@ -190,7 +191,7 @@ def listchildren(self): tree = pyclbr.readmodule_ex(name, [dir] + sys.path) except ImportError: return [] - self.classes, items = traverse_node(tree, name) + self.classes, items = _traverse_node(tree, name) items.sort() return [s for item, s in items] @@ -272,7 +273,7 @@ def listchildren(self): return [] result = [] for name, ob in self.cl.children.items(): - classes, items = traverse_node({name: ob}) + classes, items = _traverse_node({name: ob}) result.append((ob.lineno, classes, items[0][1])) result.sort() return [item[1:] for item in result] diff --git a/Lib/idlelib/idle_test/test_browser.py b/Lib/idlelib/idle_test/test_browser.py index 44a8796d5102be..455c3c8869db80 100644 --- a/Lib/idlelib/idle_test/test_browser.py +++ b/Lib/idlelib/idle_test/test_browser.py @@ -1,79 +1,275 @@ """ Test idlelib.browser. """ -from idlelib import browser -from test.support import requires -requires('gui') +import os.path import unittest +import pyclbr + +from idlelib import browser, filelist +from idlelib.tree import TreeNode +from test.support import requires from unittest import mock from tkinter import Tk +from idlelib.idle_test.mock_idle import Func from collections import deque -import pyclbr -from textwrap import dedent class ClassBrowserTest(unittest.TestCase): @classmethod def setUpClass(cls): + requires('gui') cls.root = Tk() cls.root.withdraw() + cls.flist = filelist.FileList(cls.root) + cls.file = __file__ + cls.path = os.path.dirname(cls.file) + cls.module = os.path.basename(cls.file).rstrip('.py') + cls.cb = browser.ClassBrowser(cls.flist, cls.module, [cls.path], _utest=True) @classmethod def tearDownClass(cls): + cls.cb.close() cls.root.destroy() - del cls.root + del cls.root, cls.flist, cls.cb + + def test_init(self): + cb = self.cb + eq = self.assertEqual + eq(cb.name, self.module) + eq(cb.file, self.file) + eq(cb.flist, self.flist) + eq(pyclbr._modules, {}) + self.assertIsInstance(cb.node, TreeNode) + + def test_settitle(self): + cb = self.cb + self.assertIn(self.module, cb.top.title()) + self.assertEqual(cb.top.iconname(), 'Class Browser') + + def test_rootnode(self): + cb = self.cb + rn = cb.rootnode() + self.assertIsInstance(rn, browser.ModuleBrowserTreeItem) + + def test_close(self): + cb = self.cb + cb.top.destroy = Func() + cb.node.destroy = Func() + cb.close() + self.assertTrue(cb.top.destroy.called) + self.assertTrue(cb.node.destroy.called) + del cb.top.destroy, cb.node.destroy + + +# Same nested tree creation as in test_pyclbr.py except for super on C0. +mb = pyclbr +module, fname = 'test', 'test.py' +f0 = mb.Function(module, 'f0', fname, 1) +f1 = mb._nest_function(f0, 'f1', 2) +f2 = mb._nest_function(f1, 'f2', 3) +c1 = mb._nest_class(f0, 'c1', 5) +C0 = mb.Class(module, 'C0', ['base'], fname, 6) +F1 = mb._nest_function(C0, 'F1', 8) +C1 = mb._nest_class(C0, 'C1', 11, ['']) +C2 = mb._nest_class(C1, 'C2', 12) +F3 = mb._nest_function(C2, 'F3', 14) +mock_pyclbr_tree = {'f0': f0, 'C0': C0} + + +class TraverseNodeTest(unittest.TestCase): + + def test__traverse_node(self): + # Nothing to traverse if parameter name isn't same as tree module. + tn = browser._traverse_node(mock_pyclbr_tree, 'different name') + self.assertEqual(tn, ({}, [])) + + # Parameter matches tree module. + tn = browser._traverse_node(mock_pyclbr_tree, 'test') + expected = ({'f0': f0, 'C0(base)': C0}, [(1, 'f0'), (6, 'C0(base)')]) + self.assertEqual(tn, expected) + # No name parameter. + tn = browser._traverse_node({'f1': f1}) + expected = ({'f1': f1}, [(2, 'f1')]) + self.assertEqual(tn, expected) -class BrowserTreeItemTest(unittest.TestCase): + +class ModuleBrowserTreeItemTest(unittest.TestCase): @classmethod def setUpClass(cls): - mb = pyclbr - # Set arguments for descriptor creation and _create_tree call. - # Same as test_nested in test_pyclbr with the addition of a super - # for C0. - m, p, cls.f, t, i = 'test', '', 'test.py', {}, None - source = dedent("""\ - def f0: - def f1(a,b,c): - def f2(a=1, b=2, c=3): pass - return f1(a,b,d) - class c1: pass - class C0(base): - "Test class." - def F1(): - "Method." - return 'return' - class C1(): - class C2: - "Class nested within nested class." - def F3(): return 1+1 - """) - mock_pyclbr_tree = mb._create_tree(m, p, cls.f, source, t, i) - - cls.save_readmodule_ex = browser.pyclbr.readmodule_ex + cls.orig_readmodule_ex = browser.pyclbr.readmodule_ex browser.pyclbr.readmodule_ex = mock.Mock(return_value=mock_pyclbr_tree) - cls.mbt = browser.ModuleBrowserTreeItem(cls.f) - cls.sublist = cls.mbt.GetSubList() + cls.mbt = browser.ModuleBrowserTreeItem(fname) @classmethod def tearDownClass(cls): - del cls.sublist, cls.mbt - browser.pyclbr.readmodule_ex = cls.save_readmodule_ex - del cls.save_readmodule_ex + browser.pyclbr.readmodule_ex = cls.orig_readmodule_ex + del cls.orig_readmodule_ex, cls.mbt + + def test_init(self): + mbt = self.mbt + self.assertEqual(mbt.file, fname) + + def test_gettext(self): + mbt = self.mbt + self.assertEqual(mbt.GetText(), fname) + + def test_geticonname(self): + mbt = self.mbt + self.assertEqual(mbt.GetIconName(), 'python') + + def test_isexpandable(self): + mbt = self.mbt + self.assertTrue(mbt.IsExpandable()) + + def test_listchildren(self): + mbt = self.mbt + expected_names = ['f0', 'C0(base)'] + expected_classes = {'f0': f0, 'C0(base)': C0} + self.assertNotEqual(mbt.classes, expected_classes) + + items = mbt.listchildren() + self.assertEqual(items, expected_names) + # self.classes is set in listchildren. + self.assertEqual(mbt.classes, expected_classes) - def test_ModuleBrowserTreeItem(self): - self.assertEqual(self.mbt.GetText(), self.f) - self.assertEqual(self.mbt.GetIconName(), 'python') - self.assertTrue(self.mbt.IsExpandable()) + def test_getsublist(self): + mbt = self.mbt + expected_names = ['f0', 'C0(base)'] + expected_classes = {'f0': f0, 'C0(base)': C0} + sublist = mbt.GetSubList() + for index, item in enumerate(sublist): + self.assertIsInstance(item, browser.ChildBrowserTreeItem) + self.assertEqual(sublist[index].name, expected_names[index]) + self.assertEqual(sublist[index].classes, expected_classes) + self.assertEqual(sublist[index].file, fname) + mbt.classes.clear() - self.assertEqual('f0', self.sublist[0].name) - self.assertEqual('C0(base)', self.sublist[1].name) - for s in self.sublist: - self.assertIsInstance(s, browser.ChildBrowserTreeItem) + def test_ondoubleclick(self): + mbt = self.mbt + fopen = browser.file_open = mock.Mock() - def test_ChildBrowserTreeItem(self): + with mock.patch('os.path.exists', return_value=False): + mbt.OnDoubleClick() + fopen.assert_not_called() + + with mock.patch('os.path.exists', return_value=True): + mbt.OnDoubleClick() + fopen.assert_called() + fopen.called_with(fname) + + del browser.file_open + + +class ChildBrowserTreeItemTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.cbt_C0 = browser.ChildBrowserTreeItem('C0', mock_pyclbr_tree, fname) + cls.f1_classes = {'f1': f1} + cls.cbt_f1 = browser.ChildBrowserTreeItem('f1', cls.f1_classes, fname) + cls.F1_classes = {'F1': F1} + cls.cbt_F1 = browser.ChildBrowserTreeItem('F1', cls.F1_classes, fname) + cls.cbt_C1 = browser.ChildBrowserTreeItem('F1', {'C1': C1}, fname) + + @classmethod + def tearDownClass(cls): + del cls.cbt_C0, cls.cbt_f1, cls.cbt_F1, cls.cbt_C1 + + def test_init(self): + cbt_C0 = self.cbt_C0 + cbt_f1 = self.cbt_f1 + eq = self.assertEqual + + eq(cbt_C0.name, 'C0') + eq(cbt_C0.classes, mock_pyclbr_tree) + eq(cbt_C0.file, fname) + eq(cbt_C0.cl, mock_pyclbr_tree['C0']) + self.assertFalse(cbt_C0.isfunction) + + eq(cbt_f1.name, 'f1') + eq(cbt_f1.classes, self.f1_classes) + eq(cbt_f1.file, fname) + eq(cbt_f1.cl, self.f1_classes['f1']) + self.assertTrue(cbt_f1.isfunction) + + def test_gettext(self): + self.assertEqual(self.cbt_C0.GetText(), 'class C0') + self.assertEqual(self.cbt_f1.GetText(), 'def f1(...)') + + def test_geticonname(self): + self.assertEqual(self.cbt_C0.GetIconName(), 'folder') + self.assertEqual(self.cbt_f1.GetIconName(), 'python') + + def test_isexpandable(self): + self.assertTrue(self.cbt_C0.IsExpandable()) + self.assertTrue(self.cbt_f1.IsExpandable()) + self.assertFalse(self.cbt_F1.IsExpandable()) + + def test_listchildren(self): + self.assertEqual(self.cbt_C0.listchildren(), [({'F1': F1}, 'F1'), + ({'C1()': C1}, 'C1()')]) + self.assertEqual(self.cbt_f1.listchildren(), [({'f2': f2}, 'f2')]) + self.assertEqual(self.cbt_F1.listchildren(), []) + + def test_getsublist(self): + eq = self.assertEqual + # When GetSubList is called, additional ChildBrowserTreeItem + # instances are created for children of the current node. + # C0 has childen F1 and C1() (test class with children) + C0_expected = [({'F1': F1}, 'F1'), ({'C1()': C1}, 'C1()')] + for i, sublist in enumerate(self.cbt_C0.GetSubList()): + self.assertIsInstance(sublist, browser.ChildBrowserTreeItem) + eq(sublist.name, C0_expected[i][1]) + eq(sublist.classes, C0_expected[i][0]) + eq(sublist.file, fname) + eq(sublist.cl, C0_expected[i][0][sublist.name]) + + # f1 has children f2. (test function with children) + for sublist in self.cbt_f1.GetSubList(): + self.assertIsInstance(sublist, browser.ChildBrowserTreeItem) + eq(sublist.name, 'f2') + eq(sublist.classes, {'f2': f2}) + eq(sublist.file, fname) + eq(sublist.cl, f2) + + # F1 has no children. + eq(self.cbt_F1.GetSubList(), []) + + def test_ondoubleclick(self): + fopen = browser.file_open = mock.Mock() + goto = fopen.return_value.gotoline = mock.Mock() + + with mock.patch('os.path.exists', return_value=False): + self.cbt_C0.OnDoubleClick() + fopen.assert_not_called() + + with mock.patch('os.path.exists', return_value=True): + self.cbt_F1.OnDoubleClick() + fopen.assert_called() + goto.assert_called() + goto.assert_called_with(self.cbt_F1.cl.lineno) + + del browser.file_open + + +class NestedChildrenTest(unittest.TestCase): + "Test that all the nodes in a nested tree are added to the BrowserTree." + + @classmethod + def setUpClass(cls): + cls.orig_readmodule_ex = browser.pyclbr.readmodule_ex + browser.pyclbr.readmodule_ex = mock.Mock(return_value=mock_pyclbr_tree) + cls.sublist = browser.ModuleBrowserTreeItem(fname).GetSubList() + + @classmethod + def tearDownClass(cls): + browser.pyclbr.readmodule_ex = cls.orig_readmodule_ex + del cls.orig_readmodule_ex, cls.sublist + + def test_nested(self): queue = deque() actual_names = [] # The tree items are processed in breadth first order. @@ -94,5 +290,6 @@ def test_ChildBrowserTreeItem(self): actual_names.append(cb.name) self.assertEqual(actual_names, expected_names) + if __name__ == '__main__': unittest.main(verbosity=2) From 7d8b4a64a9a93a3c8f61179b88458aaa2c39f0b5 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Mon, 14 Aug 2017 15:14:12 -0400 Subject: [PATCH 04/20] News blurb. --- .../NEWS.d/next/IDLE/2017-08-14-15-13-50.bpo-1612262.-x_Oyq.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/IDLE/2017-08-14-15-13-50.bpo-1612262.-x_Oyq.rst diff --git a/Misc/NEWS.d/next/IDLE/2017-08-14-15-13-50.bpo-1612262.-x_Oyq.rst b/Misc/NEWS.d/next/IDLE/2017-08-14-15-13-50.bpo-1612262.-x_Oyq.rst new file mode 100644 index 00000000000000..7156c3afbd2423 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2017-08-14-15-13-50.bpo-1612262.-x_Oyq.rst @@ -0,0 +1,2 @@ +IDLE module browser now shows nested classes and functions. Original patch +by Guilherme Polo. Tests added by Cheryl Sabella. From 2992dfb8e82aab68c887f7c21522e385f7cc94bf Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Tue, 12 Sep 2017 19:12:42 -0400 Subject: [PATCH 05/20] Improve docstring and variable names. --- Lib/idlelib/browser.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py index 8de12bacf8876d..00e5b469665224 100644 --- a/Lib/idlelib/browser.py +++ b/Lib/idlelib/browser.py @@ -27,29 +27,35 @@ def _traverse_node(node, name=None): """Return the immediate children for a node. - Node is the current node being traversed. The return value is - a tuple with the first value being a dictionary of the - Class/Function instances of the node and the second is - a list of tuples with (lineno, name). + Args: + node: Current node to traverse. + name: Name of module to traverse. + + Returns: + A tuple where the first value is a dictionary of the + Class/Function children instances of the node and the + second is a list of tuples of the form (lineno, name), + where lineno is the line number for the Class/Function + and name is the name of the child (and is also the key + to the returned dictionary). """ items = [] children = {} - for key, cl in node.items(): - if name is None or cl.module == name: - s = key - if hasattr(cl, 'super') and cl.super: + for key, obj in node.items(): + if name is None or obj.module == name: + if hasattr(obj, 'super') and obj.super: supers = [] - for sup in cl.super: + for sup in obj.super: if type(sup) is type(''): sname = sup else: sname = sup.name - if sup.module != cl.module: + if sup.module != obj.module: sname = f'{sup.module}.{sname}' supers.append(sname) - s += '({})'.format(', '.join(supers)) - items.append((cl.lineno, s)) - children[s] = cl + key += '({})'.format(', '.join(supers)) + items.append((obj.lineno, key)) + children[key] = obj return children, items From 40f0e0d700d2ed98ace1eae1f4d1a06e2c5a2d5d Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Thu, 21 Sep 2017 16:43:45 -0400 Subject: [PATCH 06/20] Revise htest. --- Lib/idlelib/browser.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py index 00e5b469665224..1865fa19aed553 100644 --- a/Lib/idlelib/browser.py +++ b/Lib/idlelib/browser.py @@ -287,13 +287,15 @@ def listchildren(self): def _class_browser(parent): # htest # try: + file = sys.argv[1] # If pass file on command line + # If this succeeds, unittest will fail. + except IndexError: file = __file__ - except NameError: - file = sys.argv[0] - if sys.argv[1:]: - file = sys.argv[1] - else: - file = sys.argv[0] + # Add objects for htest + class Nested_in_func(): + def nested_in_class(): pass + def closure(): + class Nested_in_closure: pass dir, file = os.path.split(file) name = os.path.splitext(file)[0] flist = pyshell.PyShellFileList(parent) From 742bef5b438c61b602d80d7fba6899da0361592f Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Thu, 21 Sep 2017 17:43:59 -0400 Subject: [PATCH 07/20] Make pathbrowser htest work with changed class browser. _utest is accessed in init(). --- Lib/idlelib/browser.py | 2 ++ Lib/idlelib/pathbrowser.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py index 1865fa19aed553..171c298eaa120d 100644 --- a/Lib/idlelib/browser.py +++ b/Lib/idlelib/browser.py @@ -62,6 +62,8 @@ def _traverse_node(node, name=None): class ClassBrowser: """Browse module classes and functions in IDLE. """ + # This class is the base class for pathbrowser.PathBrowser. + # Init and close are inherited, other methods are overriden. def __init__(self, flist, name, path, _htest=False, _utest=False): # XXX This API should change, if the file doesn't end in ".py" diff --git a/Lib/idlelib/pathbrowser.py b/Lib/idlelib/pathbrowser.py index 6c19508d314d8f..598dff8d56b37e 100644 --- a/Lib/idlelib/pathbrowser.py +++ b/Lib/idlelib/pathbrowser.py @@ -9,11 +9,12 @@ class PathBrowser(ClassBrowser): - def __init__(self, flist, _htest=False): + def __init__(self, flist, _htest=False, _utest=False): """ _htest - bool, change box location when running htest """ self._htest = _htest + self._utest = _utest self.init(flist) def settitle(self): From 4f9825004dbdbd213b5db36e21db5979153a80ac Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Thu, 21 Sep 2017 20:10:39 -0400 Subject: [PATCH 08/20] copy GetSublist and part of listChildren from Module to Child BrowswerTreeItem. Now both call and use _traverse_node the same way. Failing unittests need re-writing. Htest passes. --- Lib/idlelib/browser.py | 31 +++++++++++- Lib/idlelib/idle_test/test_browser.py | 68 +++++++++++++-------------- 2 files changed, 64 insertions(+), 35 deletions(-) diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py index 171c298eaa120d..d8276ac7044b01 100644 --- a/Lib/idlelib/browser.py +++ b/Lib/idlelib/browser.py @@ -266,6 +266,19 @@ def GetSubList(self): sublist.append(item) return sublist + def GetSubList(self): + """Return the list of ChildBrowserTreeItem items. + + Each item returned from listclasses is the first level of + classes/functions within the module. + """ + print("#### child sublist") + sublist = [] + for name in self.listchildren(): + item = ChildBrowserTreeItem(name, self.classes, self.file) + sublist.append(item) + return sublist + def OnDoubleClick(self): "Open module with file_open and position to lineno, if it exists." if not os.path.exists(self.file): @@ -286,6 +299,22 @@ def listchildren(self): result.sort() return [item[1:] for item in result] + def listchildren(self): + """Return list of classes and functions in the module. + + The dictionary output from pyclbr is re-written as a + list of tuples in the form (lineno, name) and + then sorted so that the classes and functions are + processed in line number order. The returned list only + contains the name and not the line number. An instance + variable self.classes contains the pyclbr dictionary values, + which are instances of Class and Function. + """ + print("CHILD listchild ###", self.cl.children, self.cl.name) + self.classes, items = _traverse_node(self.cl.children) + items.sort() + return [s for item, s in items] + def _class_browser(parent): # htest # try: @@ -294,7 +323,7 @@ def _class_browser(parent): # htest # except IndexError: file = __file__ # Add objects for htest - class Nested_in_func(): + class Nested_in_func(TreeNode): def nested_in_class(): pass def closure(): class Nested_in_closure: pass diff --git a/Lib/idlelib/idle_test/test_browser.py b/Lib/idlelib/idle_test/test_browser.py index 455c3c8869db80..affdc90c4ced03 100644 --- a/Lib/idlelib/idle_test/test_browser.py +++ b/Lib/idlelib/idle_test/test_browser.py @@ -255,40 +255,40 @@ def test_ondoubleclick(self): del browser.file_open -class NestedChildrenTest(unittest.TestCase): - "Test that all the nodes in a nested tree are added to the BrowserTree." - - @classmethod - def setUpClass(cls): - cls.orig_readmodule_ex = browser.pyclbr.readmodule_ex - browser.pyclbr.readmodule_ex = mock.Mock(return_value=mock_pyclbr_tree) - cls.sublist = browser.ModuleBrowserTreeItem(fname).GetSubList() - - @classmethod - def tearDownClass(cls): - browser.pyclbr.readmodule_ex = cls.orig_readmodule_ex - del cls.orig_readmodule_ex, cls.sublist - - def test_nested(self): - queue = deque() - actual_names = [] - # The tree items are processed in breadth first order. - # Verify that processing each sublist hits every node and - # in the right order. - expected_names = ['f0', 'C0(base)', - 'f1', 'c1', 'F1', 'C1()', - 'f2', 'C2', - 'F3'] - queue.extend(self.sublist) - while queue: - cb = queue.popleft() - sublist = cb.GetSubList() - queue.extend(sublist) - self.assertIn(cb.name, cb.GetText()) - self.assertIn(cb.GetIconName(), ('python', 'folder')) - self.assertIs(cb.IsExpandable(), sublist != []) - actual_names.append(cb.name) - self.assertEqual(actual_names, expected_names) +##class NestedChildrenTest(unittest.TestCase): +## "Test that all the nodes in a nested tree are added to the BrowserTree." +## +## @classmethod +## def setUpClass(cls): +## cls.orig_readmodule_ex = browser.pyclbr.readmodule_ex +## browser.pyclbr.readmodule_ex = mock.Mock(return_value=mock_pyclbr_tree) +## cls.sublist = browser.ModuleBrowserTreeItem(fname).GetSubList() +## +## @classmethod +## def tearDownClass(cls): +## browser.pyclbr.readmodule_ex = cls.orig_readmodule_ex +## del cls.orig_readmodule_ex, cls.sublist +## +## def test_nested(self): +## queue = deque() +## actual_names = [] +## # The tree items are processed in breadth first order. +## # Verify that processing each sublist hits every node and +## # in the right order. +## expected_names = ['f0', 'C0(base)', +## 'f1', 'c1', 'F1', 'C1()', +## 'f2', 'C2', +## 'F3'] +## queue.extend(self.sublist) +## while queue: +## cb = queue.popleft() +## sublist = cb.GetSubList() +## queue.extend(sublist) +## self.assertIn(cb.name, cb.GetText()) +## self.assertIn(cb.GetIconName(), ('python', 'folder')) +## self.assertIs(cb.IsExpandable(), sublist != []) +## actual_names.append(cb.name) +## self.assertEqual(actual_names, expected_names) if __name__ == '__main__': From 02a09cae274dc410ac16d7023ff7df3af6dfad3c Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Thu, 21 Sep 2017 20:16:21 -0400 Subject: [PATCH 09/20] Remove debugging prints. --- Lib/idlelib/browser.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py index d8276ac7044b01..c326f4da8f0ee4 100644 --- a/Lib/idlelib/browser.py +++ b/Lib/idlelib/browser.py @@ -272,7 +272,6 @@ def GetSubList(self): Each item returned from listclasses is the first level of classes/functions within the module. """ - print("#### child sublist") sublist = [] for name in self.listchildren(): item = ChildBrowserTreeItem(name, self.classes, self.file) @@ -310,7 +309,6 @@ def listchildren(self): variable self.classes contains the pyclbr dictionary values, which are instances of Class and Function. """ - print("CHILD listchild ###", self.cl.children, self.cl.name) self.classes, items = _traverse_node(self.cl.children) items.sort() return [s for item, s in items] From fa67c45c5c92cff9998a97bfa1de7ee0c06d0926 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Thu, 21 Sep 2017 20:59:26 -0400 Subject: [PATCH 10/20] Delete replaced functions. Change ChildBrowserTreeItem API. Initialize from pyclbr information Object. --- Lib/idlelib/browser.py | 72 ++++++++---------------------------------- 1 file changed, 13 insertions(+), 59 deletions(-) diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py index c326f4da8f0ee4..f06b08e177fd10 100644 --- a/Lib/idlelib/browser.py +++ b/Lib/idlelib/browser.py @@ -164,8 +164,8 @@ def GetSubList(self): """ sublist = [] for name in self.listchildren(): - item = ChildBrowserTreeItem(name, self.classes, self.file) - sublist.append(item) + obj = ChildBrowserTreeItem(self.classes[name]) + sublist.append(obj) return sublist def OnDoubleClick(self): @@ -210,27 +210,12 @@ class ChildBrowserTreeItem(TreeItem): Uses TreeItem as the basis for the structure of the tree. """ - def __init__(self, name, classes, file): - """Create a TreeItem for the class/function. + def __init__(self, obj): + """Create a TreeItem for a pyclbr class/function object.""" - Args: - name: Name of the class/function. - classes: Dictonary of Class/Function instances from pyclbr. - file: Full path and module name. - - Instance variables: - self.cl: Class/Function instance for the class/function name. - self.isfunction: True if self.cl is a Function. - """ - self.name = name - # XXX - Does classes need to be an instance variable? - self.classes = classes - self.file = file - try: - self.cl = self.classes[self.name] - except (IndexError, KeyError): - self.cl = None - self.isfunction = isinstance(self.cl, pyclbr.Function) + self.obj = obj + self.name = obj.name + self.isfunction = isinstance(obj, pyclbr.Function) def GetText(self): "Return the name of the function/class to display." @@ -247,35 +232,15 @@ def GetIconName(self): return "folder" def IsExpandable(self): - "Return True if this class has methods." - if self.cl: - try: - return not not self.cl.children - except AttributeError: - return False - return None + "Return True if self.obj has nested objects." + return self.obj.children != {} def GetSubList(self): - "Return recursive list of ChildBrowserTreeItem items." - if not self.cl: - return [] - sublist = [] - for obj in self.listchildren(): - classes, item_name = obj - item = ChildBrowserTreeItem(item_name, classes, self.file) - sublist.append(item) - return sublist - - def GetSubList(self): - """Return the list of ChildBrowserTreeItem items. - - Each item returned from listclasses is the first level of - classes/functions within the module. - """ + "Return ChildBrowserTreeItems for children." sublist = [] for name in self.listchildren(): - item = ChildBrowserTreeItem(name, self.classes, self.file) - sublist.append(item) + obj = ChildBrowserTreeItem(self.classes[name]) + sublist.append(obj) return sublist def OnDoubleClick(self): @@ -287,17 +252,6 @@ def OnDoubleClick(self): lineno = self.cl.lineno edit.gotoline(lineno) - def listchildren(self): - "Return list of nested classes/functions sorted by lineno." - if not self.cl: - return [] - result = [] - for name, ob in self.cl.children.items(): - classes, items = _traverse_node({name: ob}) - result.append((ob.lineno, classes, items[0][1])) - result.sort() - return [item[1:] for item in result] - def listchildren(self): """Return list of classes and functions in the module. @@ -309,7 +263,7 @@ def listchildren(self): variable self.classes contains the pyclbr dictionary values, which are instances of Class and Function. """ - self.classes, items = _traverse_node(self.cl.children) + self.classes, items = _traverse_node(self.obj.children) items.sort() return [s for item, s in items] From 6599b0b5db5afd38a87296697f2c341e3514b163 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Thu, 21 Sep 2017 21:13:39 -0400 Subject: [PATCH 11/20] Return Objects from listchildren. --- Lib/idlelib/browser.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py index f06b08e177fd10..eb3abf6910e888 100644 --- a/Lib/idlelib/browser.py +++ b/Lib/idlelib/browser.py @@ -163,9 +163,9 @@ def GetSubList(self): classes/functions within the module. """ sublist = [] - for name in self.listchildren(): - obj = ChildBrowserTreeItem(self.classes[name]) - sublist.append(obj) + for obj in self.listchildren(): + treeobj = ChildBrowserTreeItem(obj) + sublist.append(treeobj) return sublist def OnDoubleClick(self): @@ -199,9 +199,8 @@ def listchildren(self): tree = pyclbr.readmodule_ex(name, [dir] + sys.path) except ImportError: return [] - self.classes, items = _traverse_node(tree, name) - items.sort() - return [s for item, s in items] + siblings, tagged_names = _traverse_node(tree, name) + return [siblings[name] for lineno, name in sorted(tagged_names)] class ChildBrowserTreeItem(TreeItem): @@ -238,9 +237,9 @@ def IsExpandable(self): def GetSubList(self): "Return ChildBrowserTreeItems for children." sublist = [] - for name in self.listchildren(): - obj = ChildBrowserTreeItem(self.classes[name]) - sublist.append(obj) + for obj in self.listchildren(): + treeobj = ChildBrowserTreeItem(obj) + sublist.append(treeobj) return sublist def OnDoubleClick(self): @@ -263,9 +262,8 @@ def listchildren(self): variable self.classes contains the pyclbr dictionary values, which are instances of Class and Function. """ - self.classes, items = _traverse_node(self.obj.children) - items.sort() - return [s for item, s in items] + siblings, tagged_names = _traverse_node(self.obj.children) + return [siblings[name] for lineno, name in sorted(tagged_names)] def _class_browser(parent): # htest # From 46245b46b17c6b7fba44ec7df1fe1085237d23f9 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Thu, 21 Sep 2017 21:16:35 -0400 Subject: [PATCH 12/20] Condense GetSublist functions to identical list comprehensions. --- Lib/idlelib/browser.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py index eb3abf6910e888..2830b300a5e761 100644 --- a/Lib/idlelib/browser.py +++ b/Lib/idlelib/browser.py @@ -157,16 +157,8 @@ def GetIconName(self): return "python" def GetSubList(self): - """Return the list of ChildBrowserTreeItem items. - - Each item returned from listclasses is the first level of - classes/functions within the module. - """ - sublist = [] - for obj in self.listchildren(): - treeobj = ChildBrowserTreeItem(obj) - sublist.append(treeobj) - return sublist + "Return ChildBrowserTreeItems for children." + return [ChildBrowserTreeItem(obj) for obj in self.listchildren()] def OnDoubleClick(self): "Open a module in an editor window when double clicked." @@ -236,11 +228,7 @@ def IsExpandable(self): def GetSubList(self): "Return ChildBrowserTreeItems for children." - sublist = [] - for obj in self.listchildren(): - treeobj = ChildBrowserTreeItem(obj) - sublist.append(treeobj) - return sublist + return [ChildBrowserTreeItem(obj) for obj in self.listchildren()] def OnDoubleClick(self): "Open module with file_open and position to lineno, if it exists." From 2c3bab0349dfb6be9fa23d16c2cc8666d54cbcf4 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Fri, 22 Sep 2017 00:11:19 -0400 Subject: [PATCH 13/20] Change _traverse_(node, name) to transform_children(child_dict, modname). Also rewrite doc string. Change calls. --- Lib/idlelib/browser.py | 31 +++++++++++---------------- Lib/idlelib/idle_test/test_browser.py | 10 ++++----- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py index 2830b300a5e761..904ce63ce7e2e7 100644 --- a/Lib/idlelib/browser.py +++ b/Lib/idlelib/browser.py @@ -24,25 +24,18 @@ # Normally pyshell.flist.open, but there is no pyshell.flist for htest. -def _traverse_node(node, name=None): - """Return the immediate children for a node. - - Args: - node: Current node to traverse. - name: Name of module to traverse. - - Returns: - A tuple where the first value is a dictionary of the - Class/Function children instances of the node and the - second is a list of tuples of the form (lineno, name), - where lineno is the line number for the Class/Function - and name is the name of the child (and is also the key - to the returned dictionary). +def transform_children(child_dict, modname=None): + """Transform a child dictionary to an ordered sequence of objects. + + The dictionary maps names to pyclbr information objects. + Filter out imported objects. + Augment class names with bases. + Sort objects by line number. """ items = [] children = {} - for key, obj in node.items(): - if name is None or obj.module == name: + for key, obj in child_dict.items(): + if modname is None or obj.module == modname: if hasattr(obj, 'super') and obj.super: supers = [] for sup in obj.super: @@ -191,7 +184,7 @@ def listchildren(self): tree = pyclbr.readmodule_ex(name, [dir] + sys.path) except ImportError: return [] - siblings, tagged_names = _traverse_node(tree, name) + siblings, tagged_names = transform_children(tree, name) return [siblings[name] for lineno, name in sorted(tagged_names)] @@ -240,7 +233,7 @@ def OnDoubleClick(self): edit.gotoline(lineno) def listchildren(self): - """Return list of classes and functions in the module. + """Return classes and functions nested in self.obj. The dictionary output from pyclbr is re-written as a list of tuples in the form (lineno, name) and @@ -250,7 +243,7 @@ def listchildren(self): variable self.classes contains the pyclbr dictionary values, which are instances of Class and Function. """ - siblings, tagged_names = _traverse_node(self.obj.children) + siblings, tagged_names = transform_children(self.obj.children) return [siblings[name] for lineno, name in sorted(tagged_names)] diff --git a/Lib/idlelib/idle_test/test_browser.py b/Lib/idlelib/idle_test/test_browser.py index affdc90c4ced03..632189a936bb5a 100644 --- a/Lib/idlelib/idle_test/test_browser.py +++ b/Lib/idlelib/idle_test/test_browser.py @@ -77,20 +77,20 @@ def test_close(self): mock_pyclbr_tree = {'f0': f0, 'C0': C0} -class TraverseNodeTest(unittest.TestCase): +class TransformChildrenTest(unittest.TestCase): - def test__traverse_node(self): + def test_transform_children(self): # Nothing to traverse if parameter name isn't same as tree module. - tn = browser._traverse_node(mock_pyclbr_tree, 'different name') + tn = browser.transform_children(mock_pyclbr_tree, 'different name') self.assertEqual(tn, ({}, [])) # Parameter matches tree module. - tn = browser._traverse_node(mock_pyclbr_tree, 'test') + tn = browser.transform_children(mock_pyclbr_tree, 'test') expected = ({'f0': f0, 'C0(base)': C0}, [(1, 'f0'), (6, 'C0(base)')]) self.assertEqual(tn, expected) # No name parameter. - tn = browser._traverse_node({'f1': f1}) + tn = browser.transform_children({'f1': f1}) expected = ({'f1': f1}, [(2, 'f1')]) self.assertEqual(tn, expected) From 49a670c1c5c06ac11958c1e7349aa0b0b2626a94 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Fri, 22 Sep 2017 00:29:38 -0400 Subject: [PATCH 14/20] Rewrite transform_children to new docstring, including restoring addition of bases and sorting before return. Eliminate sorting after return and eliminate CBTI.listchildren. --- Lib/idlelib/browser.py | 46 +++++++++--------------------------------- 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py index 904ce63ce7e2e7..f68da8f8f0c96b 100644 --- a/Lib/idlelib/browser.py +++ b/Lib/idlelib/browser.py @@ -32,9 +32,8 @@ def transform_children(child_dict, modname=None): Augment class names with bases. Sort objects by line number. """ - items = [] - children = {} - for key, obj in child_dict.items(): + obs = [] # Use list since values should already be sorted. + for obj in child_dict.values(): if modname is None or obj.module == modname: if hasattr(obj, 'super') and obj.super: supers = [] @@ -46,10 +45,9 @@ def transform_children(child_dict, modname=None): if sup.module != obj.module: sname = f'{sup.module}.{sname}' supers.append(sname) - key += '({})'.format(', '.join(supers)) - items.append((obj.lineno, key)) - children[key] = obj - return children, items + obj.name += '({})'.format(', '.join(supers)) + obs.append(obj) + return sorted(obs, key=lambda o: o.lineno) class ClassBrowser: @@ -166,16 +164,7 @@ def IsExpandable(self): return os.path.normcase(self.file[-3:]) == ".py" def listchildren(self): - """Return list of classes and functions in the module. - - The dictionary output from pyclbr is re-written as a - list of tuples in the form (lineno, name) and - then sorted so that the classes and functions are - processed in line number order. The returned list only - contains the name and not the line number. An instance - variable self.classes contains the pyclbr dictionary values, - which are instances of Class and Function. - """ + "Return sequenced classes and functions in the module." dir, file = os.path.split(self.file) name, ext = os.path.splitext(file) if os.path.normcase(ext) != ".py": @@ -184,8 +173,7 @@ def listchildren(self): tree = pyclbr.readmodule_ex(name, [dir] + sys.path) except ImportError: return [] - siblings, tagged_names = transform_children(tree, name) - return [siblings[name] for lineno, name in sorted(tagged_names)] + return transform_children(tree, name) class ChildBrowserTreeItem(TreeItem): @@ -195,8 +183,7 @@ class ChildBrowserTreeItem(TreeItem): """ def __init__(self, obj): - """Create a TreeItem for a pyclbr class/function object.""" - + "Create a TreeItem for a pyclbr class/function object." self.obj = obj self.name = obj.name self.isfunction = isinstance(obj, pyclbr.Function) @@ -221,7 +208,8 @@ def IsExpandable(self): def GetSubList(self): "Return ChildBrowserTreeItems for children." - return [ChildBrowserTreeItem(obj) for obj in self.listchildren()] + return [ChildBrowserTreeItem(obj) + for obj in transform_children(self.obj.children)] def OnDoubleClick(self): "Open module with file_open and position to lineno, if it exists." @@ -232,20 +220,6 @@ def OnDoubleClick(self): lineno = self.cl.lineno edit.gotoline(lineno) - def listchildren(self): - """Return classes and functions nested in self.obj. - - The dictionary output from pyclbr is re-written as a - list of tuples in the form (lineno, name) and - then sorted so that the classes and functions are - processed in line number order. The returned list only - contains the name and not the line number. An instance - variable self.classes contains the pyclbr dictionary values, - which are instances of Class and Function. - """ - siblings, tagged_names = transform_children(self.obj.children) - return [siblings[name] for lineno, name in sorted(tagged_names)] - def _class_browser(parent): # htest # try: From 694e94276fbbf2ecfa91c9be4318bee21ece47f0 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Fri, 22 Sep 2017 03:30:26 -0400 Subject: [PATCH 15/20] Make unittest pass without disabling htest. --- Lib/idlelib/browser.py | 24 +-- Lib/idlelib/idle_test/test_browser.py | 221 ++++++++++---------------- 2 files changed, 95 insertions(+), 150 deletions(-) diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py index f68da8f8f0c96b..04dc3f0a92852b 100644 --- a/Lib/idlelib/browser.py +++ b/Lib/idlelib/browser.py @@ -31,6 +31,10 @@ def transform_children(child_dict, modname=None): Filter out imported objects. Augment class names with bases. Sort objects by line number. + + Mutation of obj.name depends on this function being called once. + Current tree saves TreeItems once created. Replacement would require + saving children and patching parent.children and each child.parent. """ obs = [] # Use list since values should already be sorted. for obj in child_dict.values(): @@ -190,10 +194,11 @@ def __init__(self, obj): def GetText(self): "Return the name of the function/class to display." + name = self.name if self.isfunction: - return "def " + self.name + "(...)" + return "def " + name + "(...)" else: - return "class " + self.name + return "class " + name def GetIconName(self): "Return the name of the icon to display." @@ -212,14 +217,13 @@ def GetSubList(self): for obj in transform_children(self.obj.children)] def OnDoubleClick(self): - "Open module with file_open and position to lineno, if it exists." - if not os.path.exists(self.file): - return - edit = file_open(self.file) - if hasattr(self.cl, 'lineno'): - lineno = self.cl.lineno - edit.gotoline(lineno) - + "Open module with file_open and position to lineno." + try: + edit = file_open(self.obj.file) + edit.gotoline(self.obj.lineno) + except (OSError, AttributeError): + pass + def _class_browser(parent): # htest # try: diff --git a/Lib/idlelib/idle_test/test_browser.py b/Lib/idlelib/idle_test/test_browser.py index 632189a936bb5a..f5fd176578f6df 100644 --- a/Lib/idlelib/idle_test/test_browser.py +++ b/Lib/idlelib/idle_test/test_browser.py @@ -76,76 +76,64 @@ def test_close(self): F3 = mb._nest_function(C2, 'F3', 14) mock_pyclbr_tree = {'f0': f0, 'C0': C0} +# transform_children(mock_pyclbr_tree, 'test') mutates C0.name. class TransformChildrenTest(unittest.TestCase): def test_transform_children(self): + eq = self.assertEqual + # Parameter matches tree module. + tcl = list(browser.transform_children(mock_pyclbr_tree, 'test')) + eq(tcl[0], f0) + eq(tcl[1], C0) + eq(tcl[1].name, 'C0(base)') # Nothing to traverse if parameter name isn't same as tree module. tn = browser.transform_children(mock_pyclbr_tree, 'different name') - self.assertEqual(tn, ({}, [])) - - # Parameter matches tree module. - tn = browser.transform_children(mock_pyclbr_tree, 'test') - expected = ({'f0': f0, 'C0(base)': C0}, [(1, 'f0'), (6, 'C0(base)')]) - self.assertEqual(tn, expected) - + self.assertEqual(list(tn), []) # No name parameter. - tn = browser.transform_children({'f1': f1}) - expected = ({'f1': f1}, [(2, 'f1')]) - self.assertEqual(tn, expected) + tn = browser.transform_children({'f1': f1, 'c1': c1}) + self.assertEqual(list(tn), [f1, c1]) class ModuleBrowserTreeItemTest(unittest.TestCase): @classmethod def setUpClass(cls): - cls.orig_readmodule_ex = browser.pyclbr.readmodule_ex - browser.pyclbr.readmodule_ex = mock.Mock(return_value=mock_pyclbr_tree) cls.mbt = browser.ModuleBrowserTreeItem(fname) - @classmethod - def tearDownClass(cls): - browser.pyclbr.readmodule_ex = cls.orig_readmodule_ex - del cls.orig_readmodule_ex, cls.mbt - def test_init(self): - mbt = self.mbt - self.assertEqual(mbt.file, fname) + self.assertEqual(self.mbt.file, fname) def test_gettext(self): - mbt = self.mbt - self.assertEqual(mbt.GetText(), fname) + self.assertEqual(self.mbt.GetText(), fname) def test_geticonname(self): - mbt = self.mbt - self.assertEqual(mbt.GetIconName(), 'python') + self.assertEqual(self.mbt.GetIconName(), 'python') def test_isexpandable(self): - mbt = self.mbt - self.assertTrue(mbt.IsExpandable()) + self.assertTrue(self.mbt.IsExpandable()) def test_listchildren(self): - mbt = self.mbt - expected_names = ['f0', 'C0(base)'] - expected_classes = {'f0': f0, 'C0(base)': C0} - self.assertNotEqual(mbt.classes, expected_classes) - - items = mbt.listchildren() - self.assertEqual(items, expected_names) - # self.classes is set in listchildren. - self.assertEqual(mbt.classes, expected_classes) + save_rex = browser.pyclbr.readmodule_ex + save_tc = browser.transform_children + browser.pyclbr.readmodule_ex = Func(result=mock_pyclbr_tree) + browser.transform_children = Func(result=[f0, C0]) + try: + self.assertEqual(self.mbt.listchildren(), [f0, C0]) + finally: + browser.pyclbr.readmodule_ex = save_rex + browser.transform_children = save_tc def test_getsublist(self): mbt = self.mbt - expected_names = ['f0', 'C0(base)'] - expected_classes = {'f0': f0, 'C0(base)': C0} - sublist = mbt.GetSubList() - for index, item in enumerate(sublist): - self.assertIsInstance(item, browser.ChildBrowserTreeItem) - self.assertEqual(sublist[index].name, expected_names[index]) - self.assertEqual(sublist[index].classes, expected_classes) - self.assertEqual(sublist[index].file, fname) - mbt.classes.clear() + mbt.listchildren = Func(result=[f0, C0]) + sub0, sub1 = mbt.GetSubList() + del mbt.listchildren + self.assertIsInstance(sub0, browser.ChildBrowserTreeItem) + self.assertIsInstance(sub1, browser.ChildBrowserTreeItem) + self.assertEqual(sub0.name, 'f0') + self.assertEqual(sub1.name, 'C0') + def test_ondoubleclick(self): mbt = self.mbt @@ -167,128 +155,81 @@ class ChildBrowserTreeItemTest(unittest.TestCase): @classmethod def setUpClass(cls): - cls.cbt_C0 = browser.ChildBrowserTreeItem('C0', mock_pyclbr_tree, fname) - cls.f1_classes = {'f1': f1} - cls.cbt_f1 = browser.ChildBrowserTreeItem('f1', cls.f1_classes, fname) - cls.F1_classes = {'F1': F1} - cls.cbt_F1 = browser.ChildBrowserTreeItem('F1', cls.F1_classes, fname) - cls.cbt_C1 = browser.ChildBrowserTreeItem('F1', {'C1': C1}, fname) + CBT = browser.ChildBrowserTreeItem + cls.cbt_f1 = CBT(f1) + cls.cbt_C1 = CBT(C1) + cls.cbt_F1 = CBT(F1) @classmethod def tearDownClass(cls): - del cls.cbt_C0, cls.cbt_f1, cls.cbt_F1, cls.cbt_C1 + del cls.cbt_C1, cls.cbt_f1, cls.cbt_F1 def test_init(self): - cbt_C0 = self.cbt_C0 - cbt_f1 = self.cbt_f1 eq = self.assertEqual - - eq(cbt_C0.name, 'C0') - eq(cbt_C0.classes, mock_pyclbr_tree) - eq(cbt_C0.file, fname) - eq(cbt_C0.cl, mock_pyclbr_tree['C0']) - self.assertFalse(cbt_C0.isfunction) - - eq(cbt_f1.name, 'f1') - eq(cbt_f1.classes, self.f1_classes) - eq(cbt_f1.file, fname) - eq(cbt_f1.cl, self.f1_classes['f1']) - self.assertTrue(cbt_f1.isfunction) + eq(self.cbt_C1.name, 'C1') + self.assertFalse(self.cbt_C1.isfunction) + eq(self.cbt_f1.name, 'f1') + self.assertTrue(self.cbt_f1.isfunction) def test_gettext(self): - self.assertEqual(self.cbt_C0.GetText(), 'class C0') + self.assertEqual(self.cbt_C1.GetText(), 'class C1') self.assertEqual(self.cbt_f1.GetText(), 'def f1(...)') def test_geticonname(self): - self.assertEqual(self.cbt_C0.GetIconName(), 'folder') + self.assertEqual(self.cbt_C1.GetIconName(), 'folder') self.assertEqual(self.cbt_f1.GetIconName(), 'python') def test_isexpandable(self): - self.assertTrue(self.cbt_C0.IsExpandable()) + self.assertTrue(self.cbt_C1.IsExpandable()) self.assertTrue(self.cbt_f1.IsExpandable()) self.assertFalse(self.cbt_F1.IsExpandable()) - def test_listchildren(self): - self.assertEqual(self.cbt_C0.listchildren(), [({'F1': F1}, 'F1'), - ({'C1()': C1}, 'C1()')]) - self.assertEqual(self.cbt_f1.listchildren(), [({'f2': f2}, 'f2')]) - self.assertEqual(self.cbt_F1.listchildren(), []) - def test_getsublist(self): eq = self.assertEqual - # When GetSubList is called, additional ChildBrowserTreeItem - # instances are created for children of the current node. - # C0 has childen F1 and C1() (test class with children) - C0_expected = [({'F1': F1}, 'F1'), ({'C1()': C1}, 'C1()')] - for i, sublist in enumerate(self.cbt_C0.GetSubList()): - self.assertIsInstance(sublist, browser.ChildBrowserTreeItem) - eq(sublist.name, C0_expected[i][1]) - eq(sublist.classes, C0_expected[i][0]) - eq(sublist.file, fname) - eq(sublist.cl, C0_expected[i][0][sublist.name]) - - # f1 has children f2. (test function with children) - for sublist in self.cbt_f1.GetSubList(): - self.assertIsInstance(sublist, browser.ChildBrowserTreeItem) - eq(sublist.name, 'f2') - eq(sublist.classes, {'f2': f2}) - eq(sublist.file, fname) - eq(sublist.cl, f2) - - # F1 has no children. + CBT = browser.ChildBrowserTreeItem + + f1sublist = self.cbt_f1.GetSubList() + self.assertIsInstance(f1sublist[0], CBT) + eq(len(f1sublist), 1) + eq(f1sublist[0].name, 'f2') + eq(self.cbt_F1.GetSubList(), []) def test_ondoubleclick(self): fopen = browser.file_open = mock.Mock() goto = fopen.return_value.gotoline = mock.Mock() - - with mock.patch('os.path.exists', return_value=False): - self.cbt_C0.OnDoubleClick() - fopen.assert_not_called() - - with mock.patch('os.path.exists', return_value=True): - self.cbt_F1.OnDoubleClick() - fopen.assert_called() - goto.assert_called() - goto.assert_called_with(self.cbt_F1.cl.lineno) - + self.cbt_F1.OnDoubleClick() + fopen.assert_called() + goto.assert_called() + goto.assert_called_with(self.cbt_F1.obj.lineno) del browser.file_open - - -##class NestedChildrenTest(unittest.TestCase): -## "Test that all the nodes in a nested tree are added to the BrowserTree." -## -## @classmethod -## def setUpClass(cls): -## cls.orig_readmodule_ex = browser.pyclbr.readmodule_ex -## browser.pyclbr.readmodule_ex = mock.Mock(return_value=mock_pyclbr_tree) -## cls.sublist = browser.ModuleBrowserTreeItem(fname).GetSubList() -## -## @classmethod -## def tearDownClass(cls): -## browser.pyclbr.readmodule_ex = cls.orig_readmodule_ex -## del cls.orig_readmodule_ex, cls.sublist -## -## def test_nested(self): -## queue = deque() -## actual_names = [] -## # The tree items are processed in breadth first order. -## # Verify that processing each sublist hits every node and -## # in the right order. -## expected_names = ['f0', 'C0(base)', -## 'f1', 'c1', 'F1', 'C1()', -## 'f2', 'C2', -## 'F3'] -## queue.extend(self.sublist) -## while queue: -## cb = queue.popleft() -## sublist = cb.GetSubList() -## queue.extend(sublist) -## self.assertIn(cb.name, cb.GetText()) -## self.assertIn(cb.GetIconName(), ('python', 'folder')) -## self.assertIs(cb.IsExpandable(), sublist != []) -## actual_names.append(cb.name) -## self.assertEqual(actual_names, expected_names) + # Failure test would have to raise OSError or AttributeError. + + +class NestedChildrenTest(unittest.TestCase): + "Test that all the nodes in a nested tree are added to the BrowserTree." + + def test_nested(self): + queue = deque() + actual_names = [] + # The tree items are processed in breadth first order. + # Verify that processing each sublist hits every node and + # in the right order. + expected_names = ['f0', 'C0', + 'f1', 'c1', 'F1', 'C1()', + 'f2', 'C2', + 'F3'] + CBT = browser.ChildBrowserTreeItem + queue.extend((CBT(f0), CBT(C0))) + while queue: + cb = queue.popleft() + sublist = cb.GetSubList() + queue.extend(sublist) + self.assertIn(cb.name, cb.GetText()) + self.assertIn(cb.GetIconName(), ('python', 'folder')) + self.assertIs(cb.IsExpandable(), sublist != []) + actual_names.append(cb.name) + self.assertEqual(actual_names, expected_names) if __name__ == '__main__': From 778f77d3b963f7bad493e237cbfd51fcfef62bf5 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Fri, 22 Sep 2017 03:40:00 -0400 Subject: [PATCH 16/20] Whitespace --- Lib/idlelib/browser.py | 2 +- Lib/idlelib/idle_test/test_browser.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py index 04dc3f0a92852b..cb4277d4c235f0 100644 --- a/Lib/idlelib/browser.py +++ b/Lib/idlelib/browser.py @@ -223,7 +223,7 @@ def OnDoubleClick(self): edit.gotoline(self.obj.lineno) except (OSError, AttributeError): pass - + def _class_browser(parent): # htest # try: diff --git a/Lib/idlelib/idle_test/test_browser.py b/Lib/idlelib/idle_test/test_browser.py index f5fd176578f6df..867ef564b16dcb 100644 --- a/Lib/idlelib/idle_test/test_browser.py +++ b/Lib/idlelib/idle_test/test_browser.py @@ -215,7 +215,7 @@ def test_nested(self): # The tree items are processed in breadth first order. # Verify that processing each sublist hits every node and # in the right order. - expected_names = ['f0', 'C0', + expected_names = ['f0', 'C0', # This is run before transform test. 'f1', 'c1', 'F1', 'C1()', 'f2', 'C2', 'F3'] From 32f573ff1faeb4ceb982c5d3e309ad4a93121ebb Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Fri, 22 Sep 2017 10:54:43 -0400 Subject: [PATCH 17/20] whitespace --- Lib/idlelib/browser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py index cb4277d4c235f0..e04f409f498cb3 100644 --- a/Lib/idlelib/browser.py +++ b/Lib/idlelib/browser.py @@ -227,8 +227,8 @@ def OnDoubleClick(self): def _class_browser(parent): # htest # try: - file = sys.argv[1] # If pass file on command line - # If this succeeds, unittest will fail. + file = sys.argv[1] # If pass file on command line + # If this succeeds, unittest will fail. except IndexError: file = __file__ # Add objects for htest From a1d457454bf8f9c9ed888d1b21a692994867c081 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Fri, 22 Sep 2017 11:30:40 -0400 Subject: [PATCH 18/20] Add coverage. --- Lib/idlelib/idle_test/test_browser.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/idlelib/idle_test/test_browser.py b/Lib/idlelib/idle_test/test_browser.py index 867ef564b16dcb..d9ec3677b65204 100644 --- a/Lib/idlelib/idle_test/test_browser.py +++ b/Lib/idlelib/idle_test/test_browser.py @@ -1,4 +1,7 @@ """ Test idlelib.browser. + +Coverage: 88% +(Higher, because should exclude 3 lines that .coveragerc won't exclude.) """ import os.path From e40bf5d4625b55909163c7b29ea7194bdc954eb3 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Fri, 22 Sep 2017 11:36:15 -0400 Subject: [PATCH 19/20] Tweak blurb. --- .../next/IDLE/2017-08-14-15-13-50.bpo-1612262.-x_Oyq.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/IDLE/2017-08-14-15-13-50.bpo-1612262.-x_Oyq.rst b/Misc/NEWS.d/next/IDLE/2017-08-14-15-13-50.bpo-1612262.-x_Oyq.rst index 7156c3afbd2423..0d4494c16a7a01 100644 --- a/Misc/NEWS.d/next/IDLE/2017-08-14-15-13-50.bpo-1612262.-x_Oyq.rst +++ b/Misc/NEWS.d/next/IDLE/2017-08-14-15-13-50.bpo-1612262.-x_Oyq.rst @@ -1,2 +1,3 @@ -IDLE module browser now shows nested classes and functions. Original patch -by Guilherme Polo. Tests added by Cheryl Sabella. +IDLE module browser now shows nested classes and functions. +Original patches for code and tests by Guilherme Polo and +Cheryl Sabella, respectively. From 096944c0d753c60da18343adb2cab5046e61bf7d Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Fri, 22 Sep 2017 15:25:52 -0400 Subject: [PATCH 20/20] Make transform_children robust against repeated calls with same dict. --- Lib/idlelib/browser.py | 11 ++++++----- Lib/idlelib/idle_test/test_browser.py | 3 +++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py index e04f409f498cb3..1fc04d873a86b7 100644 --- a/Lib/idlelib/browser.py +++ b/Lib/idlelib/browser.py @@ -32,14 +32,15 @@ def transform_children(child_dict, modname=None): Augment class names with bases. Sort objects by line number. - Mutation of obj.name depends on this function being called once. - Current tree saves TreeItems once created. Replacement would require - saving children and patching parent.children and each child.parent. + The current tree only calls this once per child_dic as it saves + TreeItems once created. A future tree and tests might violate this, + so a check prevents multiple in-place augmentations. """ obs = [] # Use list since values should already be sorted. - for obj in child_dict.values(): + for key, obj in child_dict.items(): if modname is None or obj.module == modname: - if hasattr(obj, 'super') and obj.super: + if hasattr(obj, 'super') and obj.super and obj.name == key: + # If obj.name != key, it has already been suffixed. supers = [] for sup in obj.super: if type(sup) is type(''): diff --git a/Lib/idlelib/idle_test/test_browser.py b/Lib/idlelib/idle_test/test_browser.py index d9ec3677b65204..025e2902d8f87a 100644 --- a/Lib/idlelib/idle_test/test_browser.py +++ b/Lib/idlelib/idle_test/test_browser.py @@ -90,6 +90,9 @@ def test_transform_children(self): eq(tcl[0], f0) eq(tcl[1], C0) eq(tcl[1].name, 'C0(base)') + # Check that second call does not add second '(base)' suffix. + tcl = list(browser.transform_children(mock_pyclbr_tree, 'test')) + eq(tcl[1].name, 'C0(base)') # Nothing to traverse if parameter name isn't same as tree module. tn = browser.transform_children(mock_pyclbr_tree, 'different name') self.assertEqual(list(tn), [])