summaryrefslogtreecommitdiff
path: root/sphinx/ext/autodoc.py
diff options
context:
space:
mode:
authorGeorg Brandl <georg@python.org>2009-02-17 16:36:30 +0100
committerGeorg Brandl <georg@python.org>2009-02-17 16:36:30 +0100
commitf2f62501e144ed8cbcff3e1dda5276fb4464c265 (patch)
tree5504b2d754ff0c2b6bc276b984585a41ef99955c /sphinx/ext/autodoc.py
parent82842e8d8e2ab9566e445c1aecf4bfed68b1b4c8 (diff)
downloadsphinx-git-f2f62501e144ed8cbcff3e1dda5276fb4464c265.tar.gz
Refactor autodoc so that it gets easy to add support for custom types of objects.
Diffstat (limited to 'sphinx/ext/autodoc.py')
-rw-r--r--sphinx/ext/autodoc.py1175
1 files changed, 696 insertions, 479 deletions
diff --git a/sphinx/ext/autodoc.py b/sphinx/ext/autodoc.py
index 662682f38..b8e048e85 100644
--- a/sphinx/ext/autodoc.py
+++ b/sphinx/ext/autodoc.py
@@ -14,18 +14,20 @@
import re
import sys
import inspect
-import linecache
-from types import FunctionType, BuiltinFunctionType, MethodType, ClassType
+from types import ModuleType, FunctionType, BuiltinFunctionType, MethodType, \
+ ClassType
from docutils import nodes
-from docutils.parsers.rst import directives
+from docutils.utils import assemble_option_dict
from docutils.statemachine import ViewList
from sphinx.util import rpartition, nested_parse_with_titles, force_decode
from sphinx.pycode import ModuleAnalyzer, PycodeError
+from sphinx.application import ExtensionError
+from sphinx.util.compat import Directive
from sphinx.util.docstrings import prepare_docstring
-clstypes = (type, ClassType)
+
try:
base_exception = BaseException
except NameError:
@@ -42,22 +44,39 @@ py_ext_sig_re = re.compile(
''', re.VERBOSE)
-class Options(object):
- pass
+class DefDict(dict):
+ def __init__(self, default):
+ dict.__init__(self)
+ self.default = default
+ def __getitem__(self, key):
+ try:
+ return dict.__getitem__(self, key)
+ except KeyError:
+ return self.default
+ def __nonzero__(self):
+ # docutils check "if option_spec"
+ return True
+identity = lambda x: x
-def get_method_type(obj):
- """
- Return the method type for an object: method, staticmethod or classmethod.
- """
- if isinstance(obj, classmethod) or \
- (isinstance(obj, MethodType) and obj.im_self is not None):
- return 'classmethod'
- elif isinstance(obj, FunctionType) or \
- (isinstance(obj, BuiltinFunctionType) and obj.__self__ is not None):
- return 'staticmethod'
- else:
- return 'method'
+
+class Options(dict):
+ def __getattr__(self, name):
+ try:
+ return self[name.replace('_', '-')]
+ except KeyError:
+ return False
+
+
+ALL = object()
+
+def members_option(arg):
+ if arg is None:
+ return ALL
+ return [x.strip() for x in arg.split(',')]
+
+def bool_option(arg):
+ return True
class AutodocReporter(object):
@@ -171,229 +190,327 @@ def isdescriptor(x):
return False
-class RstGenerator(object):
- def __init__(self, options, document, lineno):
- self.options = options
- self.env = document.settings.env
- self.reporter = document.reporter
- self.lineno = lineno
- self.filename_set = set()
- self.warnings = []
- self.result = ViewList()
-
- def warn(self, msg):
- self.warnings.append(self.reporter.warning(msg, line=self.lineno))
-
- def get_doc(self, what, obj, encoding=None):
- """Decode and return lines of the docstring(s) for the object."""
- docstrings = []
-
- # add the regular docstring if present
- if getattr(obj, '__doc__', None):
- docstrings.append(obj.__doc__)
-
- # skip some lines in module docstrings if configured (deprecated!)
- if what == 'module' and self.env.config.automodule_skip_lines \
- and docstrings:
- docstrings[0] = '\n'.join(docstrings[0].splitlines()
- [self.env.config.automodule_skip_lines:])
-
- # for classes, what the "docstring" is can be controlled via an option
- if what in ('class', 'exception'):
- content = self.env.config.autoclass_content
- if content in ('both', 'init'):
- initdocstring = getattr(obj, '__init__', None).__doc__
- # for new-style classes, no __init__ means default __init__
- if initdocstring == object.__init__.__doc__:
- initdocstring = None
- if initdocstring:
- if content == 'init':
- docstrings = [initdocstring]
- else:
- docstrings.append(initdocstring)
- # the default is only the class docstring
-
- # make sure we have Unicode docstrings, then sanitize and split
- # into lines
- return [prepare_docstring(force_decode(docstring, encoding))
- for docstring in docstrings]
-
- def process_doc(self, docstrings, what, name, obj):
- """Let the user process the docstrings."""
- for docstringlines in docstrings:
- if self.env.app:
- # let extensions preprocess docstrings
- self.env.app.emit('autodoc-process-docstring',
- what, name, obj, self.options, docstringlines)
- for line in docstringlines:
- yield line
-
- def resolve_name(self, what, name):
+class Documenter(object):
+ # name by which the directive is called (auto...) and the default
+ # generated directive name
+ objtype = 'object'
+ # indentation by which to indent the directive content
+ content_indent = u' '
+ # priority if multiple documenters return True from can_document_member
+ priority = 0
+
+ option_spec = {'noindex': bool_option}
+
+ special_attrgetters = {}
+
+ @classmethod
+ def get_attr(cls, obj, name, *defargs):
+ """getattr() override for types such as Zope interfaces."""
+ for typ, func in cls.special_attrgetters.iteritems():
+ if isinstance(obj, typ):
+ return func(obj, name, *defargs)
+ return getattr(obj, name, *defargs)
+
+ @classmethod
+ def can_document_member(cls, member, membername, isattr, parent):
+ """Called to see if a member can be documented by this documenter."""
+ raise NotImplementedError('must be implemented in subclasses')
+
+ def __init__(self, directive, name, indent=u''):
+ self.directive = directive
+ self.env = directive.env
+ self.options = directive.genopt
+ self.name = name
+ self.indent = indent
+ # the module and object path within the module, and the fully
+ # qualified name (all set after resolve_name succeeds)
+ self.modname = None
+ self.module = None
+ self.objpath = None
+ self.fullname = None
+ # extra signature items (arguments and return annotation,
+ # also set after resolve_name succeeds)
+ self.args = None
+ self.retann = None
+ # the object to document (set after import_object succeeds)
+ self.object = None
+ # the module analyzer to get at attribute docs, or None
+ self.analyzer = None
+
+ def add_line(self, line, source, *lineno):
+ self.directive.result.append(self.indent + line, source, *lineno)
+
+ def resolve_name(self, modname, parents, path, base):
+ raise NotImplementedError('must be implemented in subclasses')
+
+ def parse_name(self):
"""
Determine what module to import and what attribute to document.
- Returns a tuple of: the full name, the module name, a path of
- names to get via getattr, the signature and return annotation.
+ Returns True if parsing and resolving was successful.
"""
# first, parse the definition -- auto directives for classes and
# functions can contain a signature which is then used instead of
# an autogenerated one
try:
- mod, path, base, args, retann = py_ext_sig_re.match(name).groups()
- except:
- self.warn('invalid signature for auto%s (%r)' % (what, name))
- return None, [], None, None
+ explicit_modname, path, base, args, retann = \
+ py_ext_sig_re.match(self.name).groups()
+ except AttributeError:
+ self.directive.warn('invalid signature for auto%s (%r)' %
+ (self.objtype, self.name))
+ return False
# support explicit module and class name separation via ::
- if mod is not None:
- mod = mod[:-2]
+ if explicit_modname is not None:
+ modname = explicit_modname[:-2]
parents = path and path.rstrip('.').split('.') or []
else:
+ modname = None
parents = []
- if what == 'module':
- if mod is not None:
- self.warn('"::" in automodule name doesn\'t make sense')
- if args or retann:
- self.warn('ignoring signature arguments and return annotation '
- 'for automodule %s' % name)
- return (path or '') + base, [], None, None
-
- elif what in ('exception', 'function', 'class', 'data'):
- if mod is None:
- if path:
- mod = path.rstrip('.')
- else:
- # if documenting a toplevel object without explicit module,
- # it can be contained in another auto directive ...
- if hasattr(self.env, 'autodoc_current_module'):
- mod = self.env.autodoc_current_module
- # ... or in the scope of a module directive
- if not mod:
- mod = self.env.currmodule
- return mod, parents + [base], args, retann
+ self.modname, self.objpath = \
+ self.resolve_name(modname, parents, path, base)
- else:
- if mod is None:
- if path:
- mod_cls = path.rstrip('.')
- else:
- mod_cls = None
- # if documenting a class-level object without path,
- # there must be a current class, either from a parent
- # auto directive ...
- if hasattr(self.env, 'autodoc_current_class'):
- mod_cls = self.env.autodoc_current_class
- # ... or from a class directive
- if mod_cls is None:
- mod_cls = self.env.currclass
- # ... if still None, there's no way to know
- if mod_cls is None:
- return None, [], None, None
- mod, cls = rpartition(mod_cls, '.')
- parents = [cls]
- # if the module name is still missing, get it like above
- if not mod and hasattr(self.env, 'autodoc_current_module'):
- mod = self.env.autodoc_current_module
- if not mod:
- mod = self.env.currmodule
- return mod, parents + [base], args, retann
-
- def format_signature(self, what, name, obj, args, retann):
- """
- Return the signature of the object, formatted for display.
- """
- if what not in ('class', 'method', 'staticmethod', 'classmethod',
- 'function'):
- return ''
+ self.args = args
+ self.retann = retann
+ self.fullname = self.modname + (self.objpath and
+ '.' + '.'.join(self.objpath) or '')
+ return True
+ def import_object(self):
+ try:
+ __import__(self.modname)
+ obj = self.module = sys.modules[self.modname]
+ for part in self.objpath:
+ obj = self.get_attr(obj, part)
+ self.object = obj
+ return True
+ except (ImportError, AttributeError), err:
+ self.directive.warn(
+ 'autodoc can\'t import/find %s %r, it reported error: '
+ '"%s", please check your spelling and sys.path' %
+ (self.objtype, str(self.fullname), err))
+ return False
+
+ def get_real_modname(self):
+ return self.get_attr(self.object, '__module__', None) or self.modname
+
+ def check_module(self):
+ modname = self.get_attr(self.object, '__module__', None)
+ if modname and modname != self.modname:
+ return False
+ return True
+
+ def format_args(self):
+ return None
+
+ def format_signature(self):
err = None
- if args is not None:
+ if self.args is not None:
# signature given explicitly
- args = "(%s)" % args
+ args = "(%s)" % self.args
else:
# try to introspect the signature
try:
- args = None
- getargs = True
- if what == 'class':
- # for classes, the relevant signature is the
- # __init__ method's
- obj = getattr(obj, '__init__', None)
- # classes without __init__ method, default __init__ or
- # __init__ written in C?
- if obj is None or obj is object.__init__ or not \
- (inspect.ismethod(obj) or inspect.isfunction(obj)):
- getargs = False
- elif inspect.isbuiltin(obj) or inspect.ismethoddescriptor(obj):
- # can never get arguments of a C function or method
- getargs = False
- if getargs:
- try:
- argspec = inspect.getargspec(obj)
- except TypeError:
- # if a class should be documented as function (yay duck
- # typing) we try to use the constructor signature as function
- # signature without the first argument.
- try:
- argspec = inspect.getargspec(obj.__new__)
- except TypeError:
- argspec = inspect.getargspec(obj.__init__)
- if argspec[0]:
- del argspec[0][0]
- if what in ('class', 'method', 'staticmethod',
- 'classmethod') and argspec[0] and \
- argspec[0][0] in ('cls', 'self'):
- del argspec[0][0]
- args = inspect.formatargspec(*argspec)
+ args = self.format_args()
except Exception, e:
args = None
err = e
+ if args is None:
+ return ''
+ retann = self.retann
result = self.env.app.emit_firstresult(
- 'autodoc-process-signature', what, name, obj,
- self.options, args, retann)
+ 'autodoc-process-signature', self.objtype, self.fullname,
+ self.object, self.options, args, retann)
if result:
args, retann = result
if args is not None:
- return '%s%s' % (args, retann and (' -> %s' % retann) or '')
+ return args + (retann and (' -> %s' % retann) or '')
elif err:
# re-raise the error for perusal of the handler in generate()
raise RuntimeError(err)
else:
return ''
- def generate(self, what, name, members, add_content, indent=u'',
- check_module=False, no_docstring=False, real_module=None):
- """
- Generate reST for the object in self.result.
- """
- mod, objpath, args, retann = self.resolve_name(what, name)
- if not mod:
+ def add_directive_header(self, sig):
+ directive = getattr(self, 'directivetype', self.objtype)
+ # the name to put into the generated directive -- doesn't contain
+ # the module (except for module directive of course)
+ name_in_directive = '.'.join(self.objpath) or self.modname
+ self.add_line(u'.. %s:: %s%s' % (directive, name_in_directive, sig),
+ '<autodoc>')
+ if self.options.noindex:
+ self.add_line(u' :noindex:', '<autodoc>')
+ if self.objpath:
+ # Be explicit about the module, this is necessary since .. class::
+ # etc. don't support a prepended module name
+ self.add_line(u' :module: %s' % self.modname, '<autodoc>')
+
+ def get_doc(self, encoding=None):
+ """Decode and return lines of the docstring(s) for the object."""
+ docstring = self.get_attr(self.object, '__doc__', None)
+ if docstring:
+ # make sure we have Unicode docstrings, then sanitize and split
+ # into lines
+ return [prepare_docstring(force_decode(docstring, encoding))]
+ return []
+
+ def process_doc(self, docstrings):
+ """Let the user process the docstrings."""
+ for docstringlines in docstrings:
+ if self.env.app:
+ # let extensions preprocess docstrings
+ self.env.app.emit('autodoc-process-docstring',
+ self.objtype, self.fullname, self.object,
+ self.options, docstringlines)
+ for line in docstringlines:
+ yield line
+
+ def add_content(self, more_content, no_docstring=False):
+ # set sourcename and add content from attribute documentation
+ if self.analyzer:
+ # prevent encoding errors when the file name is non-ASCII
+ filename = unicode(self.analyzer.srcname,
+ sys.getfilesystemencoding(), 'replace')
+ sourcename = u'%s:docstring of %s' % (filename, self.fullname)
+
+ attr_docs = self.analyzer.find_attr_docs()
+ if self.objpath:
+ key = ('.'.join(self.objpath[:-1]), self.objpath[-1])
+ if key in attr_docs:
+ no_docstring = True
+ docstrings = [attr_docs[key]]
+ for i, line in enumerate(self.process_doc(docstrings)):
+ self.add_line(line, sourcename, i)
+ else:
+ sourcename = u'docstring of %s' % self.fullname
+
+ # add content from docstrings
+ if not no_docstring:
+ encoding = self.analyzer and self.analyzer.encoding
+ docstrings = self.get_doc(encoding)
+ for i, line in enumerate(self.process_doc(docstrings)):
+ self.add_line(line, sourcename, i)
+
+ # add additional content (e.g. from document), if present
+ if more_content:
+ for line, src in zip(more_content.data, more_content.items):
+ self.add_line(line, src[0], src[1])
+
+ def get_object_members(self, members=ALL):
+ if members is not ALL:
+ # specific members given
+ ret = []
+ for mname in members:
+ try:
+ ret.append((mname, self.get_attr(self.object, mname)))
+ except AttributeError:
+ self.directive.warn('missing attribute %s in object %s: '
+ % (mname, self.fullname))
+ return False, ret
+ elif self.options.inherited_members:
+ # getmembers() uses dir() which pulls in members from all
+ # base classes
+ return False, inspect.getmembers(self.object)
+ else:
+ # __dict__ contains only the members directly defined in
+ # the class
+ return False, sorted(self.object.__dict__.iteritems())
+
+ def filter_members(self, members, want_all):
+ ret = []
+
+ # search for members in source code too
+ namespace = '.'.join(self.objpath) # will be empty for modules
+
+ if self.analyzer:
+ attr_docs = self.analyzer.find_attr_docs()
+ else:
+ attr_docs = {}
+
+ # process members and determine which to skip
+ for (membername, member) in members:
+ # if isattr is True, the member is documented as an attribute
+ isattr = False
+
+ if want_all and membername.startswith('_'):
+ # ignore members whose name starts with _ by default
+ skip = True
+ elif (namespace, membername) in attr_docs:
+ # keep documented attributes
+ skip = False
+ isattr = True
+ else:
+ # ignore undocumented members if :undoc-members:
+ # is not given
+ doc = self.get_attr(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
+ if self.env.app:
+ # let extensions preprocess docstrings
+ skip_user = self.env.app.emit_firstresult(
+ 'autodoc-skip-member', self.objtype, membername, member,
+ skip, self.options)
+ if skip_user is not None:
+ skip = skip_user
+ if skip:
+ continue
+
+ ret.append((membername, member, isattr))
+
+ return ret
+
+ def document_members(self, all_members=False):
+ # set current namespace for finding members
+ self.env.autodoc_current_module = self.modname
+ if self.objpath:
+ self.env.autodoc_current_class = self.objpath[0]
+
+ want_all = all_members or self.options.inherited_members or \
+ self.options.members is ALL
+ # find out which members are documentable
+ members_check_module, members = self.get_object_members(want_all)
+
+ # document non-skipped members
+ for (mname, member, isattr) in self.filter_members(members, want_all):
+ classes = [cls for cls in AutoDirective._registry.itervalues()
+ if cls.can_document_member(member, mname, isattr, self)]
+ if not classes:
+ # don't know how to document this member
+ continue
+ # prefer the documenter with the highest priority
+ classes.sort(lambda cls: cls.priority)
+ # give explicitly separated module name, so that members
+ # of inner classes can be documented
+ full_mname = self.modname + '::' + \
+ '.'.join(self.objpath + [mname])
+ memberdocmtr = classes[-1](self.directive, full_mname,
+ self.indent)
+ memberdocmtr.generate(all_members=True,
+ real_modname=self.real_modname,
+ check_module=members_check_module)
+
+ # reset current objects
+ self.env.autodoc_current_module = None
+ self.env.autodoc_current_class = None
+
+ def generate(self, more_content=None, real_modname=None,
+ check_module=False, all_members=False):
+ if not self.parse_name():
# need a module to import
- self.warn('don\'t know which module to import for autodocumenting '
- '%r (try placing a "module" or "currentmodule" directive '
- 'in the document, or giving an explicit module name)'
- % name)
+ self.directive.warn(
+ 'don\'t know which module to import for autodocumenting '
+ '%r (try placing a "module" or "currentmodule" directive '
+ 'in the document, or giving an explicit module name)'
+ % self.name)
return
- # fully-qualified name
- fullname = mod + (objpath and '.' + '.'.join(objpath) or '')
-
- # the name to put into the generated directive -- doesn't contain
- # the module
- name_in_directive = '.'.join(objpath) or mod
# now, import the module and get object to document
- try:
- __import__(mod)
- todoc = module = sys.modules[mod]
- for part in objpath:
- todoc = getattr(todoc, part)
- except (ImportError, AttributeError), err:
- self.warn('autodoc can\'t import/find %s %r, it reported error: '
- '"%s", please check your spelling and sys.path' %
- (what, str(fullname), err))
+ if not self.import_object():
return
# If there is no real module defined, figure out which to use.
@@ -401,327 +518,427 @@ class RstGenerator(object):
# where the attribute documentation would actually be found in.
# This is used for situations where you have a module that collects the
# functions and classes of internal submodules.
- if real_module is None:
- real_module = getattr(todoc, '__module__', None) or mod
+ self.real_modname = real_modname or self.get_real_modname()
# try to also get a source code analyzer for attribute docs
try:
- analyzer = ModuleAnalyzer.for_module(real_module)
+ self.analyzer = ModuleAnalyzer.for_module(self.real_modname)
# parse right now, to get PycodeErrors on parsing
- analyzer.parse()
+ self.analyzer.parse()
except PycodeError, err:
# no source file -- e.g. for builtin and C modules
- analyzer = None
+ self.analyzer = None
else:
- self.filename_set.add(analyzer.srcname)
+ self.directive.filename_set.add(self.analyzer.srcname)
- # check __module__ of object for members not given explicitly
+ # check __module__ of object (for members not given explicitly)
if check_module:
- if hasattr(todoc, '__module__'):
- if todoc.__module__ != mod:
- return
+ if not self.check_module():
+ return
# make sure that the result starts with an empty line. This is
# necessary for some situations where another directive preprocesses
# reST and no starting newline is present
- self.result.append(u'', '')
+ self.add_line(u'', '')
# format the object's signature, if any
try:
- sig = self.format_signature(what, fullname, todoc, args, retann)
+ sig = self.format_signature()
except Exception, err:
- self.warn('error while formatting signature for %s: %s' %
- (fullname, err))
+ self.directive.warn('error while formatting signature for '
+ '%s: %s' % (self.fullname, err))
sig = ''
- # now, create the directive header
- if what == 'method':
- directive = get_method_type(todoc)
- else:
- directive = what
- self.result.append(indent + u'.. %s:: %s%s' %
- (directive, name_in_directive, sig), '<autodoc>')
- if what == 'module':
- # Add some module-specific options
- if self.options.synopsis:
- self.result.append(indent + u' :synopsis: ' +
- self.options.synopsis, '<autodoc>')
- if self.options.platform:
- self.result.append(indent + u' :platform: ' +
- self.options.platform, '<autodoc>')
- if self.options.deprecated:
- self.result.append(indent + u' :deprecated:', '<autodoc>')
- else:
- # Be explicit about the module, this is necessary since .. class::
- # doesn't support a prepended module name
- self.result.append(indent + u' :module: %s' % mod, '<autodoc>')
- if self.options.noindex:
- self.result.append(indent + u' :noindex:', '<autodoc>')
- self.result.append(u'', '<autodoc>')
+ # generate the directive header and options, if applicable
+ self.add_directive_header(sig)
+ self.add_line(u'', '<autodoc>')
+
+ # e.g. the module directive doesn't have content
+ self.indent += self.content_indent
+
+ # add all content (from docstrings, attribute docs etc.)
+ self.add_content(more_content)
+
+ # document members, if possible
+ self.document_members(all_members)
+
+
+class ModuleDocumenter(Documenter):
+ option_spec = {
+ 'members': members_option, 'undoc-members': bool_option,
+ 'noindex': bool_option, 'inherited-members': bool_option,
+ 'show-inheritance': bool_option, 'synopsis': identity,
+ 'platform': identity, 'deprecated': bool_option,
+ }
+
+ @classmethod
+ def can_document_member(cls, member, membername, isattr, parent):
+ return isinstance(member, ModuleType)
+
+ def resolve_name(self, modname, parents, path, base):
+ if modname is not None:
+ self.directive.warn('"::" in automodule name doesn\'t make sense')
+ return (path or '') + base, []
+
+ def parse_name(self):
+ ret = Documenter.parse_name(self)
+ if self.args or self.retann:
+ self.directive.warn('signature arguments or return annotation '
+ 'given for automodule %s' % self.fullname)
+ return ret
+
+ def add_directive_header(self, sig):
+ Documenter.add_directive_header(self, sig)
+
+ # add some module-specific options
+ if self.options.synopsis:
+ self.add_line(
+ u' :synopsis: ' + self.options.synopsis, '<autodoc>')
+ if self.options.platform:
+ self.add_line(
+ u' :platform: ' + self.options.platform, '<autodoc>')
+ if self.options.deprecated:
+ self.add_line(u' :deprecated:', '<autodoc>')
+
+ def get_object_members(self, members=None):
+ if members is ALL or not hasattr(self.object, '__all__'):
+ # for implicit module members, check __module__ to avoid
+ # documenting imported objects
+ return True, inspect.getmembers(self.object)
+ ret = []
+ for mname in (members or self.object.__all__):
+ try:
+ ret.append((mname, getattr(self.object, mname)))
+ except AttributeError:
+ self.directive.warn('missing attribute mentioned in :members: '
+ 'or __all__: module %s, attribute %s' %
+ (self.object.__name__, mname))
+ return False, ret
+
+
+class ModuleLevelDocumenter(Documenter):
+ def resolve_name(self, modname, parents, path, base):
+ if modname is None:
+ if path:
+ modname = path.rstrip('.')
+ else:
+ # if documenting a toplevel object without explicit module,
+ # it can be contained in another auto directive ...
+ if hasattr(self.env, 'autodoc_current_module'):
+ modname = self.env.autodoc_current_module
+ # ... or in the scope of a module directive
+ if not modname:
+ modname = self.env.currmodule
+ # ... else, it stays None, which means invalid
+ return modname, parents + [base]
+
+
+class ClassLevelDocumenter(Documenter):
+ def resolve_name(self, modname, parents, path, base):
+ if modname is None:
+ if path:
+ mod_cls = path.rstrip('.')
+ else:
+ mod_cls = None
+ # if documenting a class-level object without path,
+ # there must be a current class, either from a parent
+ # auto directive ...
+ if hasattr(self.env, 'autodoc_current_class'):
+ mod_cls = self.env.autodoc_current_class
+ # ... or from a class directive
+ if mod_cls is None:
+ mod_cls = self.env.currclass
+ # ... if still None, there's no way to know
+ if mod_cls is None:
+ return None, []
+ modname, cls = rpartition(mod_cls, '.')
+ parents = [cls]
+ # if the module name is still missing, get it like above
+ if not modname and hasattr(self.env, 'autodoc_current_module'):
+ modname = self.env.autodoc_current_module
+ if not modname:
+ modname = self.env.currmodule
+ # ... else, it stays None, which means invalid
+ return modname, parents + [base]
+
+
+class FunctionDocumenter(ModuleLevelDocumenter):
+ objtype = 'function'
+
+ @classmethod
+ def can_document_member(cls, member, membername, isattr, parent):
+ return isinstance(member, (FunctionType, BuiltinFunctionType))
+
+ def format_args(self):
+ if inspect.isbuiltin(self.object) or \
+ inspect.ismethoddescriptor(self.object):
+ # can never get arguments of a C function or method
+ return None
+ try:
+ argspec = inspect.getargspec(self.object)
+ except TypeError:
+ # if a class should be documented as function (yay duck
+ # typing) we try to use the constructor signature as function
+ # signature without the first argument.
+ try:
+ argspec = inspect.getargspec(self.object.__new__)
+ except TypeError:
+ argspec = inspect.getargspec(self.object.__init__)
+ if argspec[0]:
+ del argspec[0][0]
+ return inspect.formatargspec(*argspec)
+
+ def document_members(self, all_members=False):
+ pass
+
+
+class ClassDocumenter(ModuleLevelDocumenter):
+ objtype = 'class'
+ option_spec = {
+ 'members': members_option, 'undoc-members': bool_option,
+ 'noindex': bool_option, 'inherited-members': bool_option,
+ 'show-inheritance': bool_option,
+ }
+
+ @classmethod
+ def can_document_member(cls, member, membername, isattr, parent):
+ return isinstance(member, (type, ClassType))
+
+ def import_object(self):
+ ModuleLevelDocumenter.import_object(self)
+
+ # if the class is documented under another name, document it
+ # as data/attribute
+ self.doc_as_attr = (self.objpath[-1] != self.object.__name__)
+
+ def format_args(self):
+ args = None
+ # for classes, the relevant signature is the __init__ method's
+ initmeth = self.get_attr(self.object, '__init__', None)
+ # classes without __init__ method, default __init__ or
+ # __init__ written in C?
+ if initmeth is None or initmeth is object.__init__ or not \
+ (inspect.ismethod(initmeth) or inspect.isfunction(initmeth)):
+ return None
+ argspec = inspect.getargspec(initmeth)
+ if argspec[0] and argspec[0][0] in ('cls', 'self'):
+ del argspec[0][0]
+ return inspect.formatargspec(*argspec)
+
+ def format_signature(self):
+ if self.doc_as_attr:
+ return ''
+ return ModuleLevelDocumenter.format_signature(self)
+
+ def add_directive_header(self, sig):
+ if self.doc_as_attr:
+ self.directivetype = 'attribute'
+ Documenter.add_directive_header(self, sig)
# add inheritance info, if wanted
- if self.options.show_inheritance and what in ('class', 'exception'):
- if len(todoc.__bases__):
+ if not self.doc_as_attr and self.options.show_inheritance:
+ self.add_line(u'', '<autodoc>')
+ if len(self.object.__bases__):
bases = [b.__module__ == '__builtin__' and
u':class:`%s`' % b.__name__ or
u':class:`%s.%s`' % (b.__module__, b.__name__)
- for b in todoc.__bases__]
- self.result.append(indent + _(u' Bases: %s') %
- ', '.join(bases),
- '<autodoc>')
- self.result.append(u'', '<autodoc>')
-
- # the module directive doesn't have content
- if what != 'module':
- indent += u' '
-
- # add content from attribute documentation
- if analyzer:
- # prevent encoding errors when the file name is non-ASCII
- srcname = unicode(analyzer.srcname,
- sys.getfilesystemencoding(), 'replace')
- sourcename = u'%s:docstring of %s' % (srcname, fullname)
- attr_docs = analyzer.find_attr_docs()
- if objpath:
- key = ('.'.join(objpath[:-1]), objpath[-1])
- if key in attr_docs:
- no_docstring = True
- docstrings = [attr_docs[key]]
- for i, line in enumerate(self.process_doc(docstrings, what,
- fullname, todoc)):
- self.result.append(indent + line, sourcename, i)
- else:
- sourcename = u'docstring of %s' % fullname
- attr_docs = {}
+ for b in self.object.__bases__]
+ self.add_line(_(u' Bases: %s') % ', '.join(bases),
+ '<autodoc>')
- # add content from docstrings
- if not no_docstring:
- encoding = analyzer and analyzer.encoding
- docstrings = self.get_doc(what, todoc, encoding)
- for i, line in enumerate(self.process_doc(docstrings, what,
- fullname, todoc)):
- self.result.append(indent + line, sourcename, i)
+ def get_doc(self, encoding=None):
+ content = self.env.config.autoclass_content
- # add additional content (e.g. from document), if present
- if add_content:
- for line, src in zip(add_content.data, add_content.items):
- self.result.append(indent + line, src[0], src[1])
+ docstrings = []
+ docstring = self.get_attr(self.object, '__doc__', None)
+ if docstring:
+ docstrings.append(docstring)
+
+ # for classes, what the "docstring" is can be controlled via a
+ # config value; the default is only the class docstring
+ if content in ('both', 'init'):
+ initdocstring = self.get_attr(
+ self.get_attr(self.object, '__init__', None), '__doc__')
+ # for new-style classes, no __init__ means default __init__
+ if initdocstring == object.__init__.__doc__:
+ initdocstring = None
+ if initdocstring:
+ if content == 'init':
+ docstrings = [initdocstring]
+ else:
+ docstrings.append(initdocstring)
- # document members?
- if not members or what in ('function', 'method', 'staticmethod',
- 'classmethod', 'data', 'attribute'):
- return
+ return [prepare_docstring(force_decode(docstring, encoding))
+ for docstring in docstrings]
- # set current namespace for finding members
- self.env.autodoc_current_module = mod
- if objpath:
- self.env.autodoc_current_class = objpath[0]
-
- # look for members to include
- want_all_members = members == ['__all__']
- members_check_module = False
- if want_all_members:
- # unqualified :members: given
- if what == 'module':
- if hasattr(todoc, '__all__'):
- members_check_module = False
- all_members = []
- for mname in todoc.__all__:
- try:
- all_members.append((mname, getattr(todoc, mname)))
- except AttributeError:
- self.warn('missing attribute mentioned in __all__: '
- 'module %s, attribute %s' %
- (todoc.__name__, mname))
- else:
- # for implicit module members, check __module__ to avoid
- # documenting imported objects
- members_check_module = True
- all_members = inspect.getmembers(todoc)
- else:
- if self.options.inherited_members:
- # getmembers() uses dir() which pulls in members from all
- # base classes
- all_members = inspect.getmembers(todoc)
- else:
- # __dict__ contains only the members directly defined
- # in the class
- all_members = sorted(todoc.__dict__.iteritems())
+ def add_content(self, more_content, no_docstring=False):
+ if self.doc_as_attr:
+ content = ViewList(
+ [_('alias of :class:`%s`') % self.object.__name__], source='')
+ ModuleLevelDocumenter.add_content(self, content, no_docstring=True)
else:
- all_members = [(mname, getattr(todoc, mname)) for mname in members]
+ ModuleLevelDocumenter.add_content(self, more_content)
- # search for members in source code too
- namespace = '.'.join(objpath) # will be empty for modules
- for (membername, member) in all_members:
- # 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
+class ExceptionDocumenter(ClassDocumenter):
+ objtype = 'exception'
- if want_all_members and membername.startswith('_'):
- # ignore members whose name starts with _ by default
- skip = True
- else:
- if (namespace, membername) in attr_docs:
- # keep documented attributes
- 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
+ # needs a higher priority than ClassDocumenter
+ priority = 10
- # give the user a chance to decide whether this member
- # should be skipped
- if self.env.app:
- # let extensions preprocess docstrings
- skip_user = self.env.app.emit_firstresult(
- 'autodoc-skip-member', what, membername, member,
- skip, self.options)
- if skip_user is not None:
- skip = skip_user
- if skip:
- continue
+ @classmethod
+ def can_document_member(cls, member, membername, isattr, parent):
+ return isinstance(member, (type, ClassType)) and \
+ issubclass(member, base_exception)
- # determine member type
- if what == 'module':
- if isinstance(member, (FunctionType, BuiltinFunctionType)):
- memberwhat = 'function'
- elif isattr:
- memberwhat = 'attribute'
- elif isinstance(member, clstypes):
- if member.__name__ != membername:
- # assume it's aliased
- memberwhat = 'data'
- content = ViewList(
- [_('alias of :class:`%s`') % member.__name__],
- source='')
- elif issubclass(member, base_exception):
- memberwhat = 'exception'
- else:
- memberwhat = 'class'
- else:
- continue
- else:
- if inspect.isroutine(member):
- memberwhat = 'method'
- elif isattr:
- memberwhat = 'attribute'
- elif isinstance(member, clstypes):
- if member.__name__ != membername:
- # assume it's aliased
- memberwhat = 'attribute'
- content = ViewList(
- [_('alias of :class:`%s`') % member.__name__],
- source='')
- else:
- memberwhat = 'class'
- elif isdescriptor(member):
- memberwhat = 'attribute'
- else:
- continue
- # give explicitly separated module name, so that members
- # of inner classes can be documented
- full_membername = mod + '::' + '.'.join(objpath + [membername])
- self.generate(memberwhat, full_membername, ['__all__'],
- add_content=content, no_docstring=bool(content),
- indent=indent, check_module=members_check_module,
- real_module=real_module)
+class DataDocumenter(ModuleLevelDocumenter):
+ objtype = 'data'
- self.env.autodoc_current_module = None
- self.env.autodoc_current_class = None
+ @classmethod
+ def can_document_member(cls, member, membername, isattr, parent):
+ return isinstance(parent, ModuleDocumenter) and isattr
+ def document_members(self, all_members=False):
+ pass
-def _auto_directive(dirname, arguments, options, content, lineno,
- content_offset, block_text, state, state_machine):
- what = dirname[4:] # strip "auto"
- name = arguments[0]
- genopt = Options()
- members = options.get('members', [])
- genopt.inherited_members = 'inherited-members' in options
- if genopt.inherited_members and not members:
- # :inherited-members: implies :members:
- members = ['__all__']
- genopt.undoc_members = 'undoc-members' in options
- genopt.show_inheritance = 'show-inheritance' in options
- genopt.noindex = 'noindex' in options
- genopt.synopsis = options.get('synopsis', '')
- genopt.platform = options.get('platform', '')
- genopt.deprecated = 'deprecated' in options
-
- generator = RstGenerator(genopt, state.document, lineno)
- generator.generate(what, name, members, content)
- if not generator.result:
- return generator.warnings
-
- # record all filenames as dependencies -- this will at least partially make
- # automatic invalidation possible
- for fn in generator.filename_set:
- state.document.settings.env.note_dependency(fn)
-
- # use a custom reporter that correctly assigns lines to source and lineno
- old_reporter = state.memo.reporter
- state.memo.reporter = AutodocReporter(generator.result, state.memo.reporter)
- if dirname == 'automodule':
- node = nodes.section()
- node.document = state.document # necessary so that the child nodes
- # get the right source/line set
- nested_parse_with_titles(state, generator.result, node)
- else:
- node = nodes.paragraph()
- node.document = state.document
- state.nested_parse(generator.result, 0, node)
- state.memo.reporter = old_reporter
- return generator.warnings + node.children
-
-def auto_directive(*args, **kwds):
- return _auto_directive(*args, **kwds)
-
-def automodule_directive(*args, **kwds):
- return _auto_directive(*args, **kwds)
-
-def autoclass_directive(*args, **kwds):
- return _auto_directive(*args, **kwds)
+class MethodDocumenter(ClassLevelDocumenter):
+ objtype = 'method'
-def members_option(arg):
- if arg is None:
- return ['__all__']
- return [x.strip() for x in arg.split(',')]
+ @classmethod
+ def can_document_member(cls, member, membername, isattr, parent):
+ # other attributes are recognized via the module analyzer
+ return inspect.isroutine(member) and \
+ not isinstance(parent, ModuleDocumenter)
+
+ def format_args(self):
+ if inspect.isbuiltin(self.object) or \
+ inspect.ismethoddescriptor(self.object):
+ # can never get arguments of a C function or method
+ return None
+ argspec = inspect.getargspec(self.object)
+ if argspec[0] and argspec[0][0] in ('cls', 'self'):
+ del argspec[0][0]
+ return inspect.formatargspec(*argspec)
+
+ def document_members(self, all_members=False):
+ pass
+
+
+class ClassmethodDocumenter(MethodDocumenter):
+ objtype = 'classmethod'
+
+ priority = 10
+
+ @classmethod
+ def can_document_member(cls, member, membername, isattr, parent):
+ return inspect.isroutine(member) and \
+ not isinstance(parent, ModuleDocumenter) and \
+ (isinstance(member, classmethod) or
+ (isinstance(member, MethodType) and member.im_self is not None))
+
+
+class StaticmethodDocumenter(MethodDocumenter):
+ objtype = 'staticmethod'
+
+ priority = 10
+
+ @classmethod
+ def can_document_member(cls, member, membername, isattr, parent):
+ return inspect.isroutine(member) and \
+ not isinstance(parent, ModuleDocumenter) and \
+ (isinstance(member, FunctionType) or
+ (isinstance(member, BuiltinFunctionType) and
+ member.__self__ is not None))
+
+
+class AttributeDocumenter(ClassLevelDocumenter):
+ objtype = 'attribute'
+
+ @classmethod
+ def can_document_member(cls, member, membername, isattr, parent):
+ return isdescriptor(member) or \
+ (not isinstance(parent, ModuleDocumenter) and isattr)
+
+ def document_members(self, all_members=False):
+ pass
+
+
+class AutoDirective(Directive):
+ # a registry of objtype -> documenter class
+ _registry = {}
+
+ # standard docutils directive settings
+ has_content = True
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = True
+ # allow any options to be passed; the options are parsed further
+ # by the selected Documenter
+ option_spec = DefDict(identity)
+
+ def warn(self, msg):
+ self.warnings.append(self.reporter.warning(msg, line=self.lineno))
+
+ def run(self):
+ self.filename_set = set() # a set of dependent filenames
+ self.reporter = self.state.document.reporter
+ self.env = self.state.document.settings.env
+ self.warnings = []
+ self.result = ViewList()
+
+ # find out what documenter to call
+ objtype = self.name[4:]
+ doc_class = self._registry[objtype]
+ # process the options with the selected documenter's option_spec
+ self.genopt = Options(assemble_option_dict(
+ self.options.items(), doc_class.option_spec))
+ # generate the output
+ documenter = doc_class(self, self.arguments[0])
+ documenter.generate(more_content=self.content)
+ if not self.result:
+ return self.warnings
+
+ # use a custom reporter that correctly assigns lines to source
+ # filename/description and lineno
+ old_reporter = self.state.memo.reporter
+ self.state.memo.reporter = AutodocReporter(self.result,
+ self.state.memo.reporter)
+ if self.name == 'automodule':
+ node = nodes.section()
+ # necessary so that the child nodes get the right source/line set
+ node.document = self.state.document
+ nested_parse_with_titles(self.state, self.result, node)
+ else:
+ node = nodes.paragraph()
+ node.document = self.state.document
+ self.state.nested_parse(self.result, 0, node)
+ self.state.memo.reporter = old_reporter
+ return self.warnings + node.children
+
+
+def add_documenter(cls):
+ if not issubclass(cls, Documenter):
+ raise ExtensionError('autodoc documenter %r must be a subclass '
+ 'of Documenter' % cls)
+ if cls.objtype in AutoDirective._registry:
+ raise ExtensionError('autodoc documenter for %r is already '
+ 'registered' % cls.objtype)
+ AutoDirective._registry[cls.objtype] = cls
def setup(app):
- mod_options = {
- 'members': members_option, 'undoc-members': directives.flag,
- 'noindex': directives.flag, 'inherited-members': directives.flag,
- 'show-inheritance': directives.flag, 'synopsis': lambda x: x,
- 'platform': lambda x: x, 'deprecated': directives.flag
- }
- cls_options = {
- 'members': members_option, 'undoc-members': directives.flag,
- 'noindex': directives.flag, 'inherited-members': directives.flag,
- 'show-inheritance': directives.flag
- }
- app.add_directive('automodule', automodule_directive,
- 1, (1, 0, 1), **mod_options)
- app.add_directive('autoclass', autoclass_directive,
- 1, (1, 0, 1), **cls_options)
- app.add_directive('autoexception', autoclass_directive,
- 1, (1, 0, 1), **cls_options)
- app.add_directive('autodata', auto_directive, 1, (1, 0, 1),
- noindex=directives.flag)
- app.add_directive('autofunction', auto_directive, 1, (1, 0, 1),
- noindex=directives.flag)
- app.add_directive('automethod', auto_directive, 1, (1, 0, 1),
- noindex=directives.flag)
- app.add_directive('autoattribute', auto_directive, 1, (1, 0, 1),
- noindex=directives.flag)
- # deprecated: remove in some future version.
- app.add_config_value('automodule_skip_lines', 0, True)
+ app.add_autodocumenter(ModuleDocumenter)
+ app.add_autodocumenter(ClassDocumenter)
+ app.add_autodocumenter(ExceptionDocumenter)
+ app.add_autodocumenter(DataDocumenter)
+ app.add_autodocumenter(FunctionDocumenter)
+ app.add_autodocumenter(MethodDocumenter)
+ app.add_autodocumenter(ClassmethodDocumenter)
+ app.add_autodocumenter(StaticmethodDocumenter)
+ app.add_autodocumenter(AttributeDocumenter)
+
app.add_config_value('autoclass_content', 'class', True)
app.add_event('autodoc-process-docstring')
app.add_event('autodoc-process-signature')