344 lines
12 KiB
Python
344 lines
12 KiB
Python
|
|
""" This module is keeping track about API informations as well as
|
|
providing some interface to easily access stored data
|
|
"""
|
|
|
|
import py
|
|
import sys
|
|
import types
|
|
import inspect
|
|
|
|
from py.__.apigen.tracer.description import FunctionDesc, ClassDesc, \
|
|
MethodDesc, Desc
|
|
|
|
from py.__.apigen.tracer import model
|
|
|
|
sorted = py.builtin.sorted
|
|
|
|
def pkg_to_dict(module):
|
|
defs = module.__pkg__.exportdefs
|
|
d = {}
|
|
for key, value in defs.iteritems():
|
|
chain = key.split('.')
|
|
base = module
|
|
for elem in chain:
|
|
base = getattr(base, elem)
|
|
if value[1] == '*':
|
|
d.update(get_star_import_tree(base, key))
|
|
else:
|
|
d[key] = base
|
|
return d
|
|
|
|
def get_star_import_tree(module, modname):
|
|
""" deal with '*' entries in an initpkg situation """
|
|
ret = {}
|
|
modpath = py.path.local(inspect.getsourcefile(module))
|
|
pkgpath = module.__pkg__.getpath()
|
|
for objname in dir(module):
|
|
if objname.startswith('_'):
|
|
continue # also skip __*__ attributes
|
|
obj = getattr(module, objname)
|
|
if (isinstance(obj, types.ClassType) or
|
|
isinstance(obj, types.ObjectType)):
|
|
try:
|
|
sourcefile_object = py.path.local(
|
|
inspect.getsourcefile(obj))
|
|
except TypeError:
|
|
continue
|
|
else:
|
|
if sourcefile_object.strpath != modpath.strpath:
|
|
# not in this package
|
|
continue
|
|
dotted_name = '%s.%s' % (modname, objname)
|
|
ret[dotted_name] = obj
|
|
return ret
|
|
|
|
class DocStorage(object):
|
|
""" Class storing info about API
|
|
"""
|
|
def __init__(self):
|
|
self.module_name = None
|
|
|
|
def consider_call(self, frame, caller_frame, upward_cut_frame=None):
|
|
assert isinstance(frame, py.code.Frame)
|
|
desc = self.find_desc(frame.code, frame.raw.f_locals)
|
|
if desc:
|
|
self.generalize_args(desc, frame)
|
|
desc.consider_call_site(caller_frame, upward_cut_frame)
|
|
desc.consider_start_locals(frame)
|
|
|
|
def generalize_args(self, desc, frame):
|
|
args = [arg for key, arg in frame.getargs()]
|
|
#self.call_stack.append((desc, args))
|
|
desc.consider_call([model.guess_type(arg) for arg in args])
|
|
|
|
def generalize_retval(self, desc, arg):
|
|
desc.consider_return(model.guess_type(arg))
|
|
|
|
def consider_return(self, frame, arg):
|
|
assert isinstance(frame, py.code.Frame)
|
|
desc = self.find_desc(frame.code, frame.raw.f_locals)
|
|
if desc:
|
|
self.generalize_retval(desc, arg)
|
|
desc.consider_end_locals(frame)
|
|
|
|
def consider_exception(self, frame, arg):
|
|
desc = self.find_desc(frame.code, frame.raw.f_locals)
|
|
if desc:
|
|
exc_class, value, _ = arg
|
|
desc.consider_exception(exc_class, value)
|
|
|
|
def find_desc(self, code, locals):
|
|
try:
|
|
# argh, very fragile specialcasing
|
|
return self.desc_cache[(code.raw,
|
|
locals[code.raw.co_varnames[0]].__class__)]
|
|
except (KeyError, IndexError, AttributeError): # XXX hrmph
|
|
return self.desc_cache.get(code.raw, None)
|
|
#for desc in self.descs.values():
|
|
# if desc.has_code(frame.code.raw):
|
|
# return desc
|
|
#return None
|
|
|
|
def make_cache(self):
|
|
self.desc_cache = {}
|
|
for key, desc in self.descs.iteritems():
|
|
self.desc_cache[desc] = desc
|
|
|
|
def from_dict(self, _dict, keep_frames=False, module_name=None):
|
|
self.module_name = module_name
|
|
self.descs = {}
|
|
for key, val in _dict.iteritems():
|
|
to_key, to_val = self.make_desc(key, val)
|
|
if to_key:
|
|
self.descs[to_key] = to_val
|
|
self.make_cache()
|
|
# XXX
|
|
return self
|
|
|
|
# XXX: This function becomes slowly outdated and even might go away at some
|
|
# point. The question is whether we want to use tracer.magic or not
|
|
# at all
|
|
def add_desc(self, name, value, **kwargs):
|
|
key = name
|
|
count = 1
|
|
while key in self.descs:
|
|
key = "%s_%d" % (name, count)
|
|
count += 1
|
|
key, desc = self.make_desc(key, value, **kwargs)
|
|
if key:
|
|
self.descs[key] = desc
|
|
self.desc_cache[desc] = desc
|
|
return desc
|
|
else:
|
|
return None
|
|
|
|
def make_desc(self, key, value, add_desc=True, **kwargs):
|
|
if isinstance(value, types.FunctionType):
|
|
desc = FunctionDesc(key, value, **kwargs)
|
|
elif isinstance(value, (types.ObjectType, types.ClassType)):
|
|
desc = ClassDesc(key, value, **kwargs)
|
|
# XXX: This is the special case when we do not have __init__
|
|
# in dir(value) for uknown reason. Need to investigate it
|
|
for name in dir(value) + ['__init__']:
|
|
field = getattr(value, name, None)
|
|
if isinstance(field, types.MethodType) and \
|
|
isinstance(field.im_func, types.FunctionType):
|
|
real_name = key + '.' + name
|
|
md = MethodDesc(real_name, field)
|
|
if add_desc: # XXX hack
|
|
self.descs[real_name] = md
|
|
desc.add_method_desc(name, md)
|
|
# Some other fields as well?
|
|
elif isinstance(value, types.MethodType):
|
|
desc = MethodDesc(key, value, **kwargs)
|
|
else:
|
|
desc = Desc(value)
|
|
return (key, desc) # How to do it better? I want a desc to be a key
|
|
# value, but I cannot get full object if I do a lookup
|
|
|
|
def from_pkg(self, module, keep_frames=False):
|
|
self.module = module
|
|
self.from_dict(pkg_to_dict(module), keep_frames, module.__name__)
|
|
# XXX
|
|
return self
|
|
|
|
def from_module(self, func):
|
|
raise NotImplementedError("From module")
|
|
|
|
class AbstractDocStorageAccessor(object):
|
|
def __init__(self):
|
|
raise NotImplementedError("Purely virtual object")
|
|
|
|
def get_function_names(self):
|
|
""" Returning names of all functions
|
|
"""
|
|
|
|
def get_class_names(self):
|
|
""" Returning names of all classess
|
|
"""
|
|
|
|
def get_doc(self, name):
|
|
""" Returning __doc__ of a function
|
|
"""
|
|
|
|
def get_function_definition(self, name):
|
|
""" Returns definition of a function (source)
|
|
"""
|
|
|
|
def get_function_signature(self, name):
|
|
""" Returns types of a function
|
|
"""
|
|
|
|
def get_function_callpoints(self, name):
|
|
""" Returns list of all callpoints
|
|
"""
|
|
|
|
def get_module_name(self):
|
|
pass
|
|
|
|
def get_class_methods(self, name):
|
|
""" Returns all methods of a class
|
|
"""
|
|
|
|
#def get_object_info(self, key):
|
|
#
|
|
|
|
def get_module_info(self):
|
|
""" Returns module information
|
|
"""
|
|
|
|
class DocStorageAccessor(AbstractDocStorageAccessor):
|
|
""" Set of helper functions to access DocStorage, separated in different
|
|
class to keep abstraction
|
|
"""
|
|
def __init__(self, ds):
|
|
self.ds = ds
|
|
|
|
def _get_names(self, filter):
|
|
return [i for i, desc in self.ds.descs.iteritems() if filter(i, desc)]
|
|
|
|
def get_function_names(self):
|
|
return sorted(self._get_names(lambda i, desc: type(desc) is
|
|
FunctionDesc))
|
|
|
|
def get_class_names(self):
|
|
return sorted(self._get_names(lambda i, desc: isinstance(desc,
|
|
ClassDesc)))
|
|
|
|
#def get_function(self, name):
|
|
# return self.ds.descs[name].pyobj
|
|
|
|
def get_doc(self, name):
|
|
return self.ds.descs[name].pyobj.__doc__ or "*Not documented*"
|
|
|
|
def get_function_definition(self, name):
|
|
desc = self.ds.descs[name]
|
|
assert isinstance(desc, FunctionDesc)
|
|
code = py.code.Code(desc.code)
|
|
return code.fullsource[code.firstlineno]
|
|
|
|
def get_function_signature(self, name):
|
|
desc = self.ds.descs[name]
|
|
# we return pairs of (name, type) here
|
|
names = desc.pyobj.func_code.co_varnames[
|
|
:desc.pyobj.func_code.co_argcount]
|
|
types = desc.inputcells
|
|
return zip(names, types), desc.retval
|
|
|
|
def get_function_source(self, name):
|
|
desc = self.ds.descs[name]
|
|
try:
|
|
return str(py.code.Source(desc.pyobj))
|
|
except IOError:
|
|
return "Cannot get source"
|
|
|
|
def get_function_callpoints(self, name):
|
|
# return list of tuple (filename, fileline, frame)
|
|
return self.ds.descs[name].get_call_sites()
|
|
|
|
def get_function_local_changes(self, name):
|
|
return self.ds.descs[name].get_local_changes()
|
|
|
|
def get_function_exceptions(self, name):
|
|
return sorted([i.__name__ for i in self.ds.descs[name].exceptions.keys()])
|
|
|
|
def get_module_name(self):
|
|
if self.ds.module_name is not None:
|
|
return self.ds.module_name
|
|
elif hasattr(self.ds, 'module'):
|
|
return self.ds.module.__name__
|
|
return "Unknown module"
|
|
|
|
def get_class_methods(self, name):
|
|
desc = self.ds.descs[name]
|
|
assert isinstance(desc, ClassDesc)
|
|
return sorted(desc.getfields())
|
|
|
|
def get_module_info(self):
|
|
module = getattr(self.ds, 'module', None)
|
|
if module is None:
|
|
return "Lack of module info"
|
|
try:
|
|
retval = module.__doc__ or "*undocumented*"
|
|
retval = module.__pkg__.description
|
|
retval = module.__pkg__.long_description
|
|
except AttributeError:
|
|
pass
|
|
return retval
|
|
|
|
def get_type_desc(self, _type):
|
|
# XXX We provide only classes here
|
|
if not isinstance(_type, model.SomeClass):
|
|
return None
|
|
# XXX we might want to cache it at some point
|
|
for key, desc in self.ds.descs.iteritems():
|
|
if desc.pyobj == _type.cls:
|
|
return key, 'class', desc.is_degenerated
|
|
return None
|
|
|
|
def get_method_origin(self, name):
|
|
method = self.ds.descs[name].pyobj
|
|
cls = method.im_class
|
|
if not cls.__bases__:
|
|
return self.desc_from_pyobj(cls, cls.__name__)
|
|
curr = cls
|
|
while curr:
|
|
for base in curr.__bases__:
|
|
basefunc = getattr(base, method.im_func.func_name, None)
|
|
if (basefunc is not None and hasattr(basefunc, 'im_func') and
|
|
hasattr(basefunc.im_func, 'func_code') and
|
|
basefunc.im_func.func_code is
|
|
method.im_func.func_code):
|
|
curr = base
|
|
break
|
|
else:
|
|
break
|
|
return self.desc_from_pyobj(curr, curr.__name__)
|
|
|
|
def get_possible_base_classes(self, name):
|
|
cls = self.ds.descs[name].pyobj
|
|
if not hasattr(cls, '__bases__'):
|
|
return []
|
|
retval = []
|
|
for base in cls.__bases__:
|
|
desc = self.desc_from_pyobj(base, base.__name__)
|
|
if desc is not None:
|
|
retval.append(desc)
|
|
return retval
|
|
|
|
def desc_from_pyobj(self, pyobj, name):
|
|
for desc in self.ds.descs.values():
|
|
if isinstance(desc, ClassDesc) and desc.pyobj is pyobj:
|
|
return desc
|
|
# otherwise create empty desc
|
|
key, desc = self.ds.make_desc(name, pyobj, False)
|
|
#self.ds.descs[key] = desc
|
|
desc.is_degenerated = True
|
|
# and make sure we'll not try to link to it directly
|
|
return desc
|
|
|
|
def get_obj(self, name):
|
|
return self.ds.descs[name].pyobj
|
|
|