525 lines
20 KiB
Python
525 lines
20 KiB
Python
|
|
||
|
""" Generating ReST output (raw, not python)
|
||
|
out of data that we know about function calls
|
||
|
"""
|
||
|
|
||
|
import py
|
||
|
import sys
|
||
|
import re
|
||
|
|
||
|
from py.__.apigen.tracer.docstorage import DocStorageAccessor
|
||
|
from py.__.rest.rst import * # XXX Maybe we should list it here
|
||
|
from py.__.apigen.tracer import model
|
||
|
from py.__.rest.transform import RestTransformer
|
||
|
|
||
|
def split_of_last_part(name):
|
||
|
name = name.split(".")
|
||
|
return ".".join(name[:-1]), name[-1]
|
||
|
|
||
|
class AbstractLinkWriter(object):
|
||
|
""" Class implementing writing links to source code.
|
||
|
There should exist various classes for that, different for Trac,
|
||
|
different for CVSView, etc.
|
||
|
"""
|
||
|
def getlinkobj(self, obj, name):
|
||
|
return None
|
||
|
|
||
|
def getlink(self, filename, lineno, funcname):
|
||
|
raise NotImplementedError("Abstract link writer")
|
||
|
|
||
|
def getpkgpath(self, filename):
|
||
|
# XXX: very simple thing
|
||
|
path = py.path.local(filename).dirpath()
|
||
|
while 1:
|
||
|
try:
|
||
|
path.join('__init__.py').stat()
|
||
|
path = path.dirpath()
|
||
|
except py.error.ENOENT:
|
||
|
return path
|
||
|
|
||
|
class ViewVC(AbstractLinkWriter):
|
||
|
""" Link writer for ViewVC version control viewer
|
||
|
"""
|
||
|
def __init__(self, basepath):
|
||
|
# XXX: should try to guess from a working copy of svn
|
||
|
self.basepath = basepath
|
||
|
|
||
|
def getlink(self, filename, lineno, funcname):
|
||
|
path = str(self.getpkgpath(filename))
|
||
|
assert filename.startswith(path), (
|
||
|
"%s does not belong to package %s" % (filename, path))
|
||
|
relname = filename[len(path):]
|
||
|
if relname.endswith('.pyc'):
|
||
|
relname = relname[:-1]
|
||
|
sep = py.std.os.sep
|
||
|
if sep != '/':
|
||
|
relname = relname.replace(sep, '/')
|
||
|
return ('%s:%s' % (filename, lineno),
|
||
|
self.basepath + relname[1:] + '?view=markup')
|
||
|
|
||
|
class SourceView(AbstractLinkWriter):
|
||
|
def __init__(self, baseurl):
|
||
|
self.baseurl = baseurl
|
||
|
if self.baseurl.endswith("/"):
|
||
|
self.baseurl = baseurl[:-1]
|
||
|
|
||
|
def getlink(self, filename, lineno, funcname):
|
||
|
if filename.endswith('.pyc'):
|
||
|
filename = filename[:-1]
|
||
|
if filename is None:
|
||
|
return "<UNKNOWN>:%s" % funcname,""
|
||
|
pkgpath = self.getpkgpath(filename)
|
||
|
if not filename.startswith(str(pkgpath)):
|
||
|
# let's leave it
|
||
|
return "<UNKNOWN>:%s" % funcname,""
|
||
|
|
||
|
relname = filename[len(str(pkgpath)):]
|
||
|
if relname.endswith('.pyc'):
|
||
|
relname = relname[:-1]
|
||
|
sep = py.std.os.sep
|
||
|
if sep != '/':
|
||
|
relname = relname.replace(sep, '/')
|
||
|
return "%s:%s" % (relname, funcname),\
|
||
|
"%s%s#%s" % (self.baseurl, relname, funcname)
|
||
|
|
||
|
def getlinkobj(self, name, obj):
|
||
|
try:
|
||
|
filename = sys.modules[obj.__module__].__file__
|
||
|
return self.getlink(filename, 0, name)
|
||
|
except AttributeError:
|
||
|
return None
|
||
|
|
||
|
class DirectPaste(AbstractLinkWriter):
|
||
|
""" No-link writer (inliner)
|
||
|
"""
|
||
|
def getlink(self, filename, lineno, funcname):
|
||
|
return ('%s:%s' % (filename, lineno), "")
|
||
|
|
||
|
class DirectFS(AbstractLinkWriter):
|
||
|
""" Creates links to the files on the file system (for local docs)
|
||
|
"""
|
||
|
def getlink(self, filename, lineno, funcname):
|
||
|
return ('%s:%s' % (filename, lineno), 'file://%s' % (filename,))
|
||
|
|
||
|
class PipeWriter(object):
|
||
|
def __init__(self, output=sys.stdout):
|
||
|
self.output = output
|
||
|
|
||
|
def write_section(self, name, rest):
|
||
|
text = "Contents of file %s.txt:" % (name,)
|
||
|
self.output.write(text + "\n")
|
||
|
self.output.write("=" * len(text) + "\n")
|
||
|
self.output.write("\n")
|
||
|
self.output.write(rest.text() + "\n")
|
||
|
|
||
|
def getlink(self, type, targetname, targetfilename):
|
||
|
return '%s.txt' % (targetfilename,)
|
||
|
|
||
|
class DirWriter(object):
|
||
|
def __init__(self, directory=None):
|
||
|
if directory is None:
|
||
|
self.directory = py.test.ensuretemp("rstoutput")
|
||
|
else:
|
||
|
self.directory = py.path.local(directory)
|
||
|
|
||
|
def write_section(self, name, rest):
|
||
|
filename = '%s.txt' % (name,)
|
||
|
self.directory.ensure(filename).write(rest.text())
|
||
|
|
||
|
def getlink(self, type, targetname, targetfilename):
|
||
|
# we assume the result will get converted to HTML...
|
||
|
return '%s.html' % (targetfilename,)
|
||
|
|
||
|
class FileWriter(object):
|
||
|
def __init__(self, fpath):
|
||
|
self.fpath = fpath
|
||
|
self.fp = fpath.open('w+')
|
||
|
self._defined_targets = []
|
||
|
|
||
|
def write_section(self, name, rest):
|
||
|
self.fp.write(rest.text())
|
||
|
self.fp.flush()
|
||
|
|
||
|
def getlink(self, type, targetname, targetbasename):
|
||
|
# XXX problem: because of docutils' named anchor generation scheme,
|
||
|
# a method Foo.__init__ would clash with Foo.init (underscores are
|
||
|
# removed)
|
||
|
if targetname in self._defined_targets:
|
||
|
return None
|
||
|
self._defined_targets.append(targetname)
|
||
|
targetname = targetname.lower().replace('.', '-').replace('_', '-')
|
||
|
while '--' in targetname:
|
||
|
targetname = targetname.replace('--', '-')
|
||
|
if targetname.startswith('-'):
|
||
|
targetname = targetname[1:]
|
||
|
if targetname.endswith('-'):
|
||
|
targetname = targetname[:-1]
|
||
|
return '#%s-%s' % (type, targetname)
|
||
|
|
||
|
class HTMLDirWriter(object):
|
||
|
def __init__(self, indexhandler, filehandler, directory=None):
|
||
|
self.indexhandler = indexhandler
|
||
|
self.filehandler = filehandler
|
||
|
if directory is None:
|
||
|
self.directory = py.test.ensuretemp('dirwriter')
|
||
|
else:
|
||
|
self.directory = py.path.local(directory)
|
||
|
|
||
|
def write_section(self, name, rest):
|
||
|
if name == 'index':
|
||
|
handler = self.indexhandler
|
||
|
else:
|
||
|
handler = self.filehandler
|
||
|
h = handler(name)
|
||
|
t = RestTransformer(rest)
|
||
|
t.parse(h)
|
||
|
self.directory.ensure('%s.html' % (name,)).write(h.html)
|
||
|
|
||
|
def getlink(self, type, targetname, targetfilename):
|
||
|
return '%s.html' % (targetfilename,)
|
||
|
|
||
|
class RestGen(object):
|
||
|
def __init__(self, dsa, linkgen, writer=PipeWriter()):
|
||
|
#assert isinstance(linkgen, DirectPaste), (
|
||
|
# "Cannot use different linkgen by now")
|
||
|
self.dsa = dsa
|
||
|
self.linkgen = linkgen
|
||
|
self.writer = writer
|
||
|
self.tracebacks = {}
|
||
|
|
||
|
def write(self):
|
||
|
"""write the data to the writer"""
|
||
|
modlist = self.get_module_list()
|
||
|
classlist = self.get_class_list(module='')
|
||
|
funclist = self.get_function_list()
|
||
|
modlist.insert(0, ['', classlist, funclist])
|
||
|
|
||
|
indexrest = self.build_index([t[0] for t in modlist])
|
||
|
self.writer.write_section('index', Rest(*indexrest))
|
||
|
|
||
|
self.build_modrest(modlist)
|
||
|
|
||
|
def build_modrest(self, modlist):
|
||
|
modrest = self.build_modules(modlist)
|
||
|
for name, rest, classlist, funclist in modrest:
|
||
|
mname = name
|
||
|
if mname == '':
|
||
|
mname = self.dsa.get_module_name()
|
||
|
self.writer.write_section('module_%s' % (mname,),
|
||
|
Rest(*rest))
|
||
|
for cname, crest, cfunclist in classlist:
|
||
|
self.writer.write_section('class_%s' % (cname,),
|
||
|
Rest(*crest))
|
||
|
for fname, frest, tbdata in cfunclist:
|
||
|
self.writer.write_section('method_%s' % (fname,),
|
||
|
Rest(*frest))
|
||
|
for tbname, tbrest in tbdata:
|
||
|
self.writer.write_section('traceback_%s' % (tbname,),
|
||
|
Rest(*tbrest))
|
||
|
for fname, frest, tbdata in funclist:
|
||
|
self.writer.write_section('function_%s' % (fname,),
|
||
|
Rest(*frest))
|
||
|
for tbname, tbrest in tbdata:
|
||
|
self.writer.write_section('traceback_%s' % (tbname,),
|
||
|
Rest(*tbrest))
|
||
|
|
||
|
def build_classrest(self, classlist):
|
||
|
classrest = self.build_classes(classlist)
|
||
|
for cname, rest, cfunclist in classrest:
|
||
|
self.writer.write_section('class_%s' % (cname,),
|
||
|
Rest(*rest))
|
||
|
for fname, rest in cfunclist:
|
||
|
self.writer.write_section('method_%s' % (fname,),
|
||
|
Rest(*rest))
|
||
|
|
||
|
def build_funcrest(self, funclist):
|
||
|
funcrest = self.build_functions(funclist)
|
||
|
for fname, rest, tbdata in funcrest:
|
||
|
self.writer.write_section('function_%s' % (fname,),
|
||
|
Rest(*rest))
|
||
|
for tbname, tbrest in tbdata:
|
||
|
self.writer.write_section('traceback_%s' % (tbname,),
|
||
|
Rest(*tbrest))
|
||
|
|
||
|
def build_index(self, modules):
|
||
|
rest = [Title('index', abovechar='=', belowchar='=')]
|
||
|
rest.append(Title('exported modules:', belowchar='='))
|
||
|
for module in modules:
|
||
|
mtitle = module
|
||
|
if module == '':
|
||
|
module = self.dsa.get_module_name()
|
||
|
mtitle = '%s (top-level)' % (module,)
|
||
|
linktarget = self.writer.getlink('module', module,
|
||
|
'module_%s' % (module,))
|
||
|
rest.append(ListItem(Link(mtitle, linktarget)))
|
||
|
return rest
|
||
|
|
||
|
def build_modules(self, modules):
|
||
|
ret = []
|
||
|
for module, classes, functions in modules:
|
||
|
mname = module
|
||
|
if mname == '':
|
||
|
mname = self.dsa.get_module_name()
|
||
|
rest = [Title('module: %s' % (mname,), abovechar='=',
|
||
|
belowchar='='),
|
||
|
Title('index:', belowchar='=')]
|
||
|
if classes:
|
||
|
rest.append(Title('classes:', belowchar='^'))
|
||
|
for cls, bases, cfunclist in classes:
|
||
|
linktarget = self.writer.getlink('class', cls,
|
||
|
'class_%s' % (cls,))
|
||
|
rest.append(ListItem(Link(cls, linktarget)))
|
||
|
classrest = self.build_classes(classes)
|
||
|
if functions:
|
||
|
rest.append(Title('functions:', belowchar='^'))
|
||
|
for func in functions:
|
||
|
if module:
|
||
|
func = '%s.%s' % (module, func)
|
||
|
linktarget = self.writer.getlink('function',
|
||
|
func,
|
||
|
'function_%s' % (func,))
|
||
|
rest.append(ListItem(Link(func, linktarget)))
|
||
|
funcrest = self.build_functions(functions, module, False)
|
||
|
ret.append((module, rest, classrest, funcrest))
|
||
|
return ret
|
||
|
|
||
|
def build_classes(self, classes):
|
||
|
ret = []
|
||
|
for cls, bases, functions in classes:
|
||
|
rest = [Title('class: %s' % (cls,), belowchar='='),
|
||
|
LiteralBlock(self.dsa.get_doc(cls))]
|
||
|
# link to source
|
||
|
link_to_class = self.linkgen.getlinkobj(cls, self.dsa.get_obj(cls))
|
||
|
if link_to_class:
|
||
|
rest.append(Paragraph(Text("source: "), Link(*link_to_class)))
|
||
|
|
||
|
if bases:
|
||
|
rest.append(Title('base classes:', belowchar='^')),
|
||
|
for base in bases:
|
||
|
rest.append(ListItem(self.make_class_link(base)))
|
||
|
if functions:
|
||
|
rest.append(Title('functions:', belowchar='^'))
|
||
|
for (func, origin) in functions:
|
||
|
linktarget = self.writer.getlink('method',
|
||
|
'%s.%s' % (cls, func),
|
||
|
'method_%s.%s' % (cls,
|
||
|
func))
|
||
|
rest.append(ListItem(Link('%s.%s' % (cls, func),
|
||
|
linktarget)))
|
||
|
funcrest = self.build_functions(functions, cls, True)
|
||
|
ret.append((cls, rest, funcrest))
|
||
|
return ret
|
||
|
|
||
|
def build_functions(self, functions, parent='', methods=False):
|
||
|
ret = []
|
||
|
for function in functions:
|
||
|
origin = None
|
||
|
if methods:
|
||
|
function, origin = function
|
||
|
if parent:
|
||
|
function = '%s.%s' % (parent, function)
|
||
|
rest, tbrest = self.write_function(function, origin=origin,
|
||
|
ismethod=methods)
|
||
|
ret.append((function, rest, tbrest))
|
||
|
return ret
|
||
|
|
||
|
def get_module_list(self):
|
||
|
visited = []
|
||
|
ret = []
|
||
|
for name in self.dsa.get_class_names():
|
||
|
if '.' in name:
|
||
|
module, classname = split_of_last_part(name)
|
||
|
if module in visited:
|
||
|
continue
|
||
|
visited.append(module)
|
||
|
ret.append((module, self.get_class_list(module),
|
||
|
self.get_function_list(module)))
|
||
|
return ret
|
||
|
|
||
|
def get_class_list(self, module):
|
||
|
ret = []
|
||
|
for name in self.dsa.get_class_names():
|
||
|
classname = name
|
||
|
if '.' in name:
|
||
|
classmodule, classname = split_of_last_part(name)
|
||
|
if classmodule != module:
|
||
|
continue
|
||
|
elif module != '':
|
||
|
continue
|
||
|
bases = self.dsa.get_possible_base_classes(name)
|
||
|
ret.append((name, bases, self.get_method_list(name)))
|
||
|
return ret
|
||
|
|
||
|
def get_function_list(self, module=''):
|
||
|
ret = []
|
||
|
for name in self.dsa.get_function_names():
|
||
|
funcname = name
|
||
|
if '.' in name:
|
||
|
funcpath, funcname = split_of_last_part(name)
|
||
|
if funcpath != module:
|
||
|
continue
|
||
|
elif module != '':
|
||
|
continue
|
||
|
ret.append(funcname)
|
||
|
return ret
|
||
|
|
||
|
def get_method_list(self, classname):
|
||
|
methodnames = self.dsa.get_class_methods(classname)
|
||
|
return [(mn, self.dsa.get_method_origin('%s.%s' % (classname, mn)))
|
||
|
for mn in methodnames]
|
||
|
|
||
|
def process_type_link(self, _type):
|
||
|
# now we do simple type dispatching and provide a link in this case
|
||
|
lst = []
|
||
|
data = self.dsa.get_type_desc(_type)
|
||
|
if not data:
|
||
|
for i in _type.striter():
|
||
|
if isinstance(i, str):
|
||
|
lst.append(i)
|
||
|
else:
|
||
|
lst += self.process_type_link(i)
|
||
|
return lst
|
||
|
name, _desc_type, is_degenerated = data
|
||
|
if not is_degenerated:
|
||
|
linktarget = self.writer.getlink(_desc_type, name,
|
||
|
'%s_%s' % (_desc_type, name))
|
||
|
lst.append(Link(str(_type), linktarget))
|
||
|
else:
|
||
|
# we should provide here some way of linking to sourcegen directly
|
||
|
lst.append(name)
|
||
|
return lst
|
||
|
|
||
|
def write_function(self, functionname, origin=None, ismethod=False,
|
||
|
belowchar='-'):
|
||
|
# XXX I think the docstring should either be split on \n\n and cleaned
|
||
|
# from indentation, or treated as ReST too (although this is obviously
|
||
|
# dangerous for non-ReST docstrings)...
|
||
|
if ismethod:
|
||
|
title = Title('method: %s' % (functionname,), belowchar=belowchar)
|
||
|
else:
|
||
|
title = Title('function: %s' % (functionname,),
|
||
|
belowchar=belowchar)
|
||
|
|
||
|
lst = [title, LiteralBlock(self.dsa.get_doc(functionname)),
|
||
|
LiteralBlock(self.dsa.get_function_definition(functionname))]
|
||
|
link_to_function = self.linkgen.getlinkobj(functionname, self.dsa.get_obj(functionname))
|
||
|
if link_to_function:
|
||
|
lst.insert(1, Paragraph(Text("source: "), Link(*link_to_function)))
|
||
|
|
||
|
opar = Paragraph(Strong('origin'), ":")
|
||
|
if origin:
|
||
|
opar.add(self.make_class_link(origin))
|
||
|
else:
|
||
|
opar.add(Text('<UNKNOWN>'))
|
||
|
lst.append(opar)
|
||
|
|
||
|
lst.append(Paragraph(Strong("where"), ":"))
|
||
|
args, retval = self.dsa.get_function_signature(functionname)
|
||
|
for name, _type in args + [('return value', retval)]:
|
||
|
l = self.process_type_link(_type)
|
||
|
items = []
|
||
|
next = "%s :: " % name
|
||
|
for item in l:
|
||
|
if isinstance(item, str):
|
||
|
next += item
|
||
|
else:
|
||
|
if next:
|
||
|
items.append(Text(next))
|
||
|
next = ""
|
||
|
items.append(item)
|
||
|
if next:
|
||
|
items.append(Text(next))
|
||
|
lst.append(ListItem(*items))
|
||
|
|
||
|
local_changes = self.dsa.get_function_local_changes(functionname)
|
||
|
if local_changes:
|
||
|
lst.append(Paragraph(Strong('changes in __dict__ after execution'), ":"))
|
||
|
for k, changeset in local_changes.iteritems():
|
||
|
lst.append(ListItem('%s: %s' % (k, ', '.join(changeset))))
|
||
|
|
||
|
exceptions = self.dsa.get_function_exceptions(functionname)
|
||
|
if exceptions:
|
||
|
lst.append(Paragraph(Strong('exceptions that might appear during '
|
||
|
'execution'), ":"))
|
||
|
for exc in exceptions:
|
||
|
lst.append(ListItem(exc))
|
||
|
# XXX: right now we leave it alone
|
||
|
|
||
|
# XXX missing implementation of dsa.get_function_location()
|
||
|
#filename, lineno = self.dsa.get_function_location(functionname)
|
||
|
#linkname, linktarget = self.linkgen.getlink(filename, lineno)
|
||
|
#if linktarget:
|
||
|
# lst.append(Paragraph("Function source: ",
|
||
|
# Link(linkname, linktarget)))
|
||
|
#else:
|
||
|
source = self.dsa.get_function_source(functionname)
|
||
|
if source:
|
||
|
lst.append(Paragraph(Strong('function source'), ":"))
|
||
|
lst.append(LiteralBlock(source))
|
||
|
|
||
|
# call sites..
|
||
|
call_sites = self.dsa.get_function_callpoints(functionname)
|
||
|
tbrest = []
|
||
|
if call_sites:
|
||
|
call_site_title = Title("call sites:", belowchar='+')
|
||
|
lst.append(call_site_title)
|
||
|
|
||
|
# we have to think differently here. I would go for:
|
||
|
# 1. A quick'n'dirty statement where call has appeared first
|
||
|
# (topmost)
|
||
|
# 2. Link to short traceback
|
||
|
# 3. Link to long traceback
|
||
|
for call_site, _ in call_sites:
|
||
|
fdata, tbdata = self.call_site_link(functionname, call_site)
|
||
|
lst += fdata
|
||
|
tbrest.append(tbdata)
|
||
|
|
||
|
return lst, tbrest
|
||
|
|
||
|
def call_site_link(self, functionname, call_site):
|
||
|
tbid, tbrest = self.gen_traceback(functionname, call_site)
|
||
|
tbname = '%s.%s' % (functionname, tbid)
|
||
|
linktarget = self.writer.getlink('traceback',
|
||
|
tbname,
|
||
|
'traceback_%s' % (tbname,))
|
||
|
frest = [Paragraph("called in %s" % call_site[0].filename),
|
||
|
Paragraph(Link("traceback %s" % (tbname,),
|
||
|
linktarget))]
|
||
|
return frest, (tbname, tbrest)
|
||
|
|
||
|
def gen_traceback(self, funcname, call_site):
|
||
|
tbid = len(self.tracebacks.setdefault(funcname, []))
|
||
|
self.tracebacks[funcname].append(call_site)
|
||
|
tbrest = [Title('traceback for %s' % (funcname,))]
|
||
|
for line in call_site:
|
||
|
lineno = line.lineno - line.firstlineno
|
||
|
linkname, linktarget = self.linkgen.getlink(line.filename,
|
||
|
line.lineno + 1,
|
||
|
funcname)
|
||
|
if linktarget:
|
||
|
tbrest.append(Paragraph(Link(linkname, linktarget)))
|
||
|
else:
|
||
|
tbrest.append(Paragraph(linkname))
|
||
|
try:
|
||
|
source = line.source
|
||
|
except IOError:
|
||
|
source = "*cannot get source*"
|
||
|
mangled = []
|
||
|
for i, sline in enumerate(str(source).split('\n')):
|
||
|
if i == lineno:
|
||
|
line = '-> %s' % (sline,)
|
||
|
else:
|
||
|
line = ' %s' % (sline,)
|
||
|
mangled.append(line)
|
||
|
tbrest.append(LiteralBlock('\n'.join(mangled)))
|
||
|
return tbid, tbrest
|
||
|
|
||
|
def make_class_link(self, desc):
|
||
|
if not desc or desc.is_degenerated:
|
||
|
# create dummy link here, or no link at all
|
||
|
return Strong(desc.name)
|
||
|
else:
|
||
|
linktarget = self.writer.getlink('class', desc.name,
|
||
|
'class_%s' % (desc.name,))
|
||
|
return Link(desc.name, linktarget)
|