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