#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) Release 1.0 (in development)
============================ ============================
* #280: Autodoc can now document instance attributes assigned in
``__init__`` methods.
* Added ``alt`` option to ``graphviz`` extension directives. * Added ``alt`` option to ``graphviz`` extension directives.
* Added Epub builder. * Added Epub builder.

View File

@ -72,6 +72,7 @@ class Options(dict):
ALL = object() ALL = object()
INSTANCEATTR = object()
def members_option(arg): def members_option(arg):
"""Used to convert the :members: option to auto directives.""" """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' self.directive.warn('missing attribute %s in object %s'
% (mname, self.fullname)) % (mname, self.fullname))
return False, ret return False, ret
elif self.options.inherited_members:
if self.options.inherited_members:
# safe_getmembers() uses dir() which pulls in members from all # safe_getmembers() uses dir() which pulls in members from all
# base classes # base classes
return False, safe_getmembers(self.object) members = safe_getmembers(self.object)
else: else:
# __dict__ contains only the members directly defined in # __dict__ contains only the members directly defined in
# the class (but get them via getattr anyway, to e.g. get # the class (but get them via getattr anyway, to e.g. get
# unbound method objects instead of function objects); # unbound method objects instead of function objects);
# using keys() because apparently there are objects for which # using keys() because apparently there are objects for which
# __dict__ changes while getting attributes # __dict__ changes while getting attributes
return False, sorted([ obj_dict = self.get_attr(self.object, '__dict__')
(mname, self.get_attr(self.object, mname, None)) members = [(mname, self.get_attr(self.object, mname, None))
for mname in self.get_attr(self.object, '__dict__').keys()]) 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): def filter_members(self, members, want_all):
""" """
@ -1036,6 +1048,34 @@ class AttributeDocumenter(ClassLevelDocumenter):
pass 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): class AutoDirective(Directive):
""" """
The AutoDirective class is used for all autodoc directives. It dispatches 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(FunctionDocumenter)
app.add_autodocumenter(MethodDocumenter) app.add_autodocumenter(MethodDocumenter)
app.add_autodocumenter(AttributeDocumenter) app.add_autodocumenter(AttributeDocumenter)
app.add_autodocumenter(InstanceAttributeDocumenter)
app.add_config_value('autoclass_content', 'class', True) app.add_config_value('autoclass_content', 'class', True)
app.add_config_value('autodoc_member_order', 'alphabetic', 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): class AttrDocVisitor(nodes.NodeVisitor):
""" """
Visitor that collects docstrings for attribute assignments on toplevel and 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 The docstrings can either be in special '#:' comments before the assignment
or in a docstring after it. or in a docstring after it.
""" """
def init(self, scope, encoding): def init(self, scope, encoding):
self.scope = scope self.scope = scope
self.in_init = 0
self.encoding = encoding self.encoding = encoding
self.namespace = [] self.namespace = []
self.collected = {} self.collected = {}
def visit_classdef(self, node): def visit_classdef(self, node):
"""Visit a class."""
self.namespace.append(node[1].value) self.namespace.append(node[1].value)
self.generic_visit(node) self.generic_visit(node)
self.namespace.pop() 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): def visit_expr_stmt(self, node):
"""Visit an assignment which may have a special comment before it.""" """Visit an assignment which may have a special comment before it."""
if _eq not in node.children: if _eq not in node.children:
@ -97,20 +108,32 @@ class AttrDocVisitor(nodes.NodeVisitor):
docstring = prepare_docstring(docstring) docstring = prepare_docstring(docstring)
self.add_docstring(prev[0], 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): def add_docstring(self, node, docstring):
# add an item for each assignment target # add an item for each assignment target
for i in range(0, len(node) - 1, 2): for i in range(0, len(node) - 1, 2):
target = node[i] target = node[i]
if target.type != token.NAME: if self.in_init and self.number2name[target.type] == 'power':
# don't care about complex targets # 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 continue
else:
name = target.value
namespace = '.'.join(self.namespace) namespace = '.'.join(self.namespace)
if namespace.startswith(self.scope): if namespace.startswith(self.scope):
self.collected[namespace, target.value] = docstring self.collected[namespace, name] = docstring
class PycodeError(Exception): class PycodeError(Exception):

View File

@ -382,7 +382,10 @@ def test_generate():
('attribute', 'test_autodoc.Class.descr'), ('attribute', 'test_autodoc.Class.descr'),
('attribute', 'test_autodoc.Class.attr'), ('attribute', 'test_autodoc.Class.attr'),
('attribute', 'test_autodoc.Class.docattr'), ('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 options.members = ALL
assert_processes(should, 'class', 'Class') assert_processes(should, 'class', 'Class')
options.undoc_members = True options.undoc_members = True
@ -403,7 +406,7 @@ def test_generate():
assert_result_contains(' :platform: Platform', 'module', 'test_autodoc') assert_result_contains(' :platform: Platform', 'module', 'test_autodoc')
# test if __all__ is respected for modules # test if __all__ is respected for modules
options.members = ALL options.members = ALL
assert_result_contains('.. class:: Class', 'module', 'test_autodoc') assert_result_contains('.. class:: Class(arg)', 'module', 'test_autodoc')
try: try:
assert_result_contains('.. exception:: CustomEx', assert_result_contains('.. exception:: CustomEx',
'module', 'test_autodoc') 'module', 'test_autodoc')
@ -499,6 +502,13 @@ class Class(Base):
udocattr = 'quux' udocattr = 'quux'
u"""should be documented as well - süß""" 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): class CustomDict(dict):
"""Docstring.""" """Docstring."""