Commits

Georg Brandl  committed 16ee927

#280: Autodoc can now document instance attributes assigned in ``__init__`` methods.

  • Participants
  • Parent commits 93568df

Comments (0)

Files changed (4)

 Release 1.0 (in development)
 ============================
 
+* #280: Autodoc can now document instance attributes assigned in
+  ``__init__`` methods.
+
 * Added ``alt`` option to ``graphviz`` extension directives.
 
 * Added Epub builder.

File sphinx/ext/autodoc.py

 
 
 ALL = object()
+INSTANCEATTR = object()
 
 def members_option(arg):
     """Used to convert the :members: option to auto directives."""
                     self.directive.warn('missing attribute %s in object %s'
                                         % (mname, self.fullname))
             return False, ret
-        elif self.options.inherited_members:
+
+        if self.options.inherited_members:
             # safe_getmembers() uses dir() which pulls in members from all
             # base classes
-            return False, safe_getmembers(self.object)
+            members = safe_getmembers(self.object)
         else:
             # __dict__ contains only the members directly defined in
             # the class (but get them via getattr anyway, to e.g. get
             # unbound method objects instead of function objects);
             # using keys() because apparently there are objects for which
             # __dict__ changes while getting attributes
-            return False, sorted([
-                (mname, self.get_attr(self.object, mname, None))
-                for mname in self.get_attr(self.object, '__dict__').keys()])
+            obj_dict = self.get_attr(self.object, '__dict__')
+            members = [(mname, self.get_attr(self.object, mname, None))
+                       for mname in obj_dict.keys()]
+        membernames = set(m[0] for m in members)
+        # add instance attributes from the analyzer
+        if self.analyzer:
+            attr_docs = self.analyzer.find_attr_docs()
+            namespace = '.'.join(self.objpath)
+            for item in attr_docs.iteritems():
+                if item[0][0] == namespace:
+                    if item[0][1] not in membernames:
+                        members.append((item[0][1], INSTANCEATTR))
+        return False, sorted(members)
 
     def filter_members(self, members, want_all):
         """
         pass
 
 
+class InstanceAttributeDocumenter(AttributeDocumenter):
+    """
+    Specialized Documenter subclass for attributes that cannot be imported
+    because they are instance attributes (e.g. assigned in __init__).
+    """
+    objtype = 'instanceattribute'
+    directivetype = 'attribute'
+    member_order = 60
+
+    # must be higher than AttributeDocumenter
+    priority = 11
+
+    @classmethod
+    def can_document_member(cls, member, membername, isattr, parent):
+        """This documents only INSTANCEATTR members."""
+        return isattr and (member is INSTANCEATTR)
+
+    def import_object(self):
+        """Never import anything."""
+        # disguise as an attribute
+        self.objtype = 'attribute'
+        return True
+
+    def add_content(self, more_content, no_docstring=False):
+        """Never try to get a docstring from the object."""
+        AttributeDocumenter.add_content(self, more_content, no_docstring=True)
+
+
 class AutoDirective(Directive):
     """
     The AutoDirective class is used for all autodoc directives.  It dispatches
     app.add_autodocumenter(FunctionDocumenter)
     app.add_autodocumenter(MethodDocumenter)
     app.add_autodocumenter(AttributeDocumenter)
+    app.add_autodocumenter(InstanceAttributeDocumenter)
 
     app.add_config_value('autoclass_content', 'class', True)
     app.add_config_value('autodoc_member_order', 'alphabetic', True)

File sphinx/pycode/__init__.py

 class AttrDocVisitor(nodes.NodeVisitor):
     """
     Visitor that collects docstrings for attribute assignments on toplevel and
-    in classes.
+    in classes (class attributes and attributes set in __init__).
 
     The docstrings can either be in special '#:' comments before the assignment
     or in a docstring after it.
     """
     def init(self, scope, encoding):
         self.scope = scope
+        self.in_init = 0
         self.encoding = encoding
         self.namespace = []
         self.collected = {}
 
     def visit_classdef(self, node):
+        """Visit a class."""
         self.namespace.append(node[1].value)
         self.generic_visit(node)
         self.namespace.pop()
 
+    def visit_funcdef(self, node):
+        """Visit a function (or method)."""
+        # usually, don't descend into functions -- nothing interesting there
+        if node[1].value == '__init__':
+            # however, collect attributes set in __init__ methods
+            self.in_init += 1
+            self.generic_visit(node)
+            self.in_init -= 1
+
     def visit_expr_stmt(self, node):
         """Visit an assignment which may have a special comment before it."""
         if _eq not in node.children:
             docstring = prepare_docstring(docstring)
             self.add_docstring(prev[0], docstring)
 
-    def visit_funcdef(self, node):
-        # don't descend into functions -- nothing interesting there
-        return
-
     def add_docstring(self, node, docstring):
         # add an item for each assignment target
         for i in range(0, len(node) - 1, 2):
             target = node[i]
-            if target.type != token.NAME:
-                # don't care about complex targets
+            if self.in_init and self.number2name[target.type] == 'power':
+                # maybe an attribute assignment -- check necessary conditions
+                if (# node must have two children
+                    len(target) != 2 or
+                    # first child must be "self"
+                    target[0].type != token.NAME or target[0].value != 'self' or
+                    # second child must be a "trailer" with two children
+                    self.number2name[target[1].type] != 'trailer' or
+                    len(target[1]) != 2 or
+                    # first child must be a dot, second child a name
+                    target[1][0].type != token.DOT or
+                    target[1][1].type != token.NAME):
+                    continue
+                name = target[1][1].value
+            elif target.type != token.NAME:
+                # don't care about other complex targets
                 continue
+            else:
+                name = target.value
             namespace = '.'.join(self.namespace)
             if namespace.startswith(self.scope):
-                self.collected[namespace, target.value] = docstring
+                self.collected[namespace, name] = docstring
 
 
 class PycodeError(Exception):

File tests/test_autodoc.py

                    ('attribute', 'test_autodoc.Class.descr'),
                    ('attribute', 'test_autodoc.Class.attr'),
                    ('attribute', 'test_autodoc.Class.docattr'),
-                   ('attribute', 'test_autodoc.Class.udocattr')])
+                   ('attribute', 'test_autodoc.Class.udocattr'),
+                   ('attribute', 'test_autodoc.Class.inst_attr_comment'),
+                   ('attribute', 'test_autodoc.Class.inst_attr_string')
+                   ])
     options.members = ALL
     assert_processes(should, 'class', 'Class')
     options.undoc_members = True
     assert_result_contains('   :platform: Platform', 'module', 'test_autodoc')
     # test if __all__ is respected for modules
     options.members = ALL
-    assert_result_contains('.. class:: Class', 'module', 'test_autodoc')
+    assert_result_contains('.. class:: Class(arg)', 'module', 'test_autodoc')
     try:
         assert_result_contains('.. exception:: CustomEx',
                                'module', 'test_autodoc')
     udocattr = 'quux'
     u"""should be documented as well - süß"""
 
+    def __init__(self, arg):
+        #: a documented instance attribute
+        self.inst_attr_comment = None
+        self.inst_attr_string = None
+        """a documented instance attribute"""
+
+
 class CustomDict(dict):
     """Docstring."""