mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
#280: Autodoc can now document instance attributes assigned in `__init__
` methods.
This commit is contained in:
parent
268911eb47
commit
09147448d4
3
CHANGES
3
CHANGES
@ -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.
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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."""
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user