First iteration of an autodoc that handles attribute documentation.

This commit is contained in:
Georg Brandl 2008-12-30 02:09:29 +01:00
parent 0edeba7acb
commit 83cf763804
2 changed files with 59 additions and 23 deletions

View File

@ -22,6 +22,7 @@ from docutils.parsers.rst import directives
from docutils.statemachine import ViewList from docutils.statemachine import ViewList
from sphinx.util import rpartition, nested_parse_with_titles from sphinx.util import rpartition, nested_parse_with_titles
from sphinx.pycode import ModuleAnalyzer, PycodeError
clstypes = (type, ClassType) clstypes = (type, ClassType)
try: try:
@ -313,7 +314,7 @@ class RstGenerator(object):
'for automodule %s' % name) 'for automodule %s' % name)
return (path or '') + base, [], None, None return (path or '') + base, [], None, None
elif what in ('exception', 'function', 'class'): elif what in ('exception', 'function', 'class', 'data'):
if mod is None: if mod is None:
if path: if path:
mod = path.rstrip('.') mod = path.rstrip('.')
@ -434,7 +435,9 @@ class RstGenerator(object):
modfile = None # e.g. for builtin and C modules modfile = None # e.g. for builtin and C modules
for part in objpath: for part in objpath:
todoc = getattr(todoc, part) todoc = getattr(todoc, part)
except (ImportError, AttributeError), err: # also get a source code analyzer for attribute docs
analyzer = ModuleAnalyzer.for_module(mod)
except (ImportError, AttributeError, PycodeError), err:
self.warn('autodoc can\'t import/find %s %r, it reported error: "%s", ' self.warn('autodoc can\'t import/find %s %r, it reported error: "%s", '
'please check your spelling and sys.path' % 'please check your spelling and sys.path' %
(what, str(fullname), err)) (what, str(fullname), err))
@ -503,6 +506,15 @@ class RstGenerator(object):
else: else:
sourcename = 'docstring of %s' % fullname sourcename = 'docstring of %s' % fullname
# add content from attribute documentation
attr_docs = analyzer.find_attr_docs()
if what in ('data', 'attribute'):
key = ('.'.join(objpath[:-1]), objpath[-1])
if key in attr_docs:
no_docstring = True
for i, line in enumerate(attr_docs[key]):
self.result.append(indent + line, sourcename, i)
# add content from docstrings # add content from docstrings
if not no_docstring: if not no_docstring:
for i, line in enumerate(self.get_doc(what, fullname, todoc)): for i, line in enumerate(self.get_doc(what, fullname, todoc)):
@ -524,9 +536,9 @@ class RstGenerator(object):
self.env.autodoc_current_class = objpath[0] self.env.autodoc_current_class = objpath[0]
# add members, if possible # add members, if possible
_all = members == ['__all__'] all_members = members == ['__all__']
members_check_module = False members_check_module = False
if _all: if all_members:
# unqualified :members: given # unqualified :members: given
if what == 'module': if what == 'module':
if hasattr(todoc, '__all__'): if hasattr(todoc, '__all__'):
@ -555,14 +567,28 @@ class RstGenerator(object):
else: else:
all_members = [(mname, getattr(todoc, mname)) for mname in members] all_members = [(mname, getattr(todoc, mname)) for mname in members]
# search for members in source code too
namespace = '.'.join(objpath) # will be empty for modules
for (membername, member) in all_members: for (membername, member) in all_members:
if _all and membername.startswith('_'): # if isattr is True, the member is documented as an attribute
isattr = False
# if content is not None, no extra content from docstrings will be added
content = None
if all_members and membername.startswith('_'):
# ignore members whose name starts with _ by default # ignore members whose name starts with _ by default
skip = True skip = True
else: else:
# ignore undocumented members if :undoc-members: is not given if (namespace, membername) in attr_docs:
doc = getattr(member, '__doc__', None) # keep documented attributes
skip = not self.options.undoc_members and not doc skip = False
isattr = True
else:
# ignore undocumented members if :undoc-members: is not given
doc = getattr(member, '__doc__', None)
skip = not self.options.undoc_members and not doc
# give the user a chance to decide whether this member should be skipped # give the user a chance to decide whether this member should be skipped
if self.env.app: if self.env.app:
# let extensions preprocess docstrings # let extensions preprocess docstrings
@ -573,10 +599,11 @@ class RstGenerator(object):
if skip: if skip:
continue continue
content = None
if what == 'module': if what == 'module':
if isinstance(member, (FunctionType, BuiltinFunctionType)): if isinstance(member, (FunctionType, BuiltinFunctionType)):
memberwhat = 'function' memberwhat = 'function'
elif isattr:
memberwhat = 'attribute'
elif isinstance(member, clstypes): elif isinstance(member, clstypes):
if member.__name__ != membername: if member.__name__ != membername:
# assume it's aliased # assume it's aliased
@ -588,10 +615,13 @@ class RstGenerator(object):
else: else:
memberwhat = 'class' memberwhat = 'class'
else: else:
# XXX: todo -- attribute docs
continue continue
else: else:
if isinstance(member, clstypes): if inspect.isroutine(member):
memberwhat = 'method'
elif isattr:
memberwhat = 'attribute'
elif isinstance(member, clstypes):
if member.__name__ != membername: if member.__name__ != membername:
# assume it's aliased # assume it's aliased
memberwhat = 'attribute' memberwhat = 'attribute'
@ -599,12 +629,9 @@ class RstGenerator(object):
source='') source='')
else: else:
memberwhat = 'class' memberwhat = 'class'
elif inspect.isroutine(member):
memberwhat = 'method'
elif isdescriptor(member): elif isdescriptor(member):
memberwhat = 'attribute' memberwhat = 'attribute'
else: else:
# XXX: todo -- attribute docs
continue continue
# give explicitly separated module name, so that members of inner classes # give explicitly separated module name, so that members of inner classes
# can be documented # can be documented

View File

@ -43,7 +43,7 @@ def prepare_commentdoc(s):
result.append(line[3:]) result.append(line[3:])
if result and result[-1]: if result and result[-1]:
result.append('') result.append('')
return '\n'.join(result) return result
_eq = pytree.Leaf(token.EQUAL, '=') _eq = pytree.Leaf(token.EQUAL, '=')
@ -57,7 +57,7 @@ class ClassAttrVisitor(pytree.NodeVisitor):
def init(self, scope): def init(self, scope):
self.scope = scope self.scope = scope
self.namespace = [] self.namespace = []
self.collected = [] self.collected = {}
def visit_classdef(self, node): def visit_classdef(self, node):
self.namespace.append(node[1].value) self.namespace.append(node[1].value)
@ -87,9 +87,9 @@ class ClassAttrVisitor(pytree.NodeVisitor):
if target.type != token.NAME: if target.type != token.NAME:
# don't care about complex targets # don't care about complex targets
continue continue
name = '.'.join(self.namespace + [target.value]) namespace = '.'.join(self.namespace)
if name.startswith(self.scope): if namespace.startswith(self.scope):
self.collected.append((name, docstring)) self.collected[namespace, target.value] = docstring
def visit_funcdef(self, node): def visit_funcdef(self, node):
# don't descend into functions -- nothing interesting there # don't descend into functions -- nothing interesting there
@ -105,6 +105,8 @@ class PycodeError(Exception):
class ModuleAnalyzer(object): class ModuleAnalyzer(object):
# cache for analyzer objects
cache = {}
def __init__(self, tree, modname, srcname): def __init__(self, tree, modname, srcname):
self.tree = tree self.tree = tree
@ -131,6 +133,8 @@ class ModuleAnalyzer(object):
@classmethod @classmethod
def for_module(cls, modname): def for_module(cls, modname):
if modname in cls.cache:
return cls.cache[modname]
if modname not in sys.modules: if modname not in sys.modules:
try: try:
__import__(modname) __import__(modname)
@ -142,7 +146,9 @@ class ModuleAnalyzer(object):
source = mod.__loader__.get_source(modname) source = mod.__loader__.get_source(modname)
except Exception, err: except Exception, err:
raise PycodeError('error getting source for %r' % modname, err) raise PycodeError('error getting source for %r' % modname, err)
return cls.for_string(source, modname) obj = cls.for_string(source, modname)
cls.cache[modname] = obj
return obj
filename = getattr(mod, '__file__', None) filename = getattr(mod, '__file__', None)
if filename is None: if filename is None:
raise PycodeError('no source found for module %r' % modname) raise PycodeError('no source found for module %r' % modname)
@ -153,13 +159,16 @@ class ModuleAnalyzer(object):
raise PycodeError('source is not a .py file: %r' % filename) raise PycodeError('source is not a .py file: %r' % filename)
if not path.isfile(filename): if not path.isfile(filename):
raise PycodeError('source file is not present: %r' % filename) raise PycodeError('source file is not present: %r' % filename)
return cls.for_file(filename, modname) obj = cls.for_file(filename, modname)
cls.cache[modname] = obj
return obj
def find_attrs(self): def find_attr_docs(self, scope=''):
attr_visitor = ClassAttrVisitor(number2name, '') attr_visitor = ClassAttrVisitor(number2name, scope)
attr_visitor.visit(self.tree) attr_visitor.visit(self.tree)
return attr_visitor.collected return attr_visitor.collected
if __name__ == '__main__': if __name__ == '__main__':
x0 = time.time() x0 = time.time()
ma = ModuleAnalyzer.for_file('sphinx/builders/html.py', 'sphinx.builders.html') ma = ModuleAnalyzer.for_file('sphinx/builders/html.py', 'sphinx.builders.html')