mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
First iteration of an autodoc that handles attribute documentation.
This commit is contained in:
parent
0edeba7acb
commit
83cf763804
@ -22,6 +22,7 @@ from docutils.parsers.rst import directives
|
|||||||
from docutils.statemachine import ViewList
|
from docutils.statemachine import ViewList
|
||||||
|
|
||||||
from sphinx.util import rpartition, nested_parse_with_titles
|
from sphinx.util import rpartition, nested_parse_with_titles
|
||||||
|
from sphinx.pycode import ModuleAnalyzer, PycodeError
|
||||||
|
|
||||||
clstypes = (type, ClassType)
|
clstypes = (type, ClassType)
|
||||||
try:
|
try:
|
||||||
@ -313,7 +314,7 @@ class RstGenerator(object):
|
|||||||
'for automodule %s' % name)
|
'for automodule %s' % name)
|
||||||
return (path or '') + base, [], None, None
|
return (path or '') + base, [], None, None
|
||||||
|
|
||||||
elif what in ('exception', 'function', 'class'):
|
elif what in ('exception', 'function', 'class', 'data'):
|
||||||
if mod is None:
|
if mod is None:
|
||||||
if path:
|
if path:
|
||||||
mod = path.rstrip('.')
|
mod = path.rstrip('.')
|
||||||
@ -434,7 +435,9 @@ class RstGenerator(object):
|
|||||||
modfile = None # e.g. for builtin and C modules
|
modfile = None # e.g. for builtin and C modules
|
||||||
for part in objpath:
|
for part in objpath:
|
||||||
todoc = getattr(todoc, part)
|
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", '
|
self.warn('autodoc can\'t import/find %s %r, it reported error: "%s", '
|
||||||
'please check your spelling and sys.path' %
|
'please check your spelling and sys.path' %
|
||||||
(what, str(fullname), err))
|
(what, str(fullname), err))
|
||||||
@ -503,6 +506,15 @@ class RstGenerator(object):
|
|||||||
else:
|
else:
|
||||||
sourcename = 'docstring of %s' % fullname
|
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
|
# add content from docstrings
|
||||||
if not no_docstring:
|
if not no_docstring:
|
||||||
for i, line in enumerate(self.get_doc(what, fullname, todoc)):
|
for i, line in enumerate(self.get_doc(what, fullname, todoc)):
|
||||||
@ -524,9 +536,9 @@ class RstGenerator(object):
|
|||||||
self.env.autodoc_current_class = objpath[0]
|
self.env.autodoc_current_class = objpath[0]
|
||||||
|
|
||||||
# add members, if possible
|
# add members, if possible
|
||||||
_all = members == ['__all__']
|
all_members = members == ['__all__']
|
||||||
members_check_module = False
|
members_check_module = False
|
||||||
if _all:
|
if all_members:
|
||||||
# unqualified :members: given
|
# unqualified :members: given
|
||||||
if what == 'module':
|
if what == 'module':
|
||||||
if hasattr(todoc, '__all__'):
|
if hasattr(todoc, '__all__'):
|
||||||
@ -555,14 +567,28 @@ class RstGenerator(object):
|
|||||||
else:
|
else:
|
||||||
all_members = [(mname, getattr(todoc, mname)) for mname in members]
|
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:
|
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
|
# ignore members whose name starts with _ by default
|
||||||
skip = True
|
skip = True
|
||||||
else:
|
else:
|
||||||
# ignore undocumented members if :undoc-members: is not given
|
if (namespace, membername) in attr_docs:
|
||||||
doc = getattr(member, '__doc__', None)
|
# keep documented attributes
|
||||||
skip = not self.options.undoc_members and not doc
|
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
|
# give the user a chance to decide whether this member should be skipped
|
||||||
if self.env.app:
|
if self.env.app:
|
||||||
# let extensions preprocess docstrings
|
# let extensions preprocess docstrings
|
||||||
@ -573,10 +599,11 @@ class RstGenerator(object):
|
|||||||
if skip:
|
if skip:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
content = None
|
|
||||||
if what == 'module':
|
if what == 'module':
|
||||||
if isinstance(member, (FunctionType, BuiltinFunctionType)):
|
if isinstance(member, (FunctionType, BuiltinFunctionType)):
|
||||||
memberwhat = 'function'
|
memberwhat = 'function'
|
||||||
|
elif isattr:
|
||||||
|
memberwhat = 'attribute'
|
||||||
elif isinstance(member, clstypes):
|
elif isinstance(member, clstypes):
|
||||||
if member.__name__ != membername:
|
if member.__name__ != membername:
|
||||||
# assume it's aliased
|
# assume it's aliased
|
||||||
@ -588,10 +615,13 @@ class RstGenerator(object):
|
|||||||
else:
|
else:
|
||||||
memberwhat = 'class'
|
memberwhat = 'class'
|
||||||
else:
|
else:
|
||||||
# XXX: todo -- attribute docs
|
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
if isinstance(member, clstypes):
|
if inspect.isroutine(member):
|
||||||
|
memberwhat = 'method'
|
||||||
|
elif isattr:
|
||||||
|
memberwhat = 'attribute'
|
||||||
|
elif isinstance(member, clstypes):
|
||||||
if member.__name__ != membername:
|
if member.__name__ != membername:
|
||||||
# assume it's aliased
|
# assume it's aliased
|
||||||
memberwhat = 'attribute'
|
memberwhat = 'attribute'
|
||||||
@ -599,12 +629,9 @@ class RstGenerator(object):
|
|||||||
source='')
|
source='')
|
||||||
else:
|
else:
|
||||||
memberwhat = 'class'
|
memberwhat = 'class'
|
||||||
elif inspect.isroutine(member):
|
|
||||||
memberwhat = 'method'
|
|
||||||
elif isdescriptor(member):
|
elif isdescriptor(member):
|
||||||
memberwhat = 'attribute'
|
memberwhat = 'attribute'
|
||||||
else:
|
else:
|
||||||
# XXX: todo -- attribute docs
|
|
||||||
continue
|
continue
|
||||||
# give explicitly separated module name, so that members of inner classes
|
# give explicitly separated module name, so that members of inner classes
|
||||||
# can be documented
|
# can be documented
|
||||||
|
@ -43,7 +43,7 @@ def prepare_commentdoc(s):
|
|||||||
result.append(line[3:])
|
result.append(line[3:])
|
||||||
if result and result[-1]:
|
if result and result[-1]:
|
||||||
result.append('')
|
result.append('')
|
||||||
return '\n'.join(result)
|
return result
|
||||||
|
|
||||||
|
|
||||||
_eq = pytree.Leaf(token.EQUAL, '=')
|
_eq = pytree.Leaf(token.EQUAL, '=')
|
||||||
@ -57,7 +57,7 @@ class ClassAttrVisitor(pytree.NodeVisitor):
|
|||||||
def init(self, scope):
|
def init(self, scope):
|
||||||
self.scope = scope
|
self.scope = scope
|
||||||
self.namespace = []
|
self.namespace = []
|
||||||
self.collected = []
|
self.collected = {}
|
||||||
|
|
||||||
def visit_classdef(self, node):
|
def visit_classdef(self, node):
|
||||||
self.namespace.append(node[1].value)
|
self.namespace.append(node[1].value)
|
||||||
@ -87,9 +87,9 @@ class ClassAttrVisitor(pytree.NodeVisitor):
|
|||||||
if target.type != token.NAME:
|
if target.type != token.NAME:
|
||||||
# don't care about complex targets
|
# don't care about complex targets
|
||||||
continue
|
continue
|
||||||
name = '.'.join(self.namespace + [target.value])
|
namespace = '.'.join(self.namespace)
|
||||||
if name.startswith(self.scope):
|
if namespace.startswith(self.scope):
|
||||||
self.collected.append((name, docstring))
|
self.collected[namespace, target.value] = docstring
|
||||||
|
|
||||||
def visit_funcdef(self, node):
|
def visit_funcdef(self, node):
|
||||||
# don't descend into functions -- nothing interesting there
|
# don't descend into functions -- nothing interesting there
|
||||||
@ -105,6 +105,8 @@ class PycodeError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class ModuleAnalyzer(object):
|
class ModuleAnalyzer(object):
|
||||||
|
# cache for analyzer objects
|
||||||
|
cache = {}
|
||||||
|
|
||||||
def __init__(self, tree, modname, srcname):
|
def __init__(self, tree, modname, srcname):
|
||||||
self.tree = tree
|
self.tree = tree
|
||||||
@ -131,6 +133,8 @@ class ModuleAnalyzer(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_module(cls, modname):
|
def for_module(cls, modname):
|
||||||
|
if modname in cls.cache:
|
||||||
|
return cls.cache[modname]
|
||||||
if modname not in sys.modules:
|
if modname not in sys.modules:
|
||||||
try:
|
try:
|
||||||
__import__(modname)
|
__import__(modname)
|
||||||
@ -142,7 +146,9 @@ class ModuleAnalyzer(object):
|
|||||||
source = mod.__loader__.get_source(modname)
|
source = mod.__loader__.get_source(modname)
|
||||||
except Exception, err:
|
except Exception, err:
|
||||||
raise PycodeError('error getting source for %r' % modname, 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)
|
filename = getattr(mod, '__file__', None)
|
||||||
if filename is None:
|
if filename is None:
|
||||||
raise PycodeError('no source found for module %r' % modname)
|
raise PycodeError('no source found for module %r' % modname)
|
||||||
@ -153,13 +159,16 @@ class ModuleAnalyzer(object):
|
|||||||
raise PycodeError('source is not a .py file: %r' % filename)
|
raise PycodeError('source is not a .py file: %r' % filename)
|
||||||
if not path.isfile(filename):
|
if not path.isfile(filename):
|
||||||
raise PycodeError('source file is not present: %r' % 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):
|
def find_attr_docs(self, scope=''):
|
||||||
attr_visitor = ClassAttrVisitor(number2name, '')
|
attr_visitor = ClassAttrVisitor(number2name, scope)
|
||||||
attr_visitor.visit(self.tree)
|
attr_visitor.visit(self.tree)
|
||||||
return attr_visitor.collected
|
return attr_visitor.collected
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
x0 = time.time()
|
x0 = time.time()
|
||||||
ma = ModuleAnalyzer.for_file('sphinx/builders/html.py', 'sphinx.builders.html')
|
ma = ModuleAnalyzer.for_file('sphinx/builders/html.py', 'sphinx.builders.html')
|
||||||
|
Loading…
Reference in New Issue
Block a user