Commits

Georg Brandl committed 7450066

First iteration of an autodoc that handles attribute documentation.

  • Participants
  • Parent commits 79e8a87

Comments (0)

Files changed (2)

File sphinx/ext/autodoc.py

 from docutils.statemachine import ViewList
 
 from sphinx.util import rpartition, nested_parse_with_titles
+from sphinx.pycode import ModuleAnalyzer, PycodeError
 
 clstypes = (type, ClassType)
 try:
                           '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('.')
                 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))
         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)):
             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__'):
         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
             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
                     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'
                                            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

File sphinx/pycode/__init__.py

             result.append(line[3:])
     if result and result[-1]:
         result.append('')
-    return '\n'.join(result)
+    return result
 
 
 _eq = pytree.Leaf(token.EQUAL, '=')
     def init(self, scope):
         self.scope = scope
         self.namespace = []
-        self.collected = []
+        self.collected = {}
 
     def visit_classdef(self, node):
         self.namespace.append(node[1].value)
             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
 
 
 class ModuleAnalyzer(object):
+    # cache for analyzer objects
+    cache = {}
 
     def __init__(self, tree, modname, srcname):
         self.tree = tree
 
     @classmethod
     def for_module(cls, modname):
+        if modname in cls.cache:
+            return cls.cache[modname]
         if modname not in sys.modules:
             try:
                 __import__(modname)
                 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)
             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')