Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-1612262: IDLE: Class Browser shows nested functions and classes #2573

Merged
merged 21 commits into from
Sep 22, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a7b6411
bpo-1612262: IDLE: Class Browser for nested functions/classes
csabella Jul 4, 2017
1f4c547
Initial test for TreeItem structure
csabella Jul 12, 2017
6763dfc
More complete unit tests.
csabella Aug 5, 2017
7d8b4a6
News blurb.
terryjreedy Aug 14, 2017
2992dfb
Improve docstring and variable names.
csabella Sep 12, 2017
b947579
Merge remote-tracking branch 'origin/master' into pr_2573
terryjreedy Sep 21, 2017
40f0e0d
Revise htest.
terryjreedy Sep 21, 2017
742bef5
Make pathbrowser htest work with changed class browser.
terryjreedy Sep 21, 2017
4f98250
copy GetSublist and part of listChildren from Module to Child
terryjreedy Sep 22, 2017
02a09ca
Remove debugging prints.
terryjreedy Sep 22, 2017
fa67c45
Delete replaced functions. Change ChildBrowserTreeItem API.
terryjreedy Sep 22, 2017
6599b0b
Return Objects from listchildren.
terryjreedy Sep 22, 2017
46245b4
Condense GetSublist functions to identical list comprehensions.
terryjreedy Sep 22, 2017
2c3bab0
Change _traverse_(node, name) to transform_children(child_dict, modna…
terryjreedy Sep 22, 2017
49a670c
Rewrite transform_children to new docstring, including restoring
terryjreedy Sep 22, 2017
694e942
Make unittest pass without disabling htest.
terryjreedy Sep 22, 2017
778f77d
Whitespace
terryjreedy Sep 22, 2017
32f573f
whitespace
terryjreedy Sep 22, 2017
a1d4574
Add coverage.
terryjreedy Sep 22, 2017
e40bf5d
Tweak blurb.
terryjreedy Sep 22, 2017
096944c
Make transform_children robust against repeated calls with same dict.
terryjreedy Sep 22, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 82 additions & 156 deletions Lib/idlelib/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,49 @@
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.
Expand All @@ -47,11 +82,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):
Expand Down Expand Up @@ -80,8 +116,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."
Expand All @@ -92,6 +129,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.

Expand All @@ -115,106 +153,53 @@ def GetIconName(self):
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."
if os.path.normcase(self.file[-3:]) != ".py":
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."
Expand All @@ -224,95 +209,34 @@ def GetIconName(self):
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)
Expand All @@ -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)
Loading