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