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

This commit is contained in:
Georg Brandl 2010-01-03 13:59:30 +01:00
parent 268911eb47
commit 09147448d4
4 changed files with 92 additions and 15 deletions

View File

@ -1,6 +1,9 @@
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.

View File

@ -72,6 +72,7 @@ class Options(dict):
ALL = object()
INSTANCEATTR = object()
def members_option(arg):
"""Used to convert the :members: option to auto directives."""
@ -474,19 +475,30 @@ class Documenter(object):
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):
"""
@ -1036,6 +1048,34 @@ class AttributeDocumenter(ClassLevelDocumenter):
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
@ -1132,6 +1172,7 @@ def setup(app):
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)

View File

@ -45,22 +45,33 @@ _eq = nodes.Leaf(token.EQUAL, '=')
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:
@ -97,20 +108,32 @@ class AttrDocVisitor(nodes.NodeVisitor):
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):

View File

@ -382,7 +382,10 @@ def test_generate():
('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
@ -403,7 +406,7 @@ def test_generate():
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')
@ -499,6 +502,13 @@ class Class(Base):
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."""