diff --git a/sphinx/ext/autodoc.py b/sphinx/ext/autodoc.py index f94afc759..ddea1e444 100644 --- a/sphinx/ext/autodoc.py +++ b/sphinx/ext/autodoc.py @@ -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 diff --git a/sphinx/pycode/__init__.py b/sphinx/pycode/__init__.py index 373d4a480..0d4058bdb 100644 --- a/sphinx/pycode/__init__.py +++ b/sphinx/pycode/__init__.py @@ -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')