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