1"""Class 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- show function argument list? (have to do pattern matching on source)
9- should the classes and methods lists also be in the module's menu bar?
10- add base classes to class browser tree
11"""
12
13import os
14import sys
15import pyclbr
16
17from idlelib import PyShell
18from idlelib.WindowList import ListedToplevel
19from idlelib.TreeWidget import TreeNode, TreeItem, ScrolledCanvas
20from idlelib.configHandler import idleConf
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
25class ClassBrowser:
26
27    def __init__(self, flist, name, path, _htest=False):
28        # XXX This API should change, if the file doesn't end in ".py"
29        # XXX the code here is bogus!
30        """
31        _htest - bool, change box when location running htest.
32        """
33        global file_open
34        if not _htest:
35            file_open = PyShell.flist.open
36        self.name = name
37        self.file = os.path.join(path[0], self.name + ".py")
38        self._htest = _htest
39        self.init(flist)
40
41    def close(self, event=None):
42        self.top.destroy()
43        self.node.destroy()
44
45    def init(self, flist):
46        self.flist = flist
47        # reset pyclbr
48        pyclbr._modules.clear()
49        # create top
50        self.top = top = ListedToplevel(flist.root)
51        top.protocol("WM_DELETE_WINDOW", self.close)
52        top.bind("<Escape>", self.close)
53        if self._htest: # place dialog below parent if running htest
54            top.geometry("+%d+%d" %
55                (flist.root.winfo_rootx(), flist.root.winfo_rooty() + 200))
56        self.settitle()
57        top.focus_set()
58        # create scrolled canvas
59        theme = idleConf.CurrentTheme()
60        background = idleConf.GetHighlight(theme, 'normal')['background']
61        sc = ScrolledCanvas(top, bg=background, highlightthickness=0, takefocus=1)
62        sc.frame.pack(expand=1, fill="both")
63        item = self.rootnode()
64        self.node = node = TreeNode(sc.canvas, None, item)
65        node.update()
66        node.expand()
67
68    def settitle(self):
69        self.top.wm_title("Class Browser - " + self.name)
70        self.top.wm_iconname("Class Browser")
71
72    def rootnode(self):
73        return ModuleBrowserTreeItem(self.file)
74
75class ModuleBrowserTreeItem(TreeItem):
76
77    def __init__(self, file):
78        self.file = file
79
80    def GetText(self):
81        return os.path.basename(self.file)
82
83    def GetIconName(self):
84        return "python"
85
86    def GetSubList(self):
87        sublist = []
88        for name in self.listclasses():
89            item = ClassBrowserTreeItem(name, self.classes, self.file)
90            sublist.append(item)
91        return sublist
92
93    def OnDoubleClick(self):
94        if os.path.normcase(self.file[-3:]) != ".py":
95            return
96        if not os.path.exists(self.file):
97            return
98        PyShell.flist.open(self.file)
99
100    def IsExpandable(self):
101        return os.path.normcase(self.file[-3:]) == ".py"
102
103    def listclasses(self):
104        dir, file = os.path.split(self.file)
105        name, ext = os.path.splitext(file)
106        if os.path.normcase(ext) != ".py":
107            return []
108        try:
109            dict = pyclbr.readmodule_ex(name, [dir] + sys.path)
110        except ImportError:
111            return []
112        items = []
113        self.classes = {}
114        for key, cl in dict.items():
115            if cl.module == name:
116                s = key
117                if hasattr(cl, 'super') and cl.super:
118                    supers = []
119                    for sup in cl.super:
120                        if type(sup) is type(''):
121                            sname = sup
122                        else:
123                            sname = sup.name
124                            if sup.module != cl.module:
125                                sname = "%s.%s" % (sup.module, sname)
126                        supers.append(sname)
127                    s = s + "(%s)" % ", ".join(supers)
128                items.append((cl.lineno, s))
129                self.classes[s] = cl
130        items.sort()
131        list = []
132        for item, s in items:
133            list.append(s)
134        return list
135
136class ClassBrowserTreeItem(TreeItem):
137
138    def __init__(self, name, classes, file):
139        self.name = name
140        self.classes = classes
141        self.file = file
142        try:
143            self.cl = self.classes[self.name]
144        except (IndexError, KeyError):
145            self.cl = None
146        self.isfunction = isinstance(self.cl, pyclbr.Function)
147
148    def GetText(self):
149        if self.isfunction:
150            return "def " + self.name + "(...)"
151        else:
152            return "class " + self.name
153
154    def GetIconName(self):
155        if self.isfunction:
156            return "python"
157        else:
158            return "folder"
159
160    def IsExpandable(self):
161        if self.cl:
162            try:
163                return not not self.cl.methods
164            except AttributeError:
165                return False
166
167    def GetSubList(self):
168        if not self.cl:
169            return []
170        sublist = []
171        for name in self.listmethods():
172            item = MethodBrowserTreeItem(name, self.cl, self.file)
173            sublist.append(item)
174        return sublist
175
176    def OnDoubleClick(self):
177        if not os.path.exists(self.file):
178            return
179        edit = file_open(self.file)
180        if hasattr(self.cl, 'lineno'):
181            lineno = self.cl.lineno
182            edit.gotoline(lineno)
183
184    def listmethods(self):
185        if not self.cl:
186            return []
187        items = []
188        for name, lineno in self.cl.methods.items():
189            items.append((lineno, name))
190        items.sort()
191        list = []
192        for item, name in items:
193            list.append(name)
194        return list
195
196class MethodBrowserTreeItem(TreeItem):
197
198    def __init__(self, name, cl, file):
199        self.name = name
200        self.cl = cl
201        self.file = file
202
203    def GetText(self):
204        return "def " + self.name + "(...)"
205
206    def GetIconName(self):
207        return "python" # XXX
208
209    def IsExpandable(self):
210        return 0
211
212    def OnDoubleClick(self):
213        if not os.path.exists(self.file):
214            return
215        edit = file_open(self.file)
216        edit.gotoline(self.cl.methods[self.name])
217
218def _class_browser(parent): #Wrapper for htest
219    try:
220        file = __file__
221    except NameError:
222        file = sys.argv[0]
223        if sys.argv[1:]:
224            file = sys.argv[1]
225        else:
226            file = sys.argv[0]
227    dir, file = os.path.split(file)
228    name = os.path.splitext(file)[0]
229    flist = PyShell.PyShellFileList(parent)
230    global file_open
231    file_open = flist.open
232    ClassBrowser(flist, name, [dir], _htest=True)
233
234if __name__ == "__main__":
235    from idlelib.idle_test.htest import run
236    run(_class_browser)
237