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 sphinx.util import rpartition, nested_parse_with_titles
from sphinx.pycode import ModuleAnalyzer, PycodeError
clstypes = (type, ClassType)
try:
@ -313,7 +314,7 @@ class RstGenerator(object):
'for automodule %s' % name)
return (path or '') + base, [], None, None
elif what in ('exception', 'function', 'class'):
elif what in ('exception', 'function', 'class', 'data'):
if mod is None:
if path:
mod = path.rstrip('.')
@ -434,7 +435,9 @@ class RstGenerator(object):
modfile = None # e.g. for builtin and C modules
for part in objpath:
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", '
'please check your spelling and sys.path' %
(what, str(fullname), err))
@ -503,6 +506,15 @@ class RstGenerator(object):
else:
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
if not no_docstring:
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]
# add members, if possible
_all = members == ['__all__']
all_members = members == ['__all__']
members_check_module = False
if _all:
if all_members:
# unqualified :members: given
if what == 'module':
if hasattr(todoc, '__all__'):
@ -555,14 +567,28 @@ class RstGenerator(object):
else:
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:
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
skip = 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
if (namespace, membername) in attr_docs:
# keep documented attributes
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
if self.env.app:
# let extensions preprocess docstrings
@ -573,10 +599,11 @@ class RstGenerator(object):
if skip:
continue
content = None
if what == 'module':
if isinstance(member, (FunctionType, BuiltinFunctionType)):
memberwhat = 'function'
elif isattr:
memberwhat = 'attribute'
elif isinstance(member, clstypes):
if member.__name__ != membername:
# assume it's aliased
@ -588,10 +615,13 @@ class RstGenerator(object):
else:
memberwhat = 'class'
else:
# XXX: todo -- attribute docs
continue
else:
if isinstance(member, clstypes):
if inspect.isroutine(member):
memberwhat = 'method'
elif isattr:
memberwhat = 'attribute'
elif isinstance(member, clstypes):
if member.__name__ != membername:
# assume it's aliased
memberwhat = 'attribute'
@ -599,12 +629,9 @@ class RstGenerator(object):
source='')
else:
memberwhat = 'class'
elif inspect.isroutine(member):
memberwhat = 'method'
elif isdescriptor(member):
memberwhat = 'attribute'
else:
# XXX: todo -- attribute docs
continue
# give explicitly separated module name, so that members of inner classes
# can be documented

View File

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