Changeset 31

Show
Ignore:
Timestamp:
05/30/06 15:45:10 (7 years ago)
Author:
mk
Message:

Use the latest pydoctor (closes ticket #8).

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • branches/mk/cheesecake/model.py

    r11 r31  
    1 """ 
    2 -Code borrowed from Michael Hudson's docextractor package with the author's permission. 
    3 -The original code is available at <http://codespeak.net/svn/user/mwh/docextractor/> 
    4 """ 
    5  
    61from compiler import ast 
    72import sys 
     
    116import sets 
    127 
    13 import compiler 
    148from compiler.transformer import parse, parseFile 
    159from compiler.visitor import walk 
     10 
     11import ast_pp 
    1612 
    1713class Documentable(object): 
     
    4440            return self.parent.name2fullname(name) 
    4541 
     42    def resolveDottedName(self, dottedname, verbose=False): 
     43        parts = dottedname.split('.') 
     44        obj = self 
     45        system = self.system 
     46        while parts[0] not in obj._name2fullname: 
     47            obj = obj.parent 
     48            if obj is None: 
     49                if parts[0] in system.allobjects: 
     50                    obj = system.allobjects[parts[0]] 
     51                    break 
     52                if verbose: 
     53                    print "1 didn't find %r from %r"%(dottedname, 
     54                                                      self.fullName()) 
     55                return None 
     56        else: 
     57            fn = obj._name2fullname[parts[0]] 
     58            if fn in system.allobjects: 
     59                obj = system.allobjects[fn] 
     60            else: 
     61                if verbose: 
     62                    print "1.5 didn't find %r from %r"%(dottedname, 
     63                                                        self.fullName()) 
     64                return None 
     65        for p in parts[1:]: 
     66            if p not in obj.contents: 
     67                if verbose: 
     68                    print "2 didn't find %r from %r"%(dottedname, 
     69                                                      self.fullName()) 
     70                return None 
     71            obj = obj.contents[p] 
     72        if verbose: 
     73            print dottedname, '->', obj.fullName(), 'in', self.fullName() 
     74        return obj 
     75 
     76    def dottedNameToFullName(self, dottedname): 
     77        if '.' not in dottedname: 
     78            start, rest = dottedname, '' 
     79        else: 
     80            start, rest = dottedname.split('.', 1) 
     81            rest = '.' + rest 
     82        obj = self 
     83        while start not in obj._name2fullname: 
     84            obj = obj.parent 
     85            if obj is None: 
     86                return dottedname 
     87        return obj._name2fullname[start] + rest 
     88 
     89    def __getstate__(self): 
     90        # this is so very, very evil. 
     91        # see doc/extreme-pickling-pain.txt for more. 
     92        r = {} 
     93        for k, v in self.__dict__.iteritems(): 
     94            if isinstance(v, Documentable): 
     95                r['$'+k] = v.fullName() 
     96            elif isinstance(v, list) and v: 
     97                for vv in v: 
     98                    if vv is not None and not isinstance(vv, Documentable): 
     99                        r[k] = v 
     100                        break 
     101                else: 
     102                    rr = [] 
     103                    for vv in v: 
     104                        if vv is None: 
     105                            rr.append(vv) 
     106                        else: 
     107                            rr.append(vv.fullName()) 
     108                    r['@'+k] = rr 
     109            elif isinstance(v, dict) and v: 
     110                for vv in v.itervalues(): 
     111                    if not isinstance(vv, Documentable): 
     112                        r[k] = v 
     113                        break 
     114                else: 
     115                    rr = {} 
     116                    for kk, vv in v.iteritems(): 
     117                        rr[kk] = vv.fullName() 
     118                    r['!'+k] = rr 
     119            else: 
     120                r[k] = v 
     121        return r 
    46122 
    47123class Package(Documentable): 
     124    kind = "Package" 
    48125    def name2fullname(self, name): 
    49126        raise NameError 
    50127 
     128 
    51129class Module(Documentable): 
     130    kind = "Module" 
    52131    def name2fullname(self, name): 
    53132        if name in self._name2fullname: 
     
    59138            return name 
    60139 
     140 
    61141class Class(Documentable): 
     142    kind = "Class" 
    62143    def setup(self): 
    63144        super(Class, self).setup() 
    64145        self.bases = [] 
    65     def __repr__(self): 
    66         return "%s(%r, %r) # %r"%(self.__class__.__name__, 
    67                                   self.name, self.shortdocstring(), 
    68                                   self.bases) 
     146        self.rawbases = [] 
     147        self.baseobjects = [] 
     148        self.subclasses = [] 
     149 
    69150 
    70151class Function(Documentable): 
    71     pass 
     152    kind = "Function" 
     153 
     154 
     155class ModuleVistor(object): 
     156    def __init__(self, system, modname): 
     157        self.system = system 
     158        self.modname = modname 
     159        self.morenodes = [] 
     160 
     161    def default(self, node): 
     162        for child in node.getChildNodes(): 
     163            self.visit(child) 
     164 
     165    def postpone(self, docable, node): 
     166        self.morenodes.append((docable, node)) 
     167 
     168    def visitModule(self, node): 
     169        if self.system.current and self.modname in self.system.current.contents: 
     170            m = self.system.current.contents[self.modname] 
     171            assert m.docstring is None 
     172            m.docstring = node.doc 
     173            self.system.push(m) 
     174            self.default(node) 
     175            self.system.pop(m) 
     176        else: 
     177            if not self.system.current: 
     178                roots = [x for x in self.system.rootobjects if x.name == self.modname] 
     179                if roots: 
     180                    mod, = roots 
     181                    self.system.push(mod) 
     182                    self.default(node) 
     183                    self.system.pop(mod) 
     184                    return 
     185            self.system.pushModule(self.modname, node.doc) 
     186            self.default(node) 
     187            self.system.popModule() 
     188 
     189    def visitClass(self, node): 
     190        cls = self.system.pushClass(node.name, node.doc) 
     191        if node.lineno is not None: 
     192            cls.linenumber = node.lineno 
     193        for n in node.bases: 
     194            str_base = ast_pp.pp(n) 
     195            cls.rawbases.append(str_base) 
     196            base = cls.dottedNameToFullName(str_base) 
     197            cls.bases.append(base) 
     198        self.default(node) 
     199        self.system.popClass() 
     200 
     201    def visitFrom(self, node): 
     202        modname = expandModname(self.system, node.modname) 
     203        name2fullname = self.system.current._name2fullname 
     204        for fromname, asname in node.names: 
     205            if fromname == '*': 
     206                self.system.warning("import *", modname) 
     207                if modname not in self.system.allobjects: 
     208                    return 
     209                mod = self.system.allobjects[modname] 
     210                # this might fail if you have an import-* cycle, or if 
     211                # you're just not running the import star finder to 
     212                # save time (not that this is possibly without 
     213                # commenting stuff out yet, but...) 
     214                if isinstance(mod, Package): 
     215                    self.system.warning("import * from a package", modname) 
     216                    return 
     217                if mod.processed: 
     218                    for n in mod.contents: 
     219                        name2fullname[n] = modname + '.' + n 
     220                else: 
     221                    self.system.warning("unresolvable import *", modname) 
     222                return 
     223            if asname is None: 
     224                asname = fromname 
     225            name2fullname[asname] = modname + '.' + fromname 
     226 
     227    def visitImport(self, node): 
     228        name2fullname = self.system.current._name2fullname 
     229        for fromname, asname in node.names: 
     230            fullname = expandModname(self.system, fromname) 
     231            if asname is None: 
     232                asname = fromname.split('.', 1)[0] 
     233                # aaaaargh! python sucks. 
     234                parts = fullname.split('.') 
     235                for i, part in enumerate(fullname.split('.')[::-1]): 
     236                    if part == asname: 
     237                        fullname = '.'.join(parts[:len(parts)-i]) 
     238                        name2fullname[asname] = fullname 
     239                        break 
     240                else: 
     241                    name2fullname[asname] = '.'.join(parts) 
     242            else: 
     243                name2fullname[asname] = fullname 
     244 
     245    def visitFunction(self, node): 
     246        func = self.system.pushFunction(node.name, node.doc) 
     247        if node.lineno is not None: 
     248            func.linenumber = node.lineno 
     249        # ast.Function has a pretty lame representation of 
     250        # arguments. Let's convert it to a nice concise format 
     251        # somewhat like what inspect.getargspec returns 
     252        argnames = node.argnames[:] 
     253        kwname = starargname = None 
     254        if node.kwargs: 
     255            kwname = argnames.pop(-1) 
     256        if node.varargs: 
     257            starargname = argnames.pop(-1) 
     258        defaults = [] 
     259        for default in node.defaults: 
     260            try: 
     261                defaults.append(ast_pp.pp(default)) 
     262            except (KeyboardInterrupt, SystemExit): 
     263                raise 
     264            except Exception, e: 
     265                self.system.warning("unparseable default", "%s: %s %r"%(e.__class__.__name__, 
     266                                                                       e, default)) 
     267                defaults.append('???') 
     268        # argh, convert unpacked-arguments from tuples to lists, 
     269        # because that's what getargspec uses and the unit test 
     270        # compares it 
     271        argnames2 = [] 
     272        for argname in argnames: 
     273            if isinstance(argname, tuple): 
     274                argname = list(argname) 
     275            argnames2.append(argname) 
     276        func.argspec = (argnames2, starargname, kwname, tuple(defaults)) 
     277        self.postpone(func, node.code) 
     278        self.system.popFunction() 
     279 
     280states = [ 
     281    'blank', 
     282    'preparse', 
     283    'importstarred', 
     284    'parsed', 
     285    'finalized', 
     286    ] 
    72287 
    73288 
     
    77292    Package = Package 
    78293    Function = Function 
     294    ModuleVistor = ModuleVistor 
    79295 
    80296    def __init__(self): 
     
    85301        self.rootobjects = [] 
    86302        self.warnings = {} 
    87         # importstargraph contains edges {importedby:[imports]} but only 
     303        # importstargraph contains edges {importer:[imported]} but only 
    88304        # for import * statements 
    89305        self.importstargraph = {} 
    90         self.func_called = {} 
     306        self.state = 'blank' 
     307        self.packages = [] 
    91308 
    92309    def _push(self, cls, name, docstring): 
     
    128345        The default is that the second definition "wins". 
    129346        ''' 
     347        i = 0 
     348        fn = obj.fullName() 
     349        while (fn + ' ' + str(i)) in self.allobjects: 
     350            i += 1 
     351        prev = self.allobjects[obj.fullName()] 
     352        prev.name = obj.name + ' ' + str(i) 
     353        self.allobjects[prev.fullName()] = prev 
    130354        self.warning("duplicate", self.allobjects[obj.fullName()]) 
    131355        self.allobjects[obj.fullName()] = obj 
     
    141365        self.current = self.current.parent 
    142366 
    143     def push(self, obj, node=None): 
     367    def push(self, obj): 
    144368        self._stack.append(self.current) 
    145369        self.current = obj 
     
    159383        self._pop(self.Module) 
    160384 
    161     def pushFunction(self, name, docstring, func_called): 
    162         self.func_called.update(func_called) 
     385    def pushFunction(self, name, docstring): 
    163386        return self._push(self.Function, name, docstring) 
    164387    def popFunction(self): 
     
    204427 
    205428    def warning(self, type, detail): 
    206         fn = self.current.fullName() 
    207         #print fn, type, detail 
     429        if self.current is not None: 
     430            fn = self.current.fullName() 
     431        else: 
     432            fn = '<None>' 
     433        print fn, type, detail 
    208434        self.warnings.setdefault(type, []).append((fn, detail)) 
    209435 
     
    212438            if isinstance(o, cls): 
    213439                yield o 
     440 
     441    def finalStateComputations(self): 
     442        self.recordBasesAndSubclasses() 
     443 
     444    def recordBasesAndSubclasses(self): 
     445        for cls in self.objectsOfType(Class): 
     446            for n in cls.rawbases: 
     447                o = cls.parent.resolveDottedName(n) 
     448                cls.baseobjects.append(o) 
     449                if o: 
     450                    o.subclasses.append(cls) 
     451 
     452    def __setstate__(self, state): 
     453        # this is so very, very evil. 
     454        # see doc/extreme-pickling-pain.txt for more. 
     455        self.__dict__.update(state) 
     456        for obj in self.orderedallobjects: 
     457            for k, v in obj.__dict__.copy().iteritems(): 
     458                if k.startswith('$'): 
     459                    del obj.__dict__[k] 
     460                    obj.__dict__[k[1:]] = self.allobjects[v] 
     461                elif k.startswith('@'): 
     462                    n = [] 
     463                    for vv in v: 
     464                        if vv is None: 
     465                            n.append(None) 
     466                        else: 
     467                            n.append(self.allobjects[vv]) 
     468                    del obj.__dict__[k] 
     469                    obj.__dict__[k[1:]] = n 
     470                elif k.startswith('!'): 
     471                    n = {} 
     472                    for kk, vv in v.iteritems(): 
     473                        n[kk] = self.allobjects[vv] 
     474                    del obj.__dict__[k] 
     475                    obj.__dict__[k[1:]] = n 
     476 
    214477 
    215478def expandModname(system, modname, givewarning=True): 
     
    242505            modname = expandModname(self.system, node.modname, False) 
    243506            self.system.importstargraph.setdefault( 
    244                 modname, []).append(self.modfullname) 
    245  
    246 class ModuleVistor(object): 
    247     def __init__(self, system, modname): 
    248         self.system = system 
    249         self.modname = modname 
    250         self.morenodes = [] 
    251  
    252     def default(self, node): 
    253         for child in node.getChildNodes(): 
    254             self.visit(child) 
    255  
    256     def postpone(self, docable, node): 
    257         self.morenodes.append((docable, node)) 
    258  
    259     def visitModule(self, node): 
    260         if self.system.current and self.modname in self.system.current.contents: 
    261             m = self.system.current.contents[self.modname] 
    262             assert m.docstring is None 
    263             m.docstring = node.doc 
    264             self.system.push(m, node) 
    265             self.default(node) 
    266             self.system.pop(m) 
    267         else: 
    268             self.system.pushModule(self.modname, node.doc) 
    269             self.default(node) 
    270             self.system.popModule() 
    271  
    272     def visitClass(self, node): 
    273         cls = self.system.pushClass(node.name, node.doc) 
    274         for n in node.bases: 
    275             if isinstance(n, ast.Name): 
    276                 cls.bases.append(cls.parent.name2fullname(n.name)) 
    277             elif isinstance(n, ast.Getattr): 
    278                 p = [] 
    279                 while isinstance(n, ast.Getattr): 
    280                     p.append(n.attrname) 
    281                     n = n.expr 
    282                 assert isinstance(n, ast.Name) 
    283                 p.append(cls.parent.name2fullname(n.name)) 
    284                 p.reverse() 
    285                 assert None not in p, n 
    286                 cls.bases.append('.'.join(p)) 
    287             else: 
    288                 assert not n 
    289         self.default(node) 
    290         self.system.popClass() 
    291  
    292     def visitFrom(self, node): 
    293         modname = expandModname(self.system, node.modname) 
    294         name2fullname = self.system.current._name2fullname 
    295         for fromname, asname in node.names: 
    296             if fromname == '*': 
    297                 self.system.warning("import *", modname) 
    298                 if modname not in self.system.allobjects: 
    299                     return 
    300                 mod = self.system.allobjects[modname] 
    301                 #snarl (see below) 
    302                 #assert mod.processed 
    303                 self.system.warning("mwh is an idiot", "") 
    304                 for n in mod.contents: 
    305                     name2fullname[n] = modname + '.' + n 
    306                 return 
    307             if asname is None: 
    308                 asname = fromname 
    309             name2fullname[asname] = modname + '.' + fromname 
    310  
    311     def visitImport(self, node): 
    312         name2fullname = self.system.current._name2fullname 
    313         for fromname, asname in node.names: 
    314             fullname = expandModname(self.system, fromname) 
    315             if asname is None: 
    316                 asname = fromname.split('.', 1)[0] 
    317                 # aaaaargh! python sucks. 
    318                 parts = fullname.split('.') 
    319                 for i, part in enumerate(fullname.split('.')[::-1]): 
    320                     if part == asname: 
    321                         fullname = '.'.join(parts[:len(parts)-i]) 
    322                         name2fullname[asname] = fullname 
    323                         break 
    324                 else: 
    325                     name2fullname[asname] = '.'.join(parts) 
    326             else: 
    327                 name2fullname[asname] = fullname 
    328  
    329  
    330     def visitFunction(self, node): 
    331         fc = {} 
    332         get_function_calls(node, fc) 
    333         #print fc.keys() 
    334         func = self.system.pushFunction(node.name, node.doc, fc) 
    335         # ast.Function has a pretty lame representation of 
    336         # arguments. Let's convert it to a nice concise 
    337         # getargspec-like format and include it in the Function 
    338         # object. 
    339         argnames = node.argnames[:] 
    340         kwname = starargname = None 
    341         if node.kwargs: 
    342             kwname = argnames.pop(-1) 
    343         if node.varargs: 
    344             starargname = argnames.pop(-1) 
    345         defaults = [] 
    346         for default in node.defaults: 
    347             if isinstance(default, ast.Const): 
    348                 defaults.append(default.value) 
    349             elif isinstance(default, ast.Name): 
    350                 defaults.append(default.name) 
    351             else: 
    352                 self.system.warning("unparseable default", repr(default)) 
    353                 defaults.append('???') 
    354                 #assert False, "don't know how to handle default %r"%(default,) 
    355         # argh, convert unpacked-arguments from tuples to lists, 
    356         # because that's what getargspec uses and the unit test 
    357         # compares it 
    358         argnames2 = [] 
    359         for argname in argnames: 
    360             if isinstance(argname, tuple): 
    361                 argname = list(argname) 
    362             argnames2.append(argname) 
    363         func.argspec = (argnames2, starargname, kwname, tuple(defaults)) 
    364         #for child in node.getChildren(): 
    365         #    if isinstance(child, compiler.ast.Stmt): 
    366         #        for c in child.getChildren(): 
    367         #            print c.__class__ 
    368         #            print c 
    369         self.postpone(func, node.code) 
    370         self.system.popFunction() 
    371  
    372 def get_function_calls(node, fc): 
    373     if not isinstance(node, compiler.ast.Node): 
    374         return 
    375     for child in node.getChildren(): 
    376         #print "child:", child 
    377         if isinstance(child, compiler.ast.CallFunc): 
    378                 funcname = "" 
    379                 attrname = "" 
    380                 n = child.node 
    381                 #print "n:", n 
    382                 #print n.__class__ 
    383                 if isinstance(n, compiler.ast.Getattr): 
    384                     expr = n.expr 
    385                     if isinstance(expr, compiler.ast.Name): 
    386                         funcname = expr.name 
    387                     attrname = n.attrname 
    388                 func_called = "" 
    389                 if funcname: func_called = funcname + "." 
    390                 func_called += attrname 
    391                 if func_called: 
    392                     fc[func_called] = 1 
    393         get_function_calls(child, fc) 
    394      
     507                self.modfullname, []).append(modname) 
     508 
    395509def processModuleAst(ast, name, system): 
    396     mv = ModuleVistor(system, name) 
     510    mv = system.ModuleVistor(system, name) 
    397511    walk(ast, mv) 
    398512    while mv.morenodes: 
    399513        obj, node = mv.morenodes.pop(0) 
    400         system.push(obj, node
     514        system.push(obj
    401515        mv.visit(node) 
    402516        system.pop(obj) 
     
    405519def fromText(src, modname='<test>', system=None): 
    406520    if system is None: 
    407         system = System() 
    408     processModuleAst(parse(src), modname, system) 
    409     return system.rootobjects[0] 
     521        _system = System() 
     522    else: 
     523        _system = system 
     524    processModuleAst(parse(src), modname, _system) 
     525    if system is None: 
     526        _system.finalStateComputations() 
     527    return _system.rootobjects[0] 
    410528 
    411529 
    412530def preprocessDirectory(system, dirpath): 
    413     package = system.pushPackage(os.path.basename(dirpath), None) 
     531    assert system.state in ['blank', 'preparse'] 
     532    if os.path.basename(dirpath): 
     533        package = system.pushPackage(os.path.basename(dirpath), None) 
     534    else: 
     535        package = None 
    414536    for fname in os.listdir(dirpath): 
    415537        fullname = os.path.join(dirpath, fname) 
    416         if os.path.isdir(fullname) and os.path.exists(os.path.join(fullname, '__init__.py'))
     538        if os.path.isdir(fullname) and os.path.exists(os.path.join(fullname, '__init__.py')) and fname != 'test'
    417539            preprocessDirectory(system, fullname) 
    418540        elif fname.endswith('.py'): 
     
    422544            mod.processed = False 
    423545            system.popModule() 
    424     system.popPackage() 
    425  
    426 def processDirectory(system, dirpath): 
    427     preprocessDirectory(system, dirpath) 
     546    if package: 
     547        system.popPackage() 
     548    system.state = 'preparse' 
     549 
     550def findImportStars(system): 
     551    assert system.state in ['preparse'] 
    428552    modlist = list(system.objectsOfType(Module)) 
    429553    for mod in modlist: 
    430554        system.push(mod.parent) 
    431555        isf = ImportStarFinder(system, mod.fullName()) 
    432         walk(parseFile(mod.filepath), isf) 
     556        try: 
     557            ast = parseFile(mod.filepath) 
     558        except (SyntaxError, ValueError): 
     559            system.warning("cannot parse", mod.filepath) 
     560        walk(ast, isf) 
    433561        system.pop(mod.parent) 
    434  
    435     # snarl; a toposort is meant to go here. 
    436     newlist = modlist 
     562    system.state = 'importstarred' 
     563 
     564def extractDocstrings(system): 
     565    assert system.state in ['preparse', 'importstarred'] 
     566    # and so much more... 
     567    modlist = list(system.objectsOfType(Module)) 
     568    newlist = toposort([m.fullName() for m in modlist], system.importstargraph) 
    437569 
    438570    for mod in newlist: 
     571        mod = system.allobjects[mod] 
    439572        system.push(mod.parent) 
    440         processModuleAst(parseFile(mod.filepath), mod.name, system) 
     573        try: 
     574            ast = parseFile(mod.filepath) 
     575        except (SyntaxError, ValueError): 
     576            system.warning("cannot parse", mod.filepath) 
     577        processModuleAst(ast, mod.name, system) 
    441578        mod.processed = True 
    442579        system.pop(mod.parent) 
    443  
    444 def main(argv): 
     580    system.state = 'parsed' 
     581 
     582def finalStateComputations(system): 
     583    assert system.state in ['parsed'] 
     584    system.finalStateComputations() 
     585    system.state = 'finalized' 
     586 
     587def processDirectory(system, dirpath): 
     588    preprocessDirectory(system, dirpath) 
     589    findImportStars(system) 
     590    extractDocstrings(system) 
     591    finalStateComputations(system) 
     592 
     593def toposort(input, edges): 
     594    # this doesn't detect cycles in any clever way. 
     595    output = [] 
     596    input = dict.fromkeys(input) 
     597    def p(i): 
     598        for j in edges.get(i, []): 
     599            if j in input: 
     600                del input[j] 
     601                p(j) 
     602        output.append(i) 
     603    while input: 
     604        p(input.popitem()[0]) 
     605    return output 
     606 
     607 
     608def main(systemcls, argv): 
    445609    if '-r' in argv: 
    446610        argv.remove('-r') 
    447611        assert len(argv) == 1 
    448         system = System() 
     612        system = systemcls() 
    449613        processDirectory(system, argv[0]) 
    450614        pickle.dump(system, open('da.out', 'wb'), pickle.HIGHEST_PROTOCOL) 
     
    454618            print k, len(v) 
    455619    else: 
    456         system = System() 
     620        system = systemcls() 
    457621        for fname in argv: 
    458622            modname = os.path.splitext(os.path.basename(fname))[0] # XXX! 
     
    463627 
    464628if __name__ == '__main__': 
    465     main(sys.argv[1:]) 
     629    main(System, sys.argv[1:])