From: Terry Jan Reedy Date: Fri, 22 Sep 2017 21:28:01 +0000 (-0400) Subject: [3.6] bpo-1612262: IDLE: Class Browser shows nested functions, classes (GH-2573... X-Git-Tag: v3.6.4rc1~236 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=fa1cae5832cbcfafedc4b1879c2abc85452f4edd;p=python [3.6] bpo-1612262: IDLE: Class Browser shows nested functions, classes (GH-2573) (#3702) Original patches for code and tests by Guilherme Polo and Cheryl Sabella, respectively. (cherry picked from commit 058de11360ea6816a6e978c7be0bcbea99a3f7da) --- diff --git a/Lib/idlelib/_pyclbr.py b/Lib/idlelib/_pyclbr.py new file mode 100644 index 0000000000..e85566dbf6 --- /dev/null +++ b/Lib/idlelib/_pyclbr.py @@ -0,0 +1,402 @@ +# A private copy of 3.7.0a1 pyclbr for use by idlelib.browser +"""Parse a Python module and describe its classes and functions. + +Parse enough of a Python file to recognize imports and class and +function definitions, and to find out the superclasses of a class. + +The interface consists of a single function: + readmodule_ex(module, path=None) +where module is the name of a Python module, and path is an optional +list of directories where the module is to be searched. If present, +path is prepended to the system search path sys.path. The return value +is a dictionary. The keys of the dictionary are the names of the +classes and functions defined in the module (including classes that are +defined via the from XXX import YYY construct). The values are +instances of classes Class and Function. One special key/value pair is +present for packages: the key '__path__' has a list as its value which +contains the package search path. + +Classes and Functions have a common superclass: _Object. Every instance +has the following attributes: + module -- name of the module; + name -- name of the object; + file -- file in which the object is defined; + lineno -- line in the file where the object's definition starts; + parent -- parent of this object, if any; + children -- nested objects contained in this object. +The 'children' attribute is a dictionary mapping names to objects. + +Instances of Function describe functions with the attributes from _Object. + +Instances of Class describe classes with the attributes from _Object, +plus the following: + super -- list of super classes (Class instances if possible); + methods -- mapping of method names to beginning line numbers. +If the name of a super class is not recognized, the corresponding +entry in the list of super classes is not a class instance but a +string giving the name of the super class. Since import statements +are recognized and imported modules are scanned as well, this +shouldn't happen often. +""" + +import io +import sys +import importlib.util +import tokenize +from token import NAME, DEDENT, OP + +__all__ = ["readmodule", "readmodule_ex", "Class", "Function"] + +_modules = {} # Initialize cache of modules we've seen. + + +class _Object: + "Informaton about Python class or function." + def __init__(self, module, name, file, lineno, parent): + self.module = module + self.name = name + self.file = file + self.lineno = lineno + self.parent = parent + self.children = {} + + def _addchild(self, name, obj): + self.children[name] = obj + + +class Function(_Object): + "Information about a Python function, including methods." + def __init__(self, module, name, file, lineno, parent=None): + _Object.__init__(self, module, name, file, lineno, parent) + + +class Class(_Object): + "Information about a Python class." + def __init__(self, module, name, super, file, lineno, parent=None): + _Object.__init__(self, module, name, file, lineno, parent) + self.super = [] if super is None else super + self.methods = {} + + def _addmethod(self, name, lineno): + self.methods[name] = lineno + + +def _nest_function(ob, func_name, lineno): + "Return a Function after nesting within ob." + newfunc = Function(ob.module, func_name, ob.file, lineno, ob) + ob._addchild(func_name, newfunc) + if isinstance(ob, Class): + ob._addmethod(func_name, lineno) + return newfunc + +def _nest_class(ob, class_name, lineno, super=None): + "Return a Class after nesting within ob." + newclass = Class(ob.module, class_name, super, ob.file, lineno, ob) + ob._addchild(class_name, newclass) + return newclass + +def readmodule(module, path=None): + """Return Class objects for the top-level classes in module. + + This is the original interface, before Functions were added. + """ + + res = {} + for key, value in _readmodule(module, path or []).items(): + if isinstance(value, Class): + res[key] = value + return res + +def readmodule_ex(module, path=None): + """Return a dictionary with all functions and classes in module. + + Search for module in PATH + sys.path. + If possible, include imported superclasses. + Do this by reading source, without importing (and executing) it. + """ + return _readmodule(module, path or []) + +def _readmodule(module, path, inpackage=None): + """Do the hard work for readmodule[_ex]. + + If inpackage is given, it must be the dotted name of the package in + which we are searching for a submodule, and then PATH must be the + package search path; otherwise, we are searching for a top-level + module, and path is combined with sys.path. + """ + # Compute the full module name (prepending inpackage if set). + if inpackage is not None: + fullmodule = "%s.%s" % (inpackage, module) + else: + fullmodule = module + + # Check in the cache. + if fullmodule in _modules: + return _modules[fullmodule] + + # Initialize the dict for this module's contents. + tree = {} + + # Check if it is a built-in module; we don't do much for these. + if module in sys.builtin_module_names and inpackage is None: + _modules[module] = tree + return tree + + # Check for a dotted module name. + i = module.rfind('.') + if i >= 0: + package = module[:i] + submodule = module[i+1:] + parent = _readmodule(package, path, inpackage) + if inpackage is not None: + package = "%s.%s" % (inpackage, package) + if not '__path__' in parent: + raise ImportError('No package named {}'.format(package)) + return _readmodule(submodule, parent['__path__'], package) + + # Search the path for the module. + f = None + if inpackage is not None: + search_path = path + else: + search_path = path + sys.path + spec = importlib.util._find_spec_from_path(fullmodule, search_path) + _modules[fullmodule] = tree + # Is module a package? + if spec.submodule_search_locations is not None: + tree['__path__'] = spec.submodule_search_locations + try: + source = spec.loader.get_source(fullmodule) + if source is None: + return tree + except (AttributeError, ImportError): + # If module is not Python source, we cannot do anything. + return tree + + fname = spec.loader.get_filename(fullmodule) + return _create_tree(fullmodule, path, fname, source, tree, inpackage) + + +def _create_tree(fullmodule, path, fname, source, tree, inpackage): + """Return the tree for a particular module. + + fullmodule (full module name), inpackage+module, becomes o.module. + path is passed to recursive calls of _readmodule. + fname becomes o.file. + source is tokenized. Imports cause recursive calls to _readmodule. + tree is {} or {'__path__': }. + inpackage, None or string, is passed to recursive calls of _readmodule. + + The effect of recursive calls is mutation of global _modules. + """ + f = io.StringIO(source) + + stack = [] # Initialize stack of (class, indent) pairs. + + g = tokenize.generate_tokens(f.readline) + try: + for tokentype, token, start, _end, _line in g: + if tokentype == DEDENT: + lineno, thisindent = start + # Close previous nested classes and defs. + while stack and stack[-1][1] >= thisindent: + del stack[-1] + elif token == 'def': + lineno, thisindent = start + # Close previous nested classes and defs. + while stack and stack[-1][1] >= thisindent: + del stack[-1] + tokentype, func_name, start = next(g)[0:3] + if tokentype != NAME: + continue # Skip def with syntax error. + cur_func = None + if stack: + cur_obj = stack[-1][0] + cur_func = _nest_function(cur_obj, func_name, lineno) + else: + # It is just a function. + cur_func = Function(fullmodule, func_name, fname, lineno) + tree[func_name] = cur_func + stack.append((cur_func, thisindent)) + elif token == 'class': + lineno, thisindent = start + # Close previous nested classes and defs. + while stack and stack[-1][1] >= thisindent: + del stack[-1] + tokentype, class_name, start = next(g)[0:3] + if tokentype != NAME: + continue # Skip class with syntax error. + # Parse what follows the class name. + tokentype, token, start = next(g)[0:3] + inherit = None + if token == '(': + names = [] # Initialize list of superclasses. + level = 1 + super = [] # Tokens making up current superclass. + while True: + tokentype, token, start = next(g)[0:3] + if token in (')', ',') and level == 1: + n = "".join(super) + if n in tree: + # We know this super class. + n = tree[n] + else: + c = n.split('.') + if len(c) > 1: + # Super class form is module.class: + # look in module for class. + m = c[-2] + c = c[-1] + if m in _modules: + d = _modules[m] + if c in d: + n = d[c] + names.append(n) + super = [] + if token == '(': + level += 1 + elif token == ')': + level -= 1 + if level == 0: + break + elif token == ',' and level == 1: + pass + # Only use NAME and OP (== dot) tokens for type name. + elif tokentype in (NAME, OP) and level == 1: + super.append(token) + # Expressions in the base list are not supported. + inherit = names + if stack: + cur_obj = stack[-1][0] + cur_class = _nest_class( + cur_obj, class_name, lineno, inherit) + else: + cur_class = Class(fullmodule, class_name, inherit, + fname, lineno) + tree[class_name] = cur_class + stack.append((cur_class, thisindent)) + elif token == 'import' and start[1] == 0: + modules = _getnamelist(g) + for mod, _mod2 in modules: + try: + # Recursively read the imported module. + if inpackage is None: + _readmodule(mod, path) + else: + try: + _readmodule(mod, path, inpackage) + except ImportError: + _readmodule(mod, []) + except: + # If we can't find or parse the imported module, + # too bad -- don't die here. + pass + elif token == 'from' and start[1] == 0: + mod, token = _getname(g) + if not mod or token != "import": + continue + names = _getnamelist(g) + try: + # Recursively read the imported module. + d = _readmodule(mod, path, inpackage) + except: + # If we can't find or parse the imported module, + # too bad -- don't die here. + continue + # Add any classes that were defined in the imported module + # to our name space if they were mentioned in the list. + for n, n2 in names: + if n in d: + tree[n2 or n] = d[n] + elif n == '*': + # Don't add names that start with _. + for n in d: + if n[0] != '_': + tree[n] = d[n] + except StopIteration: + pass + + f.close() + return tree + + +def _getnamelist(g): + """Return list of (dotted-name, as-name or None) tuples for token source g. + + An as-name is the name that follows 'as' in an as clause. + """ + names = [] + while True: + name, token = _getname(g) + if not name: + break + if token == 'as': + name2, token = _getname(g) + else: + name2 = None + names.append((name, name2)) + while token != "," and "\n" not in token: + token = next(g)[1] + if token != ",": + break + return names + + +def _getname(g): + "Return (dotted-name or None, next-token) tuple for token source g." + parts = [] + tokentype, token = next(g)[0:2] + if tokentype != NAME and token != '*': + return (None, token) + parts.append(token) + while True: + tokentype, token = next(g)[0:2] + if token != '.': + break + tokentype, token = next(g)[0:2] + if tokentype != NAME: + break + parts.append(token) + return (".".join(parts), token) + + +def _main(): + "Print module output (default this file) for quick visual check." + import os + try: + mod = sys.argv[1] + except: + mod = __file__ + if os.path.exists(mod): + path = [os.path.dirname(mod)] + mod = os.path.basename(mod) + if mod.lower().endswith(".py"): + mod = mod[:-3] + else: + path = [] + tree = readmodule_ex(mod, path) + lineno_key = lambda a: getattr(a, 'lineno', 0) + objs = sorted(tree.values(), key=lineno_key, reverse=True) + indent_level = 2 + while objs: + obj = objs.pop() + if isinstance(obj, list): + # Value is a __path__ key. + continue + if not hasattr(obj, 'indent'): + obj.indent = 0 + + if isinstance(obj, _Object): + new_objs = sorted(obj.children.values(), + key=lineno_key, reverse=True) + for ob in new_objs: + ob.indent = obj.indent + indent_level + objs.extend(new_objs) + if isinstance(obj, Class): + print("{}class {} {} {}" + .format(' ' * obj.indent, obj.name, obj.super, obj.lineno)) + elif isinstance(obj, Function): + print("{}def {} {}".format(' ' * obj.indent, obj.name, obj.lineno)) + +if __name__ == "__main__": + _main() diff --git a/Lib/idlelib/browser.py b/Lib/idlelib/browser.py index 4cf4744fb0..df6d5c7cb3 100644 --- a/Lib/idlelib/browser.py +++ b/Lib/idlelib/browser.py @@ -11,7 +11,7 @@ XXX TO DO: """ import os -import pyclbr +from idlelib import _pyclbr as pyclbr import sys from idlelib.config import idleConf @@ -19,14 +19,49 @@ from idlelib import pyshell from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas from idlelib.windows import ListedToplevel + file_open = None # Method...Item and Class...Item use this. # Normally pyshell.flist.open, but there is no pyshell.flist for htest. + +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. + + 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 key, obj in child_dict.items(): + if modname is None or obj.module == modname: + 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(''): + sname = sup + else: + sname = sup.name + if sup.module != obj.module: + sname = f'{sup.module}.{sname}' + supers.append(sname) + obj.name += '({})'.format(', '.join(supers)) + obs.append(obj) + return sorted(obs, key=lambda o: o.lineno) + + 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): + 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. @@ -47,11 +82,12 @@ class ClassBrowser: 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): @@ -80,8 +116,9 @@ class ClassBrowser: 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." @@ -92,6 +129,7 @@ class ClassBrowser: "Return a ModuleBrowserTreeItem as the root of the tree." return ModuleBrowserTreeItem(self.file) + class ModuleBrowserTreeItem(TreeItem): """Browser tree for Python module. @@ -115,16 +153,8 @@ class ModuleBrowserTreeItem(TreeItem): return "python" def GetSubList(self): - """Return the list of ClassBrowserTreeItem items. - - Each item returned from listclasses is the first level of - classes/functions within the module. - """ - sublist = [] - for name in self.listclasses(): - item = ClassBrowserTreeItem(name, self.classes, self.file) - sublist.append(item) - 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." @@ -132,89 +162,44 @@ class ModuleBrowserTreeItem(TreeItem): 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." return os.path.normcase(self.file[-3:]) == ".py" - def listclasses(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. - """ + def listchildren(self): + "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": 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 - items.sort() - list = [] - for item, s in items: - list.append(s) - return list - -class ClassBrowserTreeItem(TreeItem): - """Browser tree for classes within a module. + return transform_children(tree, name) - Uses TreeItem as the basis for the structure of the tree. - """ - def __init__(self, name, classes, file): - """Create a TreeItem for the class/function. +class ChildBrowserTreeItem(TreeItem): + """Browser tree for child nodes within the module. - Args: - name: Name of the class/function. - classes: Dictonary of Class/Function instances from pyclbr. - file: Full path and module name. + Uses TreeItem as the basis for the structure of the tree. + """ - 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) + def __init__(self, obj): + "Create a TreeItem for a pyclbr class/function object." + 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." + 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." @@ -224,95 +209,34 @@ class ClassBrowserTreeItem(TreeItem): return "folder" def IsExpandable(self): - "Return True if this class has methods." - if self.cl: - try: - return not not self.cl.methods - except AttributeError: - return False - return None + "Return True if self.obj has nested objects." + return self.obj.children != {} def GetSubList(self): - """Return Class methods as a list of MethodBrowserTreeItem items. - - Each item is a method within the class. - """ - if not self.cl: - return [] - sublist = [] - for name in self.listmethods(): - item = MethodBrowserTreeItem(name, self.cl, self.file) - sublist.append(item) - return sublist + "Return ChildBrowserTreeItems for children." + 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." - 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) - - def listmethods(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 + "(...)" - - 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 + "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 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: + 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(TreeNode): + 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) @@ -321,5 +245,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 0000000000..875b0b9cb8 --- /dev/null +++ b/Lib/idlelib/idle_test/test_browser.py @@ -0,0 +1,242 @@ +""" Test idlelib.browser. + +Coverage: 88% +(Higher, because should exclude 3 lines that .coveragerc won't exclude.) +""" + +import os.path +import unittest +from idlelib import _pyclbr as 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 + + +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, 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} + +# 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)') + # 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), []) + # No name parameter. + tn = browser.transform_children({'f1': f1, 'c1': c1}) + self.assertEqual(list(tn), [f1, c1]) + + +class ModuleBrowserTreeItemTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.mbt = browser.ModuleBrowserTreeItem(fname) + + def test_init(self): + self.assertEqual(self.mbt.file, fname) + + def test_gettext(self): + self.assertEqual(self.mbt.GetText(), fname) + + def test_geticonname(self): + self.assertEqual(self.mbt.GetIconName(), 'python') + + def test_isexpandable(self): + self.assertTrue(self.mbt.IsExpandable()) + + def test_listchildren(self): + 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 + 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 + fopen = browser.file_open = mock.Mock() + + 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): + 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_C1, cls.cbt_f1, cls.cbt_F1 + + def test_init(self): + eq = self.assertEqual + 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_C1.GetText(), 'class C1') + self.assertEqual(self.cbt_f1.GetText(), 'def f1(...)') + + def test_geticonname(self): + self.assertEqual(self.cbt_C1.GetIconName(), 'folder') + self.assertEqual(self.cbt_f1.GetIconName(), 'python') + + def test_isexpandable(self): + self.assertTrue(self.cbt_C1.IsExpandable()) + self.assertTrue(self.cbt_f1.IsExpandable()) + self.assertFalse(self.cbt_F1.IsExpandable()) + + def test_getsublist(self): + eq = self.assertEqual + 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() + self.cbt_F1.OnDoubleClick() + fopen.assert_called() + goto.assert_called() + goto.assert_called_with(self.cbt_F1.obj.lineno) + del browser.file_open + # 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', # This is run before transform test. + '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__': + unittest.main(verbosity=2) diff --git a/Lib/idlelib/pathbrowser.py b/Lib/idlelib/pathbrowser.py index 6c19508d31..598dff8d56 100644 --- a/Lib/idlelib/pathbrowser.py +++ b/Lib/idlelib/pathbrowser.py @@ -9,11 +9,12 @@ from idlelib.tree import TreeItem 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): 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 0000000000..0d4494c16a --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2017-08-14-15-13-50.bpo-1612262.-x_Oyq.rst @@ -0,0 +1,3 @@ +IDLE module browser now shows nested classes and functions. +Original patches for code and tests by Guilherme Polo and +Cheryl Sabella, respectively.