• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Module browser.
2
3XXX TO DO:
4
5- reparse when source changed (maybe just a button would be OK?)
6    (or recheck on window popup)
7- add popup menu with more options (e.g. doc strings, base classes, imports)
8- add base classes to class browser tree
9- finish removing limitation to x.py files (ModuleBrowserTreeItem)
10"""
11
12import os
13import pyclbr
14import sys
15
16from idlelib.config import idleConf
17from idlelib import pyshell
18from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas
19from idlelib.window import ListedToplevel
20
21
22file_open = None  # Method...Item and Class...Item use this.
23# Normally pyshell.flist.open, but there is no pyshell.flist for htest.
24
25
26def transform_children(child_dict, modname=None):
27    """Transform a child dictionary to an ordered sequence of objects.
28
29    The dictionary maps names to pyclbr information objects.
30    Filter out imported objects.
31    Augment class names with bases.
32    Sort objects by line number.
33
34    The current tree only calls this once per child_dic as it saves
35    TreeItems once created.  A future tree and tests might violate this,
36    so a check prevents multiple in-place augmentations.
37    """
38    obs = []  # Use list since values should already be sorted.
39    for key, obj in child_dict.items():
40        if modname is None or obj.module == modname:
41            if hasattr(obj, 'super') and obj.super and obj.name == key:
42                # If obj.name != key, it has already been suffixed.
43                supers = []
44                for sup in obj.super:
45                    if type(sup) is type(''):
46                        sname = sup
47                    else:
48                        sname = sup.name
49                        if sup.module != obj.module:
50                            sname = f'{sup.module}.{sname}'
51                    supers.append(sname)
52                obj.name += '({})'.format(', '.join(supers))
53            obs.append(obj)
54    return sorted(obs, key=lambda o: o.lineno)
55
56
57class ModuleBrowser:
58    """Browse module classes and functions in IDLE.
59    """
60    # This class is also the base class for pathbrowser.PathBrowser.
61    # Init and close are inherited, other methods are overridden.
62    # PathBrowser.__init__ does not call __init__ below.
63
64    def __init__(self, master, path, *, _htest=False, _utest=False):
65        """Create a window for browsing a module's structure.
66
67        Args:
68            master: parent for widgets.
69            path: full path of file to browse.
70            _htest - bool; change box location when running htest.
71            -utest - bool; suppress contents when running unittest.
72
73        Global variables:
74            file_open: Function used for opening a file.
75
76        Instance variables:
77            name: Module name.
78            file: Full path and module with .py extension.  Used in
79                creating ModuleBrowserTreeItem as the rootnode for
80                the tree and subsequently in the children.
81        """
82        self.master = master
83        self.path = path
84        self._htest = _htest
85        self._utest = _utest
86        self.init()
87
88    def close(self, event=None):
89        "Dismiss the window and the tree nodes."
90        self.top.destroy()
91        self.node.destroy()
92
93    def init(self):
94        "Create browser tkinter widgets, including the tree."
95        global file_open
96        root = self.master
97        flist = (pyshell.flist if not (self._htest or self._utest)
98                 else pyshell.PyShellFileList(root))
99        file_open = flist.open
100        pyclbr._modules.clear()
101
102        # create top
103        self.top = top = ListedToplevel(root)
104        top.protocol("WM_DELETE_WINDOW", self.close)
105        top.bind("<Escape>", self.close)
106        if self._htest: # place dialog below parent if running htest
107            top.geometry("+%d+%d" %
108                (root.winfo_rootx(), root.winfo_rooty() + 200))
109        self.settitle()
110        top.focus_set()
111
112        # create scrolled canvas
113        theme = idleConf.CurrentTheme()
114        background = idleConf.GetHighlight(theme, 'normal')['background']
115        sc = ScrolledCanvas(top, bg=background, highlightthickness=0,
116                            takefocus=1)
117        sc.frame.pack(expand=1, fill="both")
118        item = self.rootnode()
119        self.node = node = TreeNode(sc.canvas, None, item)
120        if not self._utest:
121            node.update()
122            node.expand()
123
124    def settitle(self):
125        "Set the window title."
126        self.top.wm_title("Module Browser - " + os.path.basename(self.path))
127        self.top.wm_iconname("Module Browser")
128
129    def rootnode(self):
130        "Return a ModuleBrowserTreeItem as the root of the tree."
131        return ModuleBrowserTreeItem(self.path)
132
133
134class ModuleBrowserTreeItem(TreeItem):
135    """Browser tree for Python module.
136
137    Uses TreeItem as the basis for the structure of the tree.
138    Used by both browsers.
139    """
140
141    def __init__(self, file):
142        """Create a TreeItem for the file.
143
144        Args:
145            file: Full path and module name.
146        """
147        self.file = file
148
149    def GetText(self):
150        "Return the module name as the text string to display."
151        return os.path.basename(self.file)
152
153    def GetIconName(self):
154        "Return the name of the icon to display."
155        return "python"
156
157    def GetSubList(self):
158        "Return ChildBrowserTreeItems for children."
159        return [ChildBrowserTreeItem(obj) for obj in self.listchildren()]
160
161    def OnDoubleClick(self):
162        "Open a module in an editor window when double clicked."
163        if os.path.normcase(self.file[-3:]) != ".py":
164            return
165        if not os.path.exists(self.file):
166            return
167        file_open(self.file)
168
169    def IsExpandable(self):
170        "Return True if Python (.py) file."
171        return os.path.normcase(self.file[-3:]) == ".py"
172
173    def listchildren(self):
174        "Return sequenced classes and functions in the module."
175        dir, base = os.path.split(self.file)
176        name, ext = os.path.splitext(base)
177        if os.path.normcase(ext) != ".py":
178            return []
179        try:
180            tree = pyclbr.readmodule_ex(name, [dir] + sys.path)
181        except ImportError:
182            return []
183        return transform_children(tree, name)
184
185
186class ChildBrowserTreeItem(TreeItem):
187    """Browser tree for child nodes within the module.
188
189    Uses TreeItem as the basis for the structure of the tree.
190    """
191
192    def __init__(self, obj):
193        "Create a TreeItem for a pyclbr class/function object."
194        self.obj = obj
195        self.name = obj.name
196        self.isfunction = isinstance(obj, pyclbr.Function)
197
198    def GetText(self):
199        "Return the name of the function/class to display."
200        name = self.name
201        if self.isfunction:
202            return "def " + name + "(...)"
203        else:
204            return "class " + name
205
206    def GetIconName(self):
207        "Return the name of the icon to display."
208        if self.isfunction:
209            return "python"
210        else:
211            return "folder"
212
213    def IsExpandable(self):
214        "Return True if self.obj has nested objects."
215        return self.obj.children != {}
216
217    def GetSubList(self):
218        "Return ChildBrowserTreeItems for children."
219        return [ChildBrowserTreeItem(obj)
220                for obj in transform_children(self.obj.children)]
221
222    def OnDoubleClick(self):
223        "Open module with file_open and position to lineno."
224        try:
225            edit = file_open(self.obj.file)
226            edit.gotoline(self.obj.lineno)
227        except (OSError, AttributeError):
228            pass
229
230
231def _module_browser(parent): # htest #
232    if len(sys.argv) > 1:  # If pass file on command line.
233        file = sys.argv[1]
234    else:
235        file = __file__
236        # Add nested objects for htest.
237        class Nested_in_func(TreeNode):
238            def nested_in_class(): pass
239        def closure():
240            class Nested_in_closure: pass
241    ModuleBrowser(parent, file, _htest=True)
242
243if __name__ == "__main__":
244    if len(sys.argv) == 1:  # If pass file on command line, unittest fails.
245        from unittest import main
246        main('idlelib.idle_test.test_browser', verbosity=2, exit=False)
247    from idlelib.idle_test.htest import run
248    run(_module_browser)
249