ClassBrowser.py revision 7aced17437a6b05bc4b0b5ff93aa6a5d3a374d68
17aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer"""Class browser.
27aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
37aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David SchererXXX TO DO:
47aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
57aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer- reparse when source changed (maybe just a button would be OK?)
67aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    (or recheck on window popup)
77aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer- add popup menu with more options (e.g. doc strings, base classes, imports)
87aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer- show function argument list? (have to do pattern matching on source)
97aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer- should the classes and methods lists also be in the module's menu bar?
107aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer- add base classes to class browser tree
117aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer"""
127aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
137aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Schererimport os
147aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Schererimport sys
157aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Schererimport string
167aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Schererimport pyclbr
177aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
187aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer# XXX Patch pyclbr with dummies if it's vintage Python 1.5.2:
197aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Schererif not hasattr(pyclbr, "readmodule_ex"):
207aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    pyclbr.readmodule_ex = pyclbr.readmodule
217aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Schererif not hasattr(pyclbr, "Function"):
227aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    class Function(pyclbr.Class):
237aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        pass
247aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    pyclbr.Function = Function
257aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
267aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Schererimport PyShell
277aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Schererfrom WindowList import ListedToplevel
287aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Schererfrom TreeWidget import TreeNode, TreeItem, ScrolledCanvas
297aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
307aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Schererclass ClassBrowser:
317aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
327aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def __init__(self, flist, name, path):
337aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        # XXX This API should change, if the file doesn't end in ".py"
347aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        # XXX the code here is bogus!
357aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.name = name
367aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.file = os.path.join(path[0], self.name + ".py")
377aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.init(flist)
387aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
397aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def close(self, event=None):
407aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.top.destroy()
417aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.node.destroy()
427aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
437aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def init(self, flist):
447aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.flist = flist
457aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        # reset pyclbr
467aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        pyclbr._modules.clear()
477aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        # create top
487aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.top = top = ListedToplevel(flist.root)
497aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        top.protocol("WM_DELETE_WINDOW", self.close)
507aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        top.bind("<Escape>", self.close)
517aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.settitle()
527aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        top.focus_set()
537aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        # create scrolled canvas
547aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        sc = ScrolledCanvas(top, bg="white", highlightthickness=0, takefocus=1)
557aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        sc.frame.pack(expand=1, fill="both")
567aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        item = self.rootnode()
577aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.node = node = TreeNode(sc.canvas, None, item)
587aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        node.update()
597aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        node.expand()
607aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
617aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def settitle(self):
627aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.top.wm_title("Class Browser - " + self.name)
637aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.top.wm_iconname("Class Browser")
647aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
657aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def rootnode(self):
667aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        return ModuleBrowserTreeItem(self.file)
677aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
687aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Schererclass ModuleBrowserTreeItem(TreeItem):
697aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
707aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def __init__(self, file):
717aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.file = file
727aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
737aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def GetText(self):
747aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        return os.path.basename(self.file)
757aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
767aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def GetIconName(self):
777aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        return "python"
787aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
797aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def GetSubList(self):
807aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        sublist = []
817aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        for name in self.listclasses():
827aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            item = ClassBrowserTreeItem(name, self.classes, self.file)
837aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            sublist.append(item)
847aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        return sublist
857aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
867aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def OnDoubleClick(self):
877aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        if os.path.normcase(self.file[-3:]) != ".py":
887aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            return
897aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        if not os.path.exists(self.file):
907aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            return
917aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        PyShell.flist.open(self.file)
927aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
937aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def IsExpandable(self):
947aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        return os.path.normcase(self.file[-3:]) == ".py"
957aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
967aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def listclasses(self):
977aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        dir, file = os.path.split(self.file)
987aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        name, ext = os.path.splitext(file)
997aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        if os.path.normcase(ext) != ".py":
1007aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            return []
1017aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        try:
1027aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            dict = pyclbr.readmodule_ex(name, [dir] + sys.path)
1037aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        except ImportError, msg:
1047aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            return []
1057aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        items = []
1067aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.classes = {}
1077aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        for key, cl in dict.items():
1087aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            if cl.module == name:
1097aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer                s = key
1107aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer                if cl.super:
1117aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer                    supers = []
1127aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer                    for sup in cl.super:
1137aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer                        if type(sup) is type(''):
1147aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer                            sname = sup
1157aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer                        else:
1167aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer                            sname = sup.name
1177aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer                            if sup.module != cl.module:
1187aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer                                sname = "%s.%s" % (sup.module, sname)
1197aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer                        supers.append(sname)
1207aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer                    s = s + "(%s)" % string.join(supers, ", ")
1217aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer                items.append((cl.lineno, s))
1227aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer                self.classes[s] = cl
1237aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        items.sort()
1247aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        list = []
1257aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        for item, s in items:
1267aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            list.append(s)
1277aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        return list
1287aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
1297aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Schererclass ClassBrowserTreeItem(TreeItem):
1307aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
1317aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def __init__(self, name, classes, file):
1327aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.name = name
1337aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.classes = classes
1347aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.file = file
1357aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        try:
1367aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            self.cl = self.classes[self.name]
1377aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        except (IndexError, KeyError):
1387aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            self.cl = None
1397aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.isfunction = isinstance(self.cl, pyclbr.Function)
1407aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
1417aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def GetText(self):
1427aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        if self.isfunction:
1437aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            return "def " + self.name + "(...)"
1447aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        else:
1457aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            return "class " + self.name
1467aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
1477aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def GetIconName(self):
1487aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        if self.isfunction:
1497aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            return "python"
1507aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        else:
1517aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            return "folder"
1527aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
1537aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def IsExpandable(self):
1547aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        if self.cl:
1557aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            return not not self.cl.methods
1567aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
1577aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def GetSubList(self):
1587aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        if not self.cl:
1597aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            return []
1607aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        sublist = []
1617aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        for name in self.listmethods():
1627aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            item = MethodBrowserTreeItem(name, self.cl, self.file)
1637aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            sublist.append(item)
1647aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        return sublist
1657aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
1667aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def OnDoubleClick(self):
1677aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        if not os.path.exists(self.file):
1687aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            return
1697aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        edit = PyShell.flist.open(self.file)
1707aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        if hasattr(self.cl, 'lineno'):
1717aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            lineno = self.cl.lineno
1727aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            edit.gotoline(lineno)
1737aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
1747aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def listmethods(self):
1757aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        if not self.cl:
1767aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            return []
1777aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        items = []
1787aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        for name, lineno in self.cl.methods.items():
1797aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            items.append((lineno, name))
1807aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        items.sort()
1817aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        list = []
1827aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        for item, name in items:
1837aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            list.append(name)
1847aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        return list
1857aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
1867aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Schererclass MethodBrowserTreeItem(TreeItem):
1877aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
1887aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def __init__(self, name, cl, file):
1897aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.name = name
1907aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.cl = cl
1917aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        self.file = file
1927aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
1937aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def GetText(self):
1947aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        return "def " + self.name + "(...)"
1957aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
1967aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def GetIconName(self):
1977aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        return "python" # XXX
1987aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
1997aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def IsExpandable(self):
2007aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        return 0
2017aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
2027aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    def OnDoubleClick(self):
2037aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        if not os.path.exists(self.file):
2047aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            return
2057aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        edit = PyShell.flist.open(self.file)
2067aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        edit.gotoline(self.cl.methods[self.name])
2077aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
2087aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Schererdef main():
2097aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    try:
2107aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        file = __file__
2117aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    except NameError:
2127aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        file = sys.argv[0]
2137aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        if sys.argv[1:]:
2147aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            file = sys.argv[1]
2157aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        else:
2167aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer            file = sys.argv[0]
2177aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    dir, file = os.path.split(file)
2187aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    name = os.path.splitext(file)[0]
2197aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    ClassBrowser(PyShell.flist, name, [dir])
2207aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    if sys.stdin is sys.__stdin__:
2217aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer        mainloop()
2227aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer
2237aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Schererif __name__ == "__main__":
2247aced17437a6b05bc4b0b5ff93aa6a5d3a374d68David Scherer    main()
225