root/branches/mk/cheesecake/model.py

Revision 31, 20.4 kB (checked in by mk, 7 years ago)

Use the latest pydoctor (closes ticket #8).

Line 
1 from compiler import ast
2 import sys
3 import os
4 import cPickle as pickle
5 import __builtin__
6 import sets
7
8 from compiler.transformer import parse, parseFile
9 from compiler.visitor import walk
10
11 import ast_pp
12
13 class Documentable(object):
14     def __init__(self, system, prefix, name, docstring, parent=None):
15         self.system = system
16         self.prefix = prefix
17         self.name = name
18         self.docstring = docstring
19         self.parent = parent
20         self.setup()
21     def setup(self):
22         self.contents = {}
23         self.orderedcontents = []
24         self._name2fullname = {}
25     def fullName(self):
26         return self.prefix + self.name
27     def shortdocstring(self):
28         docstring = self.docstring
29         if docstring:
30             docstring = docstring.rstrip()
31             if len(docstring) > 20:
32                 docstring = docstring[:8] + '...' + docstring[-8:]
33         return docstring
34     def __repr__(self):
35         return "%s %r"%(self.__class__.__name__, self.fullName())
36     def name2fullname(self, name):
37         if name in self._name2fullname:
38             return self._name2fullname[name]
39         else:
40             return self.parent.name2fullname(name)
41
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
122
123 class Package(Documentable):
124     kind = "Package"
125     def name2fullname(self, name):
126         raise NameError
127
128
129 class Module(Documentable):
130     kind = "Module"
131     def name2fullname(self, name):
132         if name in self._name2fullname:
133             return self._name2fullname[name]
134         elif name in __builtin__.__dict__:
135             return name
136         else:
137             self.system.warning("optimistic name resolution", name)
138             return name
139
140
141 class Class(Documentable):
142     kind = "Class"
143     def setup(self):
144         super(Class, self).setup()
145         self.bases = []
146         self.rawbases = []
147         self.baseobjects = []
148         self.subclasses = []
149
150
151 class Function(Documentable):
152     kind = "Function"
153
154
155 class 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
280 states = [
281     'blank',
282     'preparse',
283     'importstarred',
284     'parsed',
285     'finalized',
286     ]
287
288
289 class System(object):
290     Class = Class
291     Module = Module
292     Package = Package
293     Function = Function
294     ModuleVistor = ModuleVistor
295
296     def __init__(self):
297         self.current = None
298         self._stack = []
299         self.allobjects = {}
300         self.orderedallobjects = []
301         self.rootobjects = []
302         self.warnings = {}
303         # importstargraph contains edges {importer:[imported]} but only
304         # for import * statements
305         self.importstargraph = {}
306         self.state = 'blank'
307         self.packages = []
308
309     def _push(self, cls, name, docstring):
310         if self.current:
311             prefix = self.current.fullName() + '.'
312             parent = self.current
313         else:
314             prefix = ''
315             parent = None
316         obj = cls(self, prefix, name, docstring, parent)
317         if parent:
318             parent.orderedcontents.append(obj)
319             parent.contents[name] = obj
320             parent._name2fullname[name] = obj.fullName()
321         else:
322             self.rootobjects.append(obj)
323         self.current = obj
324         self.orderedallobjects.append(obj)
325         fullName = obj.fullName()
326         #print 'push', cls.__name__, fullName
327         if fullName in self.allobjects:
328             obj = self.handleDuplicate(obj)
329         else:
330             self.allobjects[obj.fullName()] = obj
331         return obj
332
333     def handleDuplicate(self, obj):
334         '''This is called when we see two objects with the same
335         .fullName(), for example:
336
337         class C:
338             if something:
339                 def meth(self):
340                     implementation 1
341             else:
342                 def meth(self):
343                     implementation 2
344
345         The default is that the second definition "wins".
346         '''
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
354         self.warning("duplicate", self.allobjects[obj.fullName()])
355         self.allobjects[obj.fullName()] = obj
356         return obj
357
358
359     def _pop(self, cls):
360         assert isinstance(self.current, cls)
361 ##         if self.current.parent:
362 ##             print 'pop', self.current.fullName(), '->', self.current.parent.fullName()
363 ##         else:
364 ##             print 'pop', self.current.fullName(), '->', self.current.parent
365         self.current = self.current.parent
366
367     def push(self, obj):
368         self._stack.append(self.current)
369         self.current = obj
370
371     def pop(self, obj):
372         assert self.current is obj, "%r is not %r"%(self.current, obj)
373         self.current = self._stack.pop()
374
375     def pushClass(self, name, docstring):
376         return self._push(self.Class, name, docstring)
377     def popClass(self):
378         self._pop(self.Class)
379
380     def pushModule(self, name, docstring):
381         return self._push(self.Module, name, docstring)
382     def popModule(self):
383         self._pop(self.Module)
384
385     def pushFunction(self, name, docstring):
386         return self._push(self.Function, name, docstring)
387     def popFunction(self):
388         self._pop(self.Function)
389
390     def pushPackage(self, name, docstring):
391         return self._push(self.Package, name, docstring)
392     def popPackage(self):
393         self._pop(self.Package)
394
395     def report(self):
396         for o in self.rootobjects:
397             self._report(o, '')
398
399     def _report(self, o, indent):
400         print indent, o
401         for o2 in o.orderedcontents:
402             self._report(o2, indent+'  ')
403
404     def resolveAlias(self, n):
405         if '.' not in n:
406             return n
407         mod, clsname = n.split('.')
408         if not mod or mod not in self.allobjects:
409             return n
410         m = self.allobjects[mod]
411         if not isinstance(m, Module):
412             return n
413         if clsname in m._name2fullname:
414             newname = m.name2fullname(clsname)
415             if newname not in self.allobjects:
416                 return self.resolveAlias(newname)
417             else:
418                 return newname
419
420     def resolveAliases(self):
421         for ob in self.orderedallobjects:
422             if not isinstance(ob, Class):
423                 continue
424             for i, b in enumerate(ob.bases):
425                 if b not in self.allobjects:
426                     ob.bases[i] = self.resolveAlias(b)
427
428     def warning(self, type, detail):
429         if self.current is not None:
430             fn = self.current.fullName()
431         else:
432             fn = '<None>'
433         print fn, type, detail
434         self.warnings.setdefault(type, []).append((fn, detail))
435
436     def objectsOfType(self, cls):
437         for o in self.orderedallobjects:
438             if isinstance(o, cls):
439                 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
477
478 def expandModname(system, modname, givewarning=True):
479     c = system.current
480     if '.' in modname:
481         prefix, suffix = modname.split('.', 1)
482         suffix = '.' + suffix
483     else:
484         prefix, suffix = modname, ''
485     while c is not None and not isinstance(c, Package):
486         c = c.parent
487     while c is not None:
488         if prefix in c.contents:
489             break
490         c = c.parent
491     if c is not None:
492         if givewarning:
493             system.warning("local import", modname)
494         return c.contents[prefix].fullName() + suffix
495     else:
496         return prefix + suffix
497
498 class ImportStarFinder(object):
499     def __init__(self, system, modfullname):
500         self.system = system
501         self.modfullname = modfullname
502
503     def visitFrom(self, node):
504         if node.names[0][0] == '*':
505             modname = expandModname(self.system, node.modname, False)
506             self.system.importstargraph.setdefault(
507                 self.modfullname, []).append(modname)
508
509 def processModuleAst(ast, name, system):
510     mv = system.ModuleVistor(system, name)
511     walk(ast, mv)
512     while mv.morenodes:
513         obj, node = mv.morenodes.pop(0)
514         system.push(obj)
515         mv.visit(node)
516         system.pop(obj)
517
518
519 def fromText(src, modname='<test>', system=None):
520     if system is None:
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]
528
529
530 def preprocessDirectory(system, dirpath):
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
536     for fname in os.listdir(dirpath):
537         fullname = os.path.join(dirpath, fname)
538         if os.path.isdir(fullname) and os.path.exists(os.path.join(fullname, '__init__.py')) and fname != 'test':
539             preprocessDirectory(system, fullname)
540         elif fname.endswith('.py'):
541             modname = os.path.splitext(fname)[0]
542             mod = system.pushModule(modname, None)
543             mod.filepath = fullname
544             mod.processed = False
545             system.popModule()
546     if package:
547         system.popPackage()
548     system.state = 'preparse'
549
550 def findImportStars(system):
551     assert system.state in ['preparse']
552     modlist = list(system.objectsOfType(Module))
553     for mod in modlist:
554         system.push(mod.parent)
555         isf = ImportStarFinder(system, mod.fullName())
556         try:
557             ast = parseFile(mod.filepath)
558         except (SyntaxError, ValueError):
559             system.warning("cannot parse", mod.filepath)
560         walk(ast, isf)
561         system.pop(mod.parent)
562     system.state = 'importstarred'
563
564 def 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)
569
570     for mod in newlist:
571         mod = system.allobjects[mod]
572         system.push(mod.parent)
573         try:
574             ast = parseFile(mod.filepath)
575         except (SyntaxError, ValueError):
576             system.warning("cannot parse", mod.filepath)
577         processModuleAst(ast, mod.name, system)
578         mod.processed = True
579         system.pop(mod.parent)
580     system.state = 'parsed'
581
582 def finalStateComputations(system):
583     assert system.state in ['parsed']
584     system.finalStateComputations()
585     system.state = 'finalized'
586
587 def processDirectory(system, dirpath):
588     preprocessDirectory(system, dirpath)
589     findImportStars(system)
590     extractDocstrings(system)
591     finalStateComputations(system)
592
593 def 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
608 def main(systemcls, argv):
609     if '-r' in argv:
610         argv.remove('-r')
611         assert len(argv) == 1
612         system = systemcls()
613         processDirectory(system, argv[0])
614         pickle.dump(system, open('da.out', 'wb'), pickle.HIGHEST_PROTOCOL)
615         print
616         print 'warning summary:'
617         for k, v in system.warnings.iteritems():
618             print k, len(v)
619     else:
620         system = systemcls()
621         for fname in argv:
622             modname = os.path.splitext(os.path.basename(fname))[0] # XXX!
623             processModuleAst(parseFile(fname), modname, system)
624         system.report()
625
626
627
628 if __name__ == '__main__':
629     main(System, sys.argv[1:])
Note: See TracBrowser for help on using the browser.