sphinx/sphinx/ext/autodoc/__init__.py
Takeshi KOMIYA caa6579dbd Fix #8872: autodoc: stacked singledispatches are wrongly rendered
When multiple singledispatch decorators are stacked, the first typehints
are copied to the subsequent definitions unexpectedly.

Now autodoc generates a dummy function not to affect typehints to
subsequent functions.
2021-05-03 21:51:19 +09:00

2687 lines
106 KiB
Python

"""
sphinx.ext.autodoc
~~~~~~~~~~~~~~~~~~
Automatically insert docstrings for functions, classes or whole modules into
the doctree, thus avoiding duplication between docstrings and documentation
for those who like elaborate docstrings.
:copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import re
import warnings
from inspect import Parameter, Signature
from types import ModuleType
from typing import (TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Sequence,
Set, Tuple, Type, TypeVar, Union)
from docutils.statemachine import StringList
import sphinx
from sphinx.application import Sphinx
from sphinx.config import ENUM, Config
from sphinx.deprecation import RemovedInSphinx50Warning, RemovedInSphinx60Warning
from sphinx.environment import BuildEnvironment
from sphinx.ext.autodoc.importer import (get_class_members, get_object_members, import_module,
import_object)
from sphinx.ext.autodoc.mock import ismock, mock, undecorate
from sphinx.locale import _, __
from sphinx.pycode import ModuleAnalyzer, PycodeError
from sphinx.util import inspect, logging
from sphinx.util.docstrings import extract_metadata, prepare_docstring
from sphinx.util.inspect import (evaluate_signature, getdoc, object_description, safe_getattr,
stringify_signature)
from sphinx.util.typing import OptionSpec, get_type_hints, restify
from sphinx.util.typing import stringify as stringify_typehint
if TYPE_CHECKING:
from sphinx.ext.autodoc.directive import DocumenterBridge
logger = logging.getLogger(__name__)
# This type isn't exposed directly in any modules, but can be found
# here in most Python versions
MethodDescriptorType = type(type.__subclasses__)
#: extended signature RE: with explicit module name separated by ::
py_ext_sig_re = re.compile(
r'''^ ([\w.]+::)? # explicit module name
([\w.]+\.)? # module and/or class name(s)
(\w+) \s* # thing name
(?: \((.*)\) # optional: arguments
(?:\s* -> \s* (.*))? # return annotation
)? $ # and nothing more
''', re.VERBOSE)
special_member_re = re.compile(r'^__\S+__$')
def identity(x: Any) -> Any:
return x
class _All:
"""A special value for :*-members: that matches to any member."""
def __contains__(self, item: Any) -> bool:
return True
class _Empty:
"""A special value for :exclude-members: that never matches to any member."""
def __contains__(self, item: Any) -> bool:
return False
ALL = _All()
EMPTY = _Empty()
UNINITIALIZED_ATTR = object()
INSTANCEATTR = object()
SLOTSATTR = object()
def members_option(arg: Any) -> Union[object, List[str]]:
"""Used to convert the :members: option to auto directives."""
if arg in (None, True):
return ALL
elif arg is False:
return None
else:
return [x.strip() for x in arg.split(',') if x.strip()]
def members_set_option(arg: Any) -> Union[object, Set[str]]:
"""Used to convert the :members: option to auto directives."""
warnings.warn("members_set_option() is deprecated.",
RemovedInSphinx50Warning, stacklevel=2)
if arg is None:
return ALL
return {x.strip() for x in arg.split(',') if x.strip()}
def exclude_members_option(arg: Any) -> Union[object, Set[str]]:
"""Used to convert the :exclude-members: option."""
if arg in (None, True):
return EMPTY
return {x.strip() for x in arg.split(',') if x.strip()}
def inherited_members_option(arg: Any) -> Union[object, Set[str]]:
"""Used to convert the :members: option to auto directives."""
if arg in (None, True):
return 'object'
else:
return arg
def member_order_option(arg: Any) -> Optional[str]:
"""Used to convert the :members: option to auto directives."""
if arg in (None, True):
return None
elif arg in ('alphabetical', 'bysource', 'groupwise'):
return arg
else:
raise ValueError(__('invalid value for member-order option: %s') % arg)
def class_doc_from_option(arg: Any) -> Optional[str]:
"""Used to convert the :class-doc-from: option to autoclass directives."""
if arg in ('both', 'class', 'init'):
return arg
else:
raise ValueError(__('invalid value for class-doc-from option: %s') % arg)
SUPPRESS = object()
def annotation_option(arg: Any) -> Any:
if arg in (None, True):
# suppress showing the representation of the object
return SUPPRESS
else:
return arg
def bool_option(arg: Any) -> bool:
"""Used to convert flag options to auto directives. (Instead of
directives.flag(), which returns None).
"""
return True
def merge_special_members_option(options: Dict) -> None:
"""Merge :special-members: option to :members: option."""
warnings.warn("merge_special_members_option() is deprecated.",
RemovedInSphinx50Warning, stacklevel=2)
if 'special-members' in options and options['special-members'] is not ALL:
if options.get('members') is ALL:
pass
elif options.get('members'):
for member in options['special-members']:
if member not in options['members']:
options['members'].append(member)
else:
options['members'] = options['special-members']
def merge_members_option(options: Dict) -> None:
"""Merge :*-members: option to the :members: option."""
if options.get('members') is ALL:
# merging is not needed when members: ALL
return
members = options.setdefault('members', [])
for key in {'private-members', 'special-members'}:
if key in options and options[key] not in (ALL, None):
for member in options[key]:
if member not in members:
members.append(member)
# Some useful event listener factories for autodoc-process-docstring.
def cut_lines(pre: int, post: int = 0, what: str = None) -> Callable:
"""Return a listener that removes the first *pre* and last *post*
lines of every docstring. If *what* is a sequence of strings,
only docstrings of a type in *what* will be processed.
Use like this (e.g. in the ``setup()`` function of :file:`conf.py`)::
from sphinx.ext.autodoc import cut_lines
app.connect('autodoc-process-docstring', cut_lines(4, what=['module']))
This can (and should) be used in place of :confval:`automodule_skip_lines`.
"""
def process(app: Sphinx, what_: str, name: str, obj: Any, options: Any, lines: List[str]
) -> None:
if what and what_ not in what:
return
del lines[:pre]
if post:
# remove one trailing blank line.
if lines and not lines[-1]:
lines.pop(-1)
del lines[-post:]
# make sure there is a blank line at the end
if lines and lines[-1]:
lines.append('')
return process
def between(marker: str, what: Sequence[str] = None, keepempty: bool = False,
exclude: bool = False) -> Callable:
"""Return a listener that either keeps, or if *exclude* is True excludes,
lines between lines that match the *marker* regular expression. If no line
matches, the resulting docstring would be empty, so no change will be made
unless *keepempty* is true.
If *what* is a sequence of strings, only docstrings of a type in *what* will
be processed.
"""
marker_re = re.compile(marker)
def process(app: Sphinx, what_: str, name: str, obj: Any, options: Any, lines: List[str]
) -> None:
if what and what_ not in what:
return
deleted = 0
delete = not exclude
orig_lines = lines[:]
for i, line in enumerate(orig_lines):
if delete:
lines.pop(i - deleted)
deleted += 1
if marker_re.match(line):
delete = not delete
if delete:
lines.pop(i - deleted)
deleted += 1
if not lines and not keepempty:
lines[:] = orig_lines
# make sure there is a blank line at the end
if lines and lines[-1]:
lines.append('')
return process
# This class is used only in ``sphinx.ext.autodoc.directive``,
# But we define this class here to keep compatibility (see #4538)
class Options(dict):
"""A dict/attribute hybrid that returns None on nonexisting keys."""
def __getattr__(self, name: str) -> Any:
try:
return self[name.replace('_', '-')]
except KeyError:
return None
class ObjectMember(tuple):
"""A member of object.
This is used for the result of `Documenter.get_object_members()` to
represent each member of the object.
.. Note::
An instance of this class behaves as a tuple of (name, object)
for compatibility to old Sphinx. The behavior will be dropped
in the future. Therefore extensions should not use the tuple
interface.
"""
def __new__(cls, name: str, obj: Any, **kwargs: Any) -> Any:
return super().__new__(cls, (name, obj)) # type: ignore
def __init__(self, name: str, obj: Any, docstring: Optional[str] = None,
class_: Any = None, skipped: bool = False) -> None:
self.__name__ = name
self.object = obj
self.docstring = docstring
self.skipped = skipped
self.class_ = class_
ObjectMembers = Union[List[ObjectMember], List[Tuple[str, Any]]]
class Documenter:
"""
A Documenter knows how to autodocument a single object type. When
registered with the AutoDirective, it will be used to document objects
of that type when needed by autodoc.
Its *objtype* attribute selects what auto directive it is assigned to
(the directive name is 'auto' + objtype), and what directive it generates
by default, though that can be overridden by an attribute called
*directivetype*.
A Documenter has an *option_spec* that works like a docutils directive's;
in fact, it will be used to parse an auto directive's options that matches
the documenter.
"""
#: 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 = ' '
#: priority if multiple documenters return True from can_document_member
priority = 0
#: order if autodoc_member_order is set to 'groupwise'
member_order = 0
#: true if the generated content may contain titles
titles_allowed = False
option_spec: OptionSpec = {
'noindex': bool_option
}
def get_attr(self, obj: Any, name: str, *defargs: Any) -> Any:
"""getattr() override for types such as Zope interfaces."""
return autodoc_attrgetter(self.env.app, obj, name, *defargs)
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
"""Called to see if a member can be documented by this documenter."""
raise NotImplementedError('must be implemented in subclasses')
def __init__(self, directive: "DocumenterBridge", name: str, indent: str = '') -> None:
self.directive = directive
self.config: Config = directive.env.config
self.env: BuildEnvironment = 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: str = None
self.module: ModuleType = None
self.objpath: List[str] = None
self.fullname: str = None
# extra signature items (arguments and return annotation,
# also set after resolve_name succeeds)
self.args: str = None
self.retann: str = None
# the object to document (set after import_object succeeds)
self.object: Any = None
self.object_name: str = None
# the parent/owner of the object to document
self.parent: Any = None
# the module analyzer to get at attribute docs, or None
self.analyzer: ModuleAnalyzer = None
@property
def documenters(self) -> Dict[str, Type["Documenter"]]:
"""Returns registered Documenter classes"""
return self.env.app.registry.documenters
def add_line(self, line: str, source: str, *lineno: int) -> None:
"""Append one line of generated reST to the output."""
if line.strip(): # not a blank line
self.directive.result.append(self.indent + line, source, *lineno)
else:
self.directive.result.append('', source, *lineno)
def resolve_name(self, modname: str, parents: Any, path: str, base: Any
) -> Tuple[str, List[str]]:
"""Resolve the module and name of the object to document given by the
arguments and the current module/class.
Must return a pair of the module name and a chain of attributes; for
example, it would return ``('zipfile', ['ZipFile', 'open'])`` for the
``zipfile.ZipFile.open`` method.
"""
raise NotImplementedError('must be implemented in subclasses')
def parse_name(self) -> bool:
"""Determine what module to import and what attribute to document.
Returns True and sets *self.modname*, *self.objpath*, *self.fullname*,
*self.args* and *self.retann* 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:
explicit_modname, path, base, args, retann = \
py_ext_sig_re.match(self.name).groups()
except AttributeError:
logger.warning(__('invalid signature for auto%s (%r)') % (self.objtype, self.name),
type='autodoc')
return False
# support explicit module and class name separation via ::
if explicit_modname is not None:
modname = explicit_modname[:-2]
parents = path.rstrip('.').split('.') if path else []
else:
modname = None
parents = []
with mock(self.config.autodoc_mock_imports):
self.modname, self.objpath = self.resolve_name(modname, parents, path, base)
if not self.modname:
return False
self.args = args
self.retann = retann
self.fullname = (self.modname or '') + \
('.' + '.'.join(self.objpath) if self.objpath else '')
return True
def import_object(self, raiseerror: bool = False) -> bool:
"""Import the object given by *self.modname* and *self.objpath* and set
it as *self.object*.
Returns True if successful, False if an error occurred.
"""
with mock(self.config.autodoc_mock_imports):
try:
ret = import_object(self.modname, self.objpath, self.objtype,
attrgetter=self.get_attr,
warningiserror=self.config.autodoc_warningiserror)
self.module, self.parent, self.object_name, self.object = ret
if ismock(self.object):
self.object = undecorate(self.object)
return True
except ImportError as exc:
if raiseerror:
raise
else:
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
self.env.note_reread()
return False
def get_real_modname(self) -> str:
"""Get the real module name of an object to document.
It can differ from the name of the module through which the object was
imported.
"""
return self.get_attr(self.object, '__module__', None) or self.modname
def check_module(self) -> bool:
"""Check if *self.object* is really defined in the module given by
*self.modname*.
"""
if self.options.imported_members:
return True
subject = inspect.unpartial(self.object)
modname = self.get_attr(subject, '__module__', None)
if modname and modname != self.modname:
return False
return True
def format_args(self, **kwargs: Any) -> str:
"""Format the argument signature of *self.object*.
Should return None if the object does not have a signature.
"""
return None
def format_name(self) -> str:
"""Format the name of *self.object*.
This normally should be something that can be parsed by the generated
directive, but doesn't need to be (Sphinx will display it unparsed
then).
"""
# normally the name doesn't contain the module (except for module
# directives of course)
return '.'.join(self.objpath) or self.modname
def _call_format_args(self, **kwargs: Any) -> str:
if kwargs:
try:
return self.format_args(**kwargs)
except TypeError:
# avoid chaining exceptions, by putting nothing here
pass
# retry without arguments for old documenters
return self.format_args()
def format_signature(self, **kwargs: Any) -> str:
"""Format the signature (arguments and return annotation) of the object.
Let the user process it via the ``autodoc-process-signature`` event.
"""
if self.args is not None:
# signature given explicitly
args = "(%s)" % self.args
retann = self.retann
else:
# try to introspect the signature
try:
retann = None
args = self._call_format_args(**kwargs)
if args:
matched = re.match(r'^(\(.*\))\s+->\s+(.*)$', args)
if matched:
args = matched.group(1)
retann = matched.group(2)
except Exception as exc:
logger.warning(__('error while formatting arguments for %s: %s'),
self.fullname, exc, type='autodoc')
args = None
result = self.env.events.emit_firstresult('autodoc-process-signature',
self.objtype, self.fullname,
self.object, self.options, args, retann)
if result:
args, retann = result
if args is not None:
return args + ((' -> %s' % retann) if retann else '')
else:
return ''
def add_directive_header(self, sig: str) -> None:
"""Add the directive header and options to the generated content."""
domain = getattr(self, 'domain', 'py')
directive = getattr(self, 'directivetype', self.objtype)
name = self.format_name()
sourcename = self.get_sourcename()
# one signature per line, indented by column
prefix = '.. %s:%s:: ' % (domain, directive)
for i, sig_line in enumerate(sig.split("\n")):
self.add_line('%s%s%s' % (prefix, name, sig_line),
sourcename)
if i == 0:
prefix = " " * len(prefix)
if self.options.noindex:
self.add_line(' :noindex:', sourcename)
if self.objpath:
# Be explicit about the module, this is necessary since .. class::
# etc. don't support a prepended module name
self.add_line(' :module: %s' % self.modname, sourcename)
def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
"""Decode and return lines of the docstring(s) for the object.
When it returns None value, autodoc-process-docstring will not be called for this
object.
"""
if ignore is not None:
warnings.warn("The 'ignore' argument to autodoc.%s.get_doc() is deprecated."
% self.__class__.__name__,
RemovedInSphinx50Warning, stacklevel=2)
docstring = getdoc(self.object, self.get_attr, self.config.autodoc_inherit_docstrings,
self.parent, self.object_name)
if docstring:
tab_width = self.directive.state.document.settings.tab_width
return [prepare_docstring(docstring, ignore, tab_width)]
return []
def process_doc(self, docstrings: List[List[str]]) -> Iterator[str]:
"""Let the user process the docstrings before adding them."""
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)
if docstringlines and docstringlines[-1] != '':
# append a blank line to the end of the docstring
docstringlines.append('')
yield from docstringlines
def get_sourcename(self) -> str:
if (getattr(self.object, '__module__', None) and
getattr(self.object, '__qualname__', None)):
# Get the correct location of docstring from self.object
# to support inherited methods
fullname = '%s.%s' % (self.object.__module__, self.object.__qualname__)
else:
fullname = self.fullname
if self.analyzer:
return '%s:docstring of %s' % (self.analyzer.srcname, fullname)
else:
return 'docstring of %s' % fullname
def add_content(self, more_content: Optional[StringList], no_docstring: bool = False
) -> None:
"""Add content from docstrings, attribute documentation and user."""
if no_docstring:
warnings.warn("The 'no_docstring' argument to %s.add_content() is deprecated."
% self.__class__.__name__,
RemovedInSphinx50Warning, stacklevel=2)
# set sourcename and add content from attribute documentation
sourcename = self.get_sourcename()
if self.analyzer:
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
# make a copy of docstring for attributes to avoid cache
# the change of autodoc-process-docstring event.
docstrings = [list(attr_docs[key])]
for i, line in enumerate(self.process_doc(docstrings)):
self.add_line(line, sourcename, i)
# add content from docstrings
if not no_docstring:
docstrings = self.get_doc()
if docstrings is None:
# Do not call autodoc-process-docstring on get_doc() returns None.
pass
else:
if not docstrings:
# append at least a dummy docstring, so that the event
# autodoc-process-docstring is fired and can add some
# content if desired
docstrings.append([])
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, want_all: bool) -> Tuple[bool, ObjectMembers]:
"""Return `(members_check_module, members)` where `members` is a
list of `(membername, member)` pairs of the members of *self.object*.
If *want_all* is True, return all members. Else, only return those
members given by *self.options.members* (which may also be none).
"""
warnings.warn('The implementation of Documenter.get_object_members() will be '
'removed from Sphinx-6.0.', RemovedInSphinx60Warning)
members = get_object_members(self.object, self.objpath, self.get_attr, self.analyzer)
if not want_all:
if not self.options.members:
return False, [] # type: ignore
# specific members given
selected = []
for name in self.options.members: # type: str
if name in members:
selected.append((name, members[name].value))
else:
logger.warning(__('missing attribute %s in object %s') %
(name, self.fullname), type='autodoc')
return False, selected
elif self.options.inherited_members:
return False, [(m.name, m.value) for m in members.values()]
else:
return False, [(m.name, m.value) for m in members.values()
if m.directly_defined]
def filter_members(self, members: ObjectMembers, want_all: bool
) -> List[Tuple[str, Any, bool]]:
"""Filter the given member list.
Members are skipped if
- they are private (except if given explicitly or the private-members
option is set)
- they are special methods (except if given explicitly or the
special-members option is set)
- they are undocumented (except if the undoc-members option is set)
The user can override the skipping decision by connecting to the
``autodoc-skip-member`` event.
"""
def is_filtered_inherited_member(name: str, obj: Any) -> bool:
if inspect.isclass(self.object):
for cls in self.object.__mro__:
if cls.__name__ == self.options.inherited_members and cls != self.object:
# given member is a member of specified *super class*
return True
elif name in cls.__dict__:
return False
elif name in self.get_attr(cls, '__annotations__', {}):
return False
elif isinstance(obj, ObjectMember) and obj.class_ is cls:
return False
return False
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 obj in members:
membername, member = obj
# if isattr is True, the member is documented as an attribute
if member is INSTANCEATTR:
isattr = True
else:
isattr = False
doc = getdoc(member, self.get_attr, self.config.autodoc_inherit_docstrings,
self.parent, self.object_name)
if not isinstance(doc, str):
# Ignore non-string __doc__
doc = None
# if the member __doc__ is the same as self's __doc__, it's just
# inherited and therefore not the member's doc
cls = self.get_attr(member, '__class__', None)
if cls:
cls_doc = self.get_attr(cls, '__doc__', None)
if cls_doc == doc:
doc = None
if isinstance(obj, ObjectMember) and obj.docstring:
# hack for ClassDocumenter to inject docstring via ObjectMember
doc = obj.docstring
has_doc = bool(doc)
metadata = extract_metadata(doc)
if 'private' in metadata:
# consider a member private if docstring has "private" metadata
isprivate = True
elif 'public' in metadata:
# consider a member public if docstring has "public" metadata
isprivate = False
else:
isprivate = membername.startswith('_')
keep = False
if ismock(member):
# mocked module or object
pass
elif self.options.exclude_members and membername in self.options.exclude_members:
# remove members given by exclude-members
keep = False
elif want_all and special_member_re.match(membername):
# special __methods__
if self.options.special_members and membername in self.options.special_members:
if membername == '__doc__':
keep = False
elif is_filtered_inherited_member(membername, obj):
keep = False
else:
keep = has_doc or self.options.undoc_members
else:
keep = False
elif (namespace, membername) in attr_docs:
if want_all and isprivate:
if self.options.private_members is None:
keep = False
else:
keep = membername in self.options.private_members
else:
# keep documented attributes
keep = True
isattr = True
elif want_all and isprivate:
if has_doc or self.options.undoc_members:
if self.options.private_members is None:
keep = False
elif is_filtered_inherited_member(membername, obj):
keep = False
else:
keep = membername in self.options.private_members
else:
keep = False
else:
if (self.options.members is ALL and
is_filtered_inherited_member(membername, obj)):
keep = False
else:
# ignore undocumented members if :undoc-members: is not given
keep = has_doc or self.options.undoc_members
if isinstance(obj, ObjectMember) and obj.skipped:
# forcedly skipped member (ex. a module attribute not defined in __all__)
keep = False
# give the user a chance to decide whether this member
# should be skipped
if self.env.app:
# let extensions preprocess docstrings
try:
skip_user = self.env.app.emit_firstresult(
'autodoc-skip-member', self.objtype, membername, member,
not keep, self.options)
if skip_user is not None:
keep = not skip_user
except Exception as exc:
logger.warning(__('autodoc: failed to determine %r to be documented, '
'the following exception was raised:\n%s'),
member, exc, type='autodoc')
keep = False
if keep:
ret.append((membername, member, isattr))
return ret
def document_members(self, all_members: bool = False) -> None:
"""Generate reST for member documentation.
If *all_members* is True, do all members, else those given by
*self.options.members*.
"""
# set current namespace for finding members
self.env.temp_data['autodoc:module'] = self.modname
if self.objpath:
self.env.temp_data['autodoc: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
memberdocumenters: List[Tuple[Documenter, bool]] = []
for (mname, member, isattr) in self.filter_members(members, want_all):
classes = [cls for cls in self.documenters.values()
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(key=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])
documenter = classes[-1](self.directive, full_mname, self.indent)
memberdocumenters.append((documenter, isattr))
member_order = self.options.member_order or self.config.autodoc_member_order
memberdocumenters = self.sort_members(memberdocumenters, member_order)
for documenter, isattr in memberdocumenters:
documenter.generate(
all_members=True, real_modname=self.real_modname,
check_module=members_check_module and not isattr)
# reset current objects
self.env.temp_data['autodoc:module'] = None
self.env.temp_data['autodoc:class'] = None
def sort_members(self, documenters: List[Tuple["Documenter", bool]],
order: str) -> List[Tuple["Documenter", bool]]:
"""Sort the given member list."""
if order == 'groupwise':
# sort by group; alphabetically within groups
documenters.sort(key=lambda e: (e[0].member_order, e[0].name))
elif order == 'bysource':
if self.analyzer:
# sort by source order, by virtue of the module analyzer
tagorder = self.analyzer.tagorder
def keyfunc(entry: Tuple[Documenter, bool]) -> int:
fullname = entry[0].name.split('::')[1]
return tagorder.get(fullname, len(tagorder))
documenters.sort(key=keyfunc)
else:
# Assume that member discovery order matches source order.
# This is a reasonable assumption in Python 3.6 and up, where
# module.__dict__ is insertion-ordered.
pass
else: # alphabetical
documenters.sort(key=lambda e: e[0].name)
return documenters
def generate(self, more_content: Optional[StringList] = None, real_modname: str = None,
check_module: bool = False, all_members: bool = False) -> None:
"""Generate reST for the object given by *self.name*, and possibly for
its members.
If *more_content* is given, include that content. If *real_modname* is
given, use that module name to find attribute docs. If *check_module* is
True, only generate if the object is defined in the module name it is
imported from. If *all_members* is True, document all members.
"""
if not self.parse_name():
# need a module to import
logger.warning(
__('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, type='autodoc')
return
# now, import the module and get object to document
if not self.import_object():
return
# If there is no real module defined, figure out which to use.
# The real module is used in the module analyzer to look up the module
# 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.
guess_modname = self.get_real_modname()
self.real_modname: str = real_modname or guess_modname
# try to also get a source code analyzer for attribute docs
try:
self.analyzer = ModuleAnalyzer.for_module(self.real_modname)
# parse right now, to get PycodeErrors on parsing (results will
# be cached anyway)
self.analyzer.find_attr_docs()
except PycodeError as exc:
logger.debug('[autodoc] module analyzer failed: %s', exc)
# no source file -- e.g. for builtin and C modules
self.analyzer = None
# at least add the module.__file__ as a dependency
if hasattr(self.module, '__file__') and self.module.__file__:
self.directive.record_dependencies.add(self.module.__file__)
else:
self.directive.record_dependencies.add(self.analyzer.srcname)
if self.real_modname != guess_modname:
# Add module to dependency list if target object is defined in other module.
try:
analyzer = ModuleAnalyzer.for_module(guess_modname)
self.directive.record_dependencies.add(analyzer.srcname)
except PycodeError:
pass
# check __module__ of object (for members not given explicitly)
if check_module:
if not self.check_module():
return
sourcename = self.get_sourcename()
# 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.add_line('', sourcename)
# format the object's signature, if any
try:
sig = self.format_signature()
except Exception as exc:
logger.warning(__('error while formatting signature for %s: %s'),
self.fullname, exc, type='autodoc')
return
# generate the directive header and options, if applicable
self.add_directive_header(sig)
self.add_line('', sourcename)
# 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):
"""
Specialized Documenter subclass for modules.
"""
objtype = 'module'
content_indent = ''
titles_allowed = True
option_spec: OptionSpec = {
'members': members_option, 'undoc-members': bool_option,
'noindex': bool_option, 'inherited-members': inherited_members_option,
'show-inheritance': bool_option, 'synopsis': identity,
'platform': identity, 'deprecated': bool_option,
'member-order': member_order_option, 'exclude-members': exclude_members_option,
'private-members': members_option, 'special-members': members_option,
'imported-members': bool_option, 'ignore-module-all': bool_option
}
def __init__(self, *args: Any) -> None:
super().__init__(*args)
merge_members_option(self.options)
self.__all__: Optional[Sequence[str]] = None
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
# don't document submodules automatically
return False
def resolve_name(self, modname: str, parents: Any, path: str, base: Any
) -> Tuple[str, List[str]]:
if modname is not None:
logger.warning(__('"::" in automodule name doesn\'t make sense'),
type='autodoc')
return (path or '') + base, []
def parse_name(self) -> bool:
ret = super().parse_name()
if self.args or self.retann:
logger.warning(__('signature arguments or return annotation '
'given for automodule %s') % self.fullname,
type='autodoc')
return ret
def import_object(self, raiseerror: bool = False) -> bool:
ret = super().import_object(raiseerror)
try:
if not self.options.ignore_module_all:
self.__all__ = inspect.getall(self.object)
except ValueError as exc:
# invalid __all__ found.
logger.warning(__('__all__ should be a list of strings, not %r '
'(in module %s) -- ignoring __all__') %
(exc.args[0], self.fullname), type='autodoc')
return ret
def add_directive_header(self, sig: str) -> None:
Documenter.add_directive_header(self, sig)
sourcename = self.get_sourcename()
# add some module-specific options
if self.options.synopsis:
self.add_line(' :synopsis: ' + self.options.synopsis, sourcename)
if self.options.platform:
self.add_line(' :platform: ' + self.options.platform, sourcename)
if self.options.deprecated:
self.add_line(' :deprecated:', sourcename)
def get_module_members(self) -> Dict[str, ObjectMember]:
"""Get members of target module."""
if self.analyzer:
attr_docs = self.analyzer.attr_docs
else:
attr_docs = {}
members: Dict[str, ObjectMember] = {}
for name in dir(self.object):
try:
value = safe_getattr(self.object, name, None)
if ismock(value):
value = undecorate(value)
docstring = attr_docs.get(('', name), [])
members[name] = ObjectMember(name, value, docstring="\n".join(docstring))
except AttributeError:
continue
# annotation only member (ex. attr: int)
for name in inspect.getannotations(self.object):
if name not in members:
docstring = attr_docs.get(('', name), [])
members[name] = ObjectMember(name, INSTANCEATTR,
docstring="\n".join(docstring))
return members
def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]:
members = self.get_module_members()
if want_all:
if self.__all__ is None:
# for implicit module members, check __module__ to avoid
# documenting imported objects
return True, list(members.values())
else:
for member in members.values():
if member.__name__ not in self.__all__:
member.skipped = True
return False, list(members.values())
else:
memberlist = self.options.members or []
ret = []
for name in memberlist:
if name in members:
ret.append(members[name])
else:
logger.warning(__('missing attribute mentioned in :members: option: '
'module %s, attribute %s') %
(safe_getattr(self.object, '__name__', '???'), name),
type='autodoc')
return False, ret
def sort_members(self, documenters: List[Tuple["Documenter", bool]],
order: str) -> List[Tuple["Documenter", bool]]:
if order == 'bysource' and self.__all__:
# Sort alphabetically first (for members not listed on the __all__)
documenters.sort(key=lambda e: e[0].name)
# Sort by __all__
def keyfunc(entry: Tuple[Documenter, bool]) -> int:
name = entry[0].name.split('::')[1]
if self.__all__ and name in self.__all__:
return self.__all__.index(name)
else:
return len(self.__all__)
documenters.sort(key=keyfunc)
return documenters
else:
return super().sort_members(documenters, order)
class ModuleLevelDocumenter(Documenter):
"""
Specialized Documenter subclass for objects on module level (functions,
classes, data/constants).
"""
def resolve_name(self, modname: str, parents: Any, path: str, base: Any
) -> Tuple[str, List[str]]:
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 ...
modname = self.env.temp_data.get('autodoc:module')
# ... or in the scope of a module directive
if not modname:
modname = self.env.ref_context.get('py:module')
# ... else, it stays None, which means invalid
return modname, parents + [base]
class ClassLevelDocumenter(Documenter):
"""
Specialized Documenter subclass for objects on class level (methods,
attributes).
"""
def resolve_name(self, modname: str, parents: Any, path: str, base: Any
) -> Tuple[str, List[str]]:
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 ...
mod_cls = self.env.temp_data.get('autodoc:class')
# ... or from a class directive
if mod_cls is None:
mod_cls = self.env.ref_context.get('py:class')
# ... if still None, there's no way to know
if mod_cls is None:
return None, []
modname, sep, cls = mod_cls.rpartition('.')
parents = [cls]
# if the module name is still missing, get it like above
if not modname:
modname = self.env.temp_data.get('autodoc:module')
if not modname:
modname = self.env.ref_context.get('py:module')
# ... else, it stays None, which means invalid
return modname, parents + [base]
class DocstringSignatureMixin:
"""
Mixin for FunctionDocumenter and MethodDocumenter to provide the
feature of reading the signature from the docstring.
"""
_new_docstrings: List[List[str]] = None
_signatures: List[str] = None
def _find_signature(self) -> Tuple[str, str]:
# candidates of the object name
valid_names = [self.objpath[-1]] # type: ignore
if isinstance(self, ClassDocumenter):
valid_names.append('__init__')
if hasattr(self.object, '__mro__'):
valid_names.extend(cls.__name__ for cls in self.object.__mro__)
docstrings = self.get_doc()
if docstrings is None:
return None, None
self._new_docstrings = docstrings[:]
self._signatures = []
result = None
for i, doclines in enumerate(docstrings):
for j, line in enumerate(doclines):
if not line:
# no lines in docstring, no match
break
if line.endswith('\\'):
line = line.rstrip('\\').rstrip()
# match first line of docstring against signature RE
match = py_ext_sig_re.match(line)
if not match:
break
exmod, path, base, args, retann = match.groups()
# the base name must match ours
if base not in valid_names:
break
# re-prepare docstring to ignore more leading indentation
tab_width = self.directive.state.document.settings.tab_width # type: ignore
self._new_docstrings[i] = prepare_docstring('\n'.join(doclines[j + 1:]),
tabsize=tab_width)
if result is None:
# first signature
result = args, retann
else:
# subsequent signatures
self._signatures.append("(%s) -> %s" % (args, retann))
if result:
# finish the loop when signature found
break
return result
def get_doc(self, ignore: int = None) -> List[List[str]]:
if self._new_docstrings is not None:
return self._new_docstrings
return super().get_doc(ignore) # type: ignore
def format_signature(self, **kwargs: Any) -> str:
if self.args is None and self.config.autodoc_docstring_signature: # type: ignore
# only act if a signature is not explicitly given already, and if
# the feature is enabled
result = self._find_signature()
if result is not None:
self.args, self.retann = result
sig = super().format_signature(**kwargs) # type: ignore
if self._signatures:
return "\n".join([sig] + self._signatures)
else:
return sig
class DocstringStripSignatureMixin(DocstringSignatureMixin):
"""
Mixin for AttributeDocumenter to provide the
feature of stripping any function signature from the docstring.
"""
def format_signature(self, **kwargs: Any) -> str:
if self.args is None and self.config.autodoc_docstring_signature: # type: ignore
# only act if a signature is not explicitly given already, and if
# the feature is enabled
result = self._find_signature()
if result is not None:
# Discarding _args is a only difference with
# DocstringSignatureMixin.format_signature.
# Documenter.format_signature use self.args value to format.
_args, self.retann = result
return super().format_signature(**kwargs)
class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: ignore
"""
Specialized Documenter subclass for functions.
"""
objtype = 'function'
member_order = 30
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
# supports functions, builtins and bound methods exported at the module level
return (inspect.isfunction(member) or inspect.isbuiltin(member) or
(inspect.isroutine(member) and isinstance(parent, ModuleDocumenter)))
def format_args(self, **kwargs: Any) -> str:
if self.config.autodoc_typehints in ('none', 'description'):
kwargs.setdefault('show_annotation', False)
try:
self.env.app.emit('autodoc-before-process-signature', self.object, False)
sig = inspect.signature(self.object, type_aliases=self.config.autodoc_type_aliases)
args = stringify_signature(sig, **kwargs)
except TypeError as exc:
logger.warning(__("Failed to get a function signature for %s: %s"),
self.fullname, exc)
return None
except ValueError:
args = ''
if self.config.strip_signature_backslash:
# escape backslashes for reST
args = args.replace('\\', '\\\\')
return args
def document_members(self, all_members: bool = False) -> None:
pass
def add_directive_header(self, sig: str) -> None:
sourcename = self.get_sourcename()
super().add_directive_header(sig)
if inspect.iscoroutinefunction(self.object):
self.add_line(' :async:', sourcename)
def format_signature(self, **kwargs: Any) -> str:
sigs = []
if (self.analyzer and
'.'.join(self.objpath) in self.analyzer.overloads and
self.config.autodoc_typehints == 'signature'):
# Use signatures for overloaded functions instead of the implementation function.
overloaded = True
else:
overloaded = False
sig = super().format_signature(**kwargs)
sigs.append(sig)
if inspect.is_singledispatch_function(self.object):
# append signature of singledispatch'ed functions
for typ, func in self.object.registry.items():
if typ is object:
pass # default implementation. skipped.
else:
dispatchfunc = self.annotate_to_first_argument(func, typ)
if dispatchfunc:
documenter = FunctionDocumenter(self.directive, '')
documenter.object = dispatchfunc
documenter.objpath = [None]
sigs.append(documenter.format_signature())
if overloaded:
actual = inspect.signature(self.object,
type_aliases=self.config.autodoc_type_aliases)
__globals__ = safe_getattr(self.object, '__globals__', {})
for overload in self.analyzer.overloads.get('.'.join(self.objpath)):
overload = self.merge_default_value(actual, overload)
overload = evaluate_signature(overload, __globals__,
self.config.autodoc_type_aliases)
sig = stringify_signature(overload, **kwargs)
sigs.append(sig)
return "\n".join(sigs)
def merge_default_value(self, actual: Signature, overload: Signature) -> Signature:
"""Merge default values of actual implementation to the overload variants."""
parameters = list(overload.parameters.values())
for i, param in enumerate(parameters):
actual_param = actual.parameters.get(param.name)
if actual_param and param.default == '...':
parameters[i] = param.replace(default=actual_param.default)
return overload.replace(parameters=parameters)
def annotate_to_first_argument(self, func: Callable, typ: Type) -> Optional[Callable]:
"""Annotate type hint to the first argument of function if needed."""
try:
sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases)
except TypeError as exc:
logger.warning(__("Failed to get a function signature for %s: %s"),
self.fullname, exc)
return None
except ValueError:
return None
if len(sig.parameters) == 0:
return None
def dummy():
pass
params = list(sig.parameters.values())
if params[0].annotation is Parameter.empty:
params[0] = params[0].replace(annotation=typ)
try:
dummy.__signature__ = sig.replace(parameters=params) # type: ignore
return dummy
except (AttributeError, TypeError):
# failed to update signature (ex. built-in or extension types)
return None
else:
return None
class DecoratorDocumenter(FunctionDocumenter):
"""
Specialized Documenter subclass for decorator functions.
"""
objtype = 'decorator'
# must be lower than FunctionDocumenter
priority = -1
def format_args(self, **kwargs: Any) -> Any:
args = super().format_args(**kwargs)
if ',' in args:
return args
else:
return None
# Types which have confusing metaclass signatures it would be best not to show.
# These are listed by name, rather than storing the objects themselves, to avoid
# needing to import the modules.
_METACLASS_CALL_BLACKLIST = [
'enum.EnumMeta.__call__',
]
# Types whose __new__ signature is a pass-thru.
_CLASS_NEW_BLACKLIST = [
'typing.Generic.__new__',
]
class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: ignore
"""
Specialized Documenter subclass for classes.
"""
objtype = 'class'
member_order = 20
option_spec: OptionSpec = {
'members': members_option, 'undoc-members': bool_option,
'noindex': bool_option, 'inherited-members': inherited_members_option,
'show-inheritance': bool_option, 'member-order': member_order_option,
'exclude-members': exclude_members_option,
'private-members': members_option, 'special-members': members_option,
'class-doc-from': class_doc_from_option,
}
_signature_class: Any = None
_signature_method_name: str = None
def __init__(self, *args: Any) -> None:
super().__init__(*args)
merge_members_option(self.options)
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
return isinstance(member, type)
def import_object(self, raiseerror: bool = False) -> bool:
ret = super().import_object(raiseerror)
# if the class is documented under another name, document it
# as data/attribute
if ret:
if hasattr(self.object, '__name__'):
self.doc_as_attr = (self.objpath[-1] != self.object.__name__)
else:
self.doc_as_attr = True
return ret
def _get_signature(self) -> Tuple[Optional[Any], Optional[str], Optional[Signature]]:
def get_user_defined_function_or_method(obj: Any, attr: str) -> Any:
""" Get the `attr` function or method from `obj`, if it is user-defined. """
if inspect.is_builtin_class_method(obj, attr):
return None
attr = self.get_attr(obj, attr, None)
if not (inspect.ismethod(attr) or inspect.isfunction(attr)):
return None
return attr
# This sequence is copied from inspect._signature_from_callable.
# ValueError means that no signature could be found, so we keep going.
# First, we check the obj has a __signature__ attribute
if (hasattr(self.object, '__signature__') and
isinstance(self.object.__signature__, Signature)):
return None, None, self.object.__signature__
# Next, let's see if it has an overloaded __call__ defined
# in its metaclass
call = get_user_defined_function_or_method(type(self.object), '__call__')
if call is not None:
if "{0.__module__}.{0.__qualname__}".format(call) in _METACLASS_CALL_BLACKLIST:
call = None
if call is not None:
self.env.app.emit('autodoc-before-process-signature', call, True)
try:
sig = inspect.signature(call, bound_method=True,
type_aliases=self.config.autodoc_type_aliases)
return type(self.object), '__call__', sig
except ValueError:
pass
# Now we check if the 'obj' class has a '__new__' method
new = get_user_defined_function_or_method(self.object, '__new__')
if new is not None:
if "{0.__module__}.{0.__qualname__}".format(new) in _CLASS_NEW_BLACKLIST:
new = None
if new is not None:
self.env.app.emit('autodoc-before-process-signature', new, True)
try:
sig = inspect.signature(new, bound_method=True,
type_aliases=self.config.autodoc_type_aliases)
return self.object, '__new__', sig
except ValueError:
pass
# Finally, we should have at least __init__ implemented
init = get_user_defined_function_or_method(self.object, '__init__')
if init is not None:
self.env.app.emit('autodoc-before-process-signature', init, True)
try:
sig = inspect.signature(init, bound_method=True,
type_aliases=self.config.autodoc_type_aliases)
return self.object, '__init__', sig
except ValueError:
pass
# None of the attributes are user-defined, so fall back to let inspect
# handle it.
# We don't know the exact method that inspect.signature will read
# the signature from, so just pass the object itself to our hook.
self.env.app.emit('autodoc-before-process-signature', self.object, False)
try:
sig = inspect.signature(self.object, bound_method=False,
type_aliases=self.config.autodoc_type_aliases)
return None, None, sig
except ValueError:
pass
# Still no signature: happens e.g. for old-style classes
# with __init__ in C and no `__text_signature__`.
return None, None, None
def format_args(self, **kwargs: Any) -> str:
if self.config.autodoc_typehints in ('none', 'description'):
kwargs.setdefault('show_annotation', False)
try:
self._signature_class, self._signature_method_name, sig = self._get_signature()
except TypeError as exc:
# __signature__ attribute contained junk
logger.warning(__("Failed to get a constructor signature for %s: %s"),
self.fullname, exc)
return None
if sig is None:
return None
return stringify_signature(sig, show_return_annotation=False, **kwargs)
def format_signature(self, **kwargs: Any) -> str:
if self.doc_as_attr:
return ''
sig = super().format_signature()
sigs = []
overloads = self.get_overloaded_signatures()
if overloads and self.config.autodoc_typehints == 'signature':
# Use signatures for overloaded methods instead of the implementation method.
method = safe_getattr(self._signature_class, self._signature_method_name, None)
__globals__ = safe_getattr(method, '__globals__', {})
for overload in overloads:
overload = evaluate_signature(overload, __globals__,
self.config.autodoc_type_aliases)
parameters = list(overload.parameters.values())
overload = overload.replace(parameters=parameters[1:],
return_annotation=Parameter.empty)
sig = stringify_signature(overload, **kwargs)
sigs.append(sig)
else:
sigs.append(sig)
return "\n".join(sigs)
def get_overloaded_signatures(self) -> List[Signature]:
if self._signature_class and self._signature_method_name:
for cls in self._signature_class.__mro__:
try:
analyzer = ModuleAnalyzer.for_module(cls.__module__)
analyzer.analyze()
qualname = '.'.join([cls.__qualname__, self._signature_method_name])
if qualname in analyzer.overloads:
return analyzer.overloads.get(qualname)
elif qualname in analyzer.tagorder:
# the constructor is defined in the class, but not overrided.
return []
except PycodeError:
pass
return []
def get_canonical_fullname(self) -> Optional[str]:
__modname__ = safe_getattr(self.object, '__module__', self.modname)
__qualname__ = safe_getattr(self.object, '__qualname__', None)
if __qualname__ is None:
__qualname__ = safe_getattr(self.object, '__name__', None)
if __qualname__ and '<locals>' in __qualname__:
# No valid qualname found if the object is defined as locals
__qualname__ = None
if __modname__ and __qualname__:
return '.'.join([__modname__, __qualname__])
else:
return None
def add_directive_header(self, sig: str) -> None:
sourcename = self.get_sourcename()
if self.doc_as_attr:
self.directivetype = 'attribute'
super().add_directive_header(sig)
if self.analyzer and '.'.join(self.objpath) in self.analyzer.finals:
self.add_line(' :final:', sourcename)
canonical_fullname = self.get_canonical_fullname()
if not self.doc_as_attr and canonical_fullname and self.fullname != canonical_fullname:
self.add_line(' :canonical: %s' % canonical_fullname, sourcename)
# add inheritance info, if wanted
if not self.doc_as_attr and self.options.show_inheritance:
sourcename = self.get_sourcename()
self.add_line('', sourcename)
if hasattr(self.object, '__orig_bases__') and len(self.object.__orig_bases__):
# A subclass of generic types
# refs: PEP-560 <https://www.python.org/dev/peps/pep-0560/>
bases = [restify(cls) for cls in self.object.__orig_bases__]
self.add_line(' ' + _('Bases: %s') % ', '.join(bases), sourcename)
elif hasattr(self.object, '__bases__') and len(self.object.__bases__):
# A normal class
bases = [restify(cls) for cls in self.object.__bases__]
self.add_line(' ' + _('Bases: %s') % ', '.join(bases), sourcename)
def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]:
members = get_class_members(self.object, self.objpath, self.get_attr)
if not want_all:
if not self.options.members:
return False, [] # type: ignore
# specific members given
selected = []
for name in self.options.members: # type: str
if name in members:
selected.append(members[name])
else:
logger.warning(__('missing attribute %s in object %s') %
(name, self.fullname), type='autodoc')
return False, selected
elif self.options.inherited_members:
return False, list(members.values())
else:
return False, [m for m in members.values() if m.class_ == self.object]
def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
if self.doc_as_attr:
# Don't show the docstring of the class when it is an alias.
return None
lines = getattr(self, '_new_docstrings', None)
if lines is not None:
return lines
classdoc_from = self.options.get('class-doc-from', self.config.autoclass_content)
docstrings = []
attrdocstring = self.get_attr(self.object, '__doc__', None)
if attrdocstring:
docstrings.append(attrdocstring)
# for classes, what the "docstring" is can be controlled via a
# config value; the default is only the class docstring
if classdoc_from in ('both', 'init'):
__init__ = self.get_attr(self.object, '__init__', None)
initdocstring = getdoc(__init__, self.get_attr,
self.config.autodoc_inherit_docstrings,
self.parent, self.object_name)
# for new-style classes, no __init__ means default __init__
if (initdocstring is not None and
(initdocstring == object.__init__.__doc__ or # for pypy
initdocstring.strip() == object.__init__.__doc__)): # for !pypy
initdocstring = None
if not initdocstring:
# try __new__
__new__ = self.get_attr(self.object, '__new__', None)
initdocstring = getdoc(__new__, self.get_attr,
self.config.autodoc_inherit_docstrings,
self.parent, self.object_name)
# for new-style classes, no __new__ means default __new__
if (initdocstring is not None and
(initdocstring == object.__new__.__doc__ or # for pypy
initdocstring.strip() == object.__new__.__doc__)): # for !pypy
initdocstring = None
if initdocstring:
if classdoc_from == 'init':
docstrings = [initdocstring]
else:
docstrings.append(initdocstring)
tab_width = self.directive.state.document.settings.tab_width
return [prepare_docstring(docstring, ignore, tab_width) for docstring in docstrings]
def add_content(self, more_content: Optional[StringList], no_docstring: bool = False
) -> None:
if self.doc_as_attr:
try:
more_content = StringList([_('alias of %s') % restify(self.object)], source='')
except AttributeError:
pass # Invalid class object is passed.
super().add_content(more_content)
def document_members(self, all_members: bool = False) -> None:
if self.doc_as_attr:
return
super().document_members(all_members)
def generate(self, more_content: Optional[StringList] = None, real_modname: str = None,
check_module: bool = False, all_members: bool = False) -> None:
# Do not pass real_modname and use the name from the __module__
# attribute of the class.
# If a class gets imported into the module real_modname
# the analyzer won't find the source of the class, if
# it looks in real_modname.
return super().generate(more_content=more_content,
check_module=check_module,
all_members=all_members)
class ExceptionDocumenter(ClassDocumenter):
"""
Specialized ClassDocumenter subclass for exceptions.
"""
objtype = 'exception'
member_order = 10
# needs a higher priority than ClassDocumenter
priority = 10
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
return isinstance(member, type) and issubclass(member, BaseException)
class DataDocumenterMixinBase:
# define types of instance variables
config: Config = None
env: BuildEnvironment = None
modname: str = None
parent: Any = None
object: Any = None
objpath: List[str] = None
def should_suppress_directive_header(self) -> bool:
"""Check directive header should be suppressed."""
return False
def should_suppress_value_header(self) -> bool:
"""Check :value: header should be suppressed."""
return False
def update_content(self, more_content: StringList) -> None:
"""Update docstring for the NewType object."""
pass
class GenericAliasMixin(DataDocumenterMixinBase):
"""
Mixin for DataDocumenter and AttributeDocumenter to provide the feature for
supporting GenericAliases.
"""
def should_suppress_directive_header(self) -> bool:
return (inspect.isgenericalias(self.object) or
super().should_suppress_directive_header())
def update_content(self, more_content: StringList) -> None:
if inspect.isgenericalias(self.object):
more_content.append(_('alias of %s') % restify(self.object), '')
more_content.append('', '')
super().update_content(more_content)
class NewTypeMixin(DataDocumenterMixinBase):
"""
Mixin for DataDocumenter and AttributeDocumenter to provide the feature for
supporting NewTypes.
"""
def should_suppress_directive_header(self) -> bool:
return (inspect.isNewType(self.object) or
super().should_suppress_directive_header())
def update_content(self, more_content: StringList) -> None:
if inspect.isNewType(self.object):
supertype = restify(self.object.__supertype__)
more_content.append(_('alias of %s') % supertype, '')
more_content.append('', '')
super().update_content(more_content)
class TypeVarMixin(DataDocumenterMixinBase):
"""
Mixin for DataDocumenter and AttributeDocumenter to provide the feature for
supporting TypeVars.
"""
def should_suppress_directive_header(self) -> bool:
return (isinstance(self.object, TypeVar) or
super().should_suppress_directive_header())
def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
if ignore is not None:
warnings.warn("The 'ignore' argument to autodoc.%s.get_doc() is deprecated."
% self.__class__.__name__,
RemovedInSphinx50Warning, stacklevel=2)
if isinstance(self.object, TypeVar):
if self.object.__doc__ != TypeVar.__doc__:
return super().get_doc() # type: ignore
else:
return []
else:
return super().get_doc() # type: ignore
def update_content(self, more_content: StringList) -> None:
if isinstance(self.object, TypeVar):
attrs = [repr(self.object.__name__)]
for constraint in self.object.__constraints__:
attrs.append(stringify_typehint(constraint))
if self.object.__bound__:
attrs.append(r"bound=\ " + restify(self.object.__bound__))
if self.object.__covariant__:
attrs.append("covariant=True")
if self.object.__contravariant__:
attrs.append("contravariant=True")
more_content.append(_('alias of TypeVar(%s)') % ", ".join(attrs), '')
more_content.append('', '')
super().update_content(more_content)
class UninitializedGlobalVariableMixin(DataDocumenterMixinBase):
"""
Mixin for DataDocumenter to provide the feature for supporting uninitialized
(type annotation only) global variables.
"""
def import_object(self, raiseerror: bool = False) -> bool:
try:
return super().import_object(raiseerror=True) # type: ignore
except ImportError as exc:
# annotation only instance variable (PEP-526)
try:
with mock(self.config.autodoc_mock_imports):
parent = import_module(self.modname, self.config.autodoc_warningiserror)
annotations = get_type_hints(parent, None,
self.config.autodoc_type_aliases)
if self.objpath[-1] in annotations:
self.object = UNINITIALIZED_ATTR
self.parent = parent
return True
except ImportError:
pass
if raiseerror:
raise
else:
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
self.env.note_reread()
return False
def should_suppress_value_header(self) -> bool:
return (self.object is UNINITIALIZED_ATTR or
super().should_suppress_value_header())
def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
if self.object is UNINITIALIZED_ATTR:
return []
else:
return super().get_doc(ignore) # type: ignore
class DataDocumenter(GenericAliasMixin, NewTypeMixin, TypeVarMixin,
UninitializedGlobalVariableMixin, ModuleLevelDocumenter):
"""
Specialized Documenter subclass for data items.
"""
objtype = 'data'
member_order = 40
priority = -10
option_spec: OptionSpec = dict(ModuleLevelDocumenter.option_spec)
option_spec["annotation"] = annotation_option
option_spec["no-value"] = bool_option
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
return isinstance(parent, ModuleDocumenter) and isattr
def update_annotations(self, parent: Any) -> None:
"""Update __annotations__ to support type_comment and so on."""
annotations = dict(inspect.getannotations(parent))
parent.__annotations__ = annotations
try:
analyzer = ModuleAnalyzer.for_module(self.modname)
analyzer.analyze()
for (classname, attrname), annotation in analyzer.annotations.items():
if classname == '' and attrname not in annotations:
annotations[attrname] = annotation
except PycodeError:
pass
def import_object(self, raiseerror: bool = False) -> bool:
ret = super().import_object(raiseerror)
if self.parent:
self.update_annotations(self.parent)
return ret
def should_suppress_value_header(self) -> bool:
if super().should_suppress_value_header():
return True
else:
doc = self.get_doc()
metadata = extract_metadata('\n'.join(sum(doc, [])))
if 'hide-value' in metadata:
return True
return False
def add_directive_header(self, sig: str) -> None:
super().add_directive_header(sig)
sourcename = self.get_sourcename()
if self.options.annotation is SUPPRESS or self.should_suppress_directive_header():
pass
elif self.options.annotation:
self.add_line(' :annotation: %s' % self.options.annotation,
sourcename)
else:
# obtain annotation for this data
annotations = get_type_hints(self.parent, None, self.config.autodoc_type_aliases)
if self.objpath[-1] in annotations:
objrepr = stringify_typehint(annotations.get(self.objpath[-1]))
self.add_line(' :type: ' + objrepr, sourcename)
try:
if self.options.no_value or self.should_suppress_value_header():
pass
else:
objrepr = object_description(self.object)
self.add_line(' :value: ' + objrepr, sourcename)
except ValueError:
pass
def document_members(self, all_members: bool = False) -> None:
pass
def get_real_modname(self) -> str:
return self.get_attr(self.parent or self.object, '__module__', None) \
or self.modname
def get_module_comment(self, attrname: str) -> Optional[List[str]]:
try:
analyzer = ModuleAnalyzer.for_module(self.modname)
analyzer.analyze()
key = ('', attrname)
if key in analyzer.attr_docs:
return list(analyzer.attr_docs[key])
except PycodeError:
pass
return None
def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
# Check the variable has a docstring-comment
comment = self.get_module_comment(self.objpath[-1])
if comment:
return [comment]
else:
return super().get_doc(ignore)
def add_content(self, more_content: Optional[StringList], no_docstring: bool = False
) -> None:
# Disable analyzing variable comment on Documenter.add_content() to control it on
# DataDocumenter.add_content()
self.analyzer = None
if not more_content:
more_content = StringList()
self.update_content(more_content)
super().add_content(more_content, no_docstring=no_docstring)
class NewTypeDataDocumenter(DataDocumenter):
"""
Specialized Documenter subclass for NewTypes.
Note: This must be invoked before FunctionDocumenter because NewType is a kind of
function object.
"""
objtype = 'newtypedata'
directivetype = 'data'
priority = FunctionDocumenter.priority + 1
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
return inspect.isNewType(member) and isattr
class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: ignore
"""
Specialized Documenter subclass for methods (normal, static and class).
"""
objtype = 'method'
directivetype = 'method'
member_order = 50
priority = 1 # must be more than FunctionDocumenter
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
return inspect.isroutine(member) and \
not isinstance(parent, ModuleDocumenter)
def import_object(self, raiseerror: bool = False) -> bool:
ret = super().import_object(raiseerror)
if not ret:
return ret
# to distinguish classmethod/staticmethod
obj = self.parent.__dict__.get(self.object_name)
if obj is None:
obj = self.object
if (inspect.isclassmethod(obj) or
inspect.isstaticmethod(obj, cls=self.parent, name=self.object_name)):
# document class and static members before ordinary ones
self.member_order = self.member_order - 1
return ret
def format_args(self, **kwargs: Any) -> str:
if self.config.autodoc_typehints in ('none', 'description'):
kwargs.setdefault('show_annotation', False)
try:
if self.object == object.__init__ and self.parent != object:
# Classes not having own __init__() method are shown as no arguments.
#
# Note: The signature of object.__init__() is (self, /, *args, **kwargs).
# But it makes users confused.
args = '()'
else:
if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name):
self.env.app.emit('autodoc-before-process-signature', self.object, False)
sig = inspect.signature(self.object, bound_method=False,
type_aliases=self.config.autodoc_type_aliases)
else:
self.env.app.emit('autodoc-before-process-signature', self.object, True)
sig = inspect.signature(self.object, bound_method=True,
type_aliases=self.config.autodoc_type_aliases)
args = stringify_signature(sig, **kwargs)
except TypeError as exc:
logger.warning(__("Failed to get a method signature for %s: %s"),
self.fullname, exc)
return None
except ValueError:
args = ''
if self.config.strip_signature_backslash:
# escape backslashes for reST
args = args.replace('\\', '\\\\')
return args
def add_directive_header(self, sig: str) -> None:
super().add_directive_header(sig)
sourcename = self.get_sourcename()
obj = self.parent.__dict__.get(self.object_name, self.object)
if inspect.isabstractmethod(obj):
self.add_line(' :abstractmethod:', sourcename)
if inspect.iscoroutinefunction(obj):
self.add_line(' :async:', sourcename)
if inspect.isclassmethod(obj):
self.add_line(' :classmethod:', sourcename)
if inspect.isstaticmethod(obj, cls=self.parent, name=self.object_name):
self.add_line(' :staticmethod:', sourcename)
if self.analyzer and '.'.join(self.objpath) in self.analyzer.finals:
self.add_line(' :final:', sourcename)
def document_members(self, all_members: bool = False) -> None:
pass
def format_signature(self, **kwargs: Any) -> str:
sigs = []
if (self.analyzer and
'.'.join(self.objpath) in self.analyzer.overloads and
self.config.autodoc_typehints == 'signature'):
# Use signatures for overloaded methods instead of the implementation method.
overloaded = True
else:
overloaded = False
sig = super().format_signature(**kwargs)
sigs.append(sig)
meth = self.parent.__dict__.get(self.objpath[-1])
if inspect.is_singledispatch_method(meth):
# append signature of singledispatch'ed functions
for typ, func in meth.dispatcher.registry.items():
if typ is object:
pass # default implementation. skipped.
else:
dispatchmeth = self.annotate_to_first_argument(func, typ)
if dispatchmeth:
documenter = MethodDocumenter(self.directive, '')
documenter.parent = self.parent
documenter.object = dispatchmeth
documenter.objpath = [None]
sigs.append(documenter.format_signature())
if overloaded:
if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name):
actual = inspect.signature(self.object, bound_method=False,
type_aliases=self.config.autodoc_type_aliases)
else:
actual = inspect.signature(self.object, bound_method=True,
type_aliases=self.config.autodoc_type_aliases)
__globals__ = safe_getattr(self.object, '__globals__', {})
for overload in self.analyzer.overloads.get('.'.join(self.objpath)):
overload = self.merge_default_value(actual, overload)
overload = evaluate_signature(overload, __globals__,
self.config.autodoc_type_aliases)
if not inspect.isstaticmethod(self.object, cls=self.parent,
name=self.object_name):
parameters = list(overload.parameters.values())
overload = overload.replace(parameters=parameters[1:])
sig = stringify_signature(overload, **kwargs)
sigs.append(sig)
return "\n".join(sigs)
def merge_default_value(self, actual: Signature, overload: Signature) -> Signature:
"""Merge default values of actual implementation to the overload variants."""
parameters = list(overload.parameters.values())
for i, param in enumerate(parameters):
actual_param = actual.parameters.get(param.name)
if actual_param and param.default == '...':
parameters[i] = param.replace(default=actual_param.default)
return overload.replace(parameters=parameters)
def annotate_to_first_argument(self, func: Callable, typ: Type) -> Optional[Callable]:
"""Annotate type hint to the first argument of function if needed."""
try:
sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases)
except TypeError as exc:
logger.warning(__("Failed to get a method signature for %s: %s"),
self.fullname, exc)
return None
except ValueError:
return None
if len(sig.parameters) == 1:
return None
def dummy():
pass
params = list(sig.parameters.values())
if params[1].annotation is Parameter.empty:
params[1] = params[1].replace(annotation=typ)
try:
dummy.__signature__ = sig.replace(parameters=params) # type: ignore
return dummy
except (AttributeError, TypeError):
# failed to update signature (ex. built-in or extension types)
return None
else:
return None
class NonDataDescriptorMixin(DataDocumenterMixinBase):
"""
Mixin for AttributeDocumenter to provide the feature for supporting non
data-descriptors.
.. note:: This mix-in must be inherited after other mix-ins. Otherwise, docstring
and :value: header will be suppressed unexpectedly.
"""
def import_object(self, raiseerror: bool = False) -> bool:
ret = super().import_object(raiseerror) # type: ignore
if ret and not inspect.isattributedescriptor(self.object):
self.non_data_descriptor = True
else:
self.non_data_descriptor = False
return ret
def should_suppress_value_header(self) -> bool:
return (not getattr(self, 'non_data_descriptor', False) or
super().should_suppress_directive_header())
def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
if getattr(self, 'non_data_descriptor', False):
# the docstring of non datadescriptor is very probably the wrong thing
# to display
return None
else:
return super().get_doc(ignore) # type: ignore
class SlotsMixin(DataDocumenterMixinBase):
"""
Mixin for AttributeDocumenter to provide the feature for supporting __slots__.
"""
def isslotsattribute(self) -> bool:
"""Check the subject is an attribute in __slots__."""
try:
__slots__ = inspect.getslots(self.parent)
if __slots__ and self.objpath[-1] in __slots__:
return True
else:
return False
except (ValueError, TypeError):
return False
def import_object(self, raiseerror: bool = False) -> bool:
ret = super().import_object(raiseerror) # type: ignore
if self.isslotsattribute():
self.object = SLOTSATTR
return ret
def should_suppress_directive_header(self) -> bool:
if self.object is SLOTSATTR:
self._datadescriptor = True
return True
else:
return super().should_suppress_directive_header()
def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
if self.object is SLOTSATTR:
try:
__slots__ = inspect.getslots(self.parent)
if __slots__ and __slots__.get(self.objpath[-1]):
docstring = prepare_docstring(__slots__[self.objpath[-1]])
return [docstring]
else:
return []
except ValueError as exc:
logger.warning(__('Invalid __slots__ found on %s. Ignored.'),
(self.parent.__qualname__, exc), type='autodoc')
return []
else:
return super().get_doc(ignore) # type: ignore
class RuntimeInstanceAttributeMixin(DataDocumenterMixinBase):
"""
Mixin for AttributeDocumenter to provide the feature for supporting runtime
instance attributes (that are defined in __init__() methods with doc-comments).
Example:
class Foo:
def __init__(self):
self.attr = None #: This is a target of this mix-in.
"""
RUNTIME_INSTANCE_ATTRIBUTE = object()
def is_runtime_instance_attribute(self, parent: Any) -> bool:
"""Check the subject is an attribute defined in __init__()."""
# An instance variable defined in __init__().
if self.get_attribute_comment(parent, self.objpath[-1]): # type: ignore
return True
else:
return False
def import_object(self, raiseerror: bool = False) -> bool:
"""Check the existence of runtime instance attribute when failed to import the
attribute."""
try:
return super().import_object(raiseerror=True) # type: ignore
except ImportError as exc:
try:
with mock(self.config.autodoc_mock_imports):
ret = import_object(self.modname, self.objpath[:-1], 'class',
attrgetter=self.get_attr, # type: ignore
warningiserror=self.config.autodoc_warningiserror)
parent = ret[3]
if self.is_runtime_instance_attribute(parent):
self.object = self.RUNTIME_INSTANCE_ATTRIBUTE
self.parent = parent
return True
except ImportError:
pass
if raiseerror:
raise
else:
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
self.env.note_reread()
return False
def should_suppress_value_header(self) -> bool:
return (self.object is self.RUNTIME_INSTANCE_ATTRIBUTE or
super().should_suppress_value_header())
class UninitializedInstanceAttributeMixin(DataDocumenterMixinBase):
"""
Mixin for AttributeDocumenter to provide the feature for supporting uninitialized
instance attributes (PEP-526 styled, annotation only attributes).
Example:
class Foo:
attr: int #: This is a target of this mix-in.
"""
def is_uninitialized_instance_attribute(self, parent: Any) -> bool:
"""Check the subject is an annotation only attribute."""
annotations = get_type_hints(parent, None, self.config.autodoc_type_aliases)
if self.objpath[-1] in annotations:
return True
else:
return False
def import_object(self, raiseerror: bool = False) -> bool:
"""Check the exisitence of uninitialized instance attribute when failed to import
the attribute."""
try:
return super().import_object(raiseerror=True) # type: ignore
except ImportError as exc:
try:
ret = import_object(self.modname, self.objpath[:-1], 'class',
attrgetter=self.get_attr, # type: ignore
warningiserror=self.config.autodoc_warningiserror)
parent = ret[3]
if self.is_uninitialized_instance_attribute(parent):
self.object = UNINITIALIZED_ATTR
self.parent = parent
return True
except ImportError:
pass
if raiseerror:
raise
else:
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
self.env.note_reread()
return False
def should_suppress_value_header(self) -> bool:
return (self.object is UNINITIALIZED_ATTR or
super().should_suppress_value_header())
def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
if self.object is UNINITIALIZED_ATTR:
return None
else:
return super().get_doc(ignore) # type: ignore
class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type: ignore
TypeVarMixin, RuntimeInstanceAttributeMixin,
UninitializedInstanceAttributeMixin, NonDataDescriptorMixin,
DocstringStripSignatureMixin, ClassLevelDocumenter):
"""
Specialized Documenter subclass for attributes.
"""
objtype = 'attribute'
member_order = 60
option_spec: OptionSpec = dict(ModuleLevelDocumenter.option_spec)
option_spec["annotation"] = annotation_option
option_spec["no-value"] = bool_option
# must be higher than the MethodDocumenter, else it will recognize
# some non-data descriptors as methods
priority = 10
@staticmethod
def is_function_or_method(obj: Any) -> bool:
return inspect.isfunction(obj) or inspect.isbuiltin(obj) or inspect.ismethod(obj)
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
if inspect.isattributedescriptor(member):
return True
elif (not isinstance(parent, ModuleDocumenter) and
not inspect.isroutine(member) and
not isinstance(member, type)):
return True
else:
return False
def document_members(self, all_members: bool = False) -> None:
pass
def isinstanceattribute(self) -> bool:
"""Check the subject is an instance attribute."""
warnings.warn('AttributeDocumenter.isinstanceattribute() is deprecated.',
RemovedInSphinx50Warning)
# uninitialized instance variable (PEP-526)
with mock(self.config.autodoc_mock_imports):
try:
ret = import_object(self.modname, self.objpath[:-1], 'class',
attrgetter=self.get_attr,
warningiserror=self.config.autodoc_warningiserror)
self.parent = ret[3]
annotations = get_type_hints(self.parent, None,
self.config.autodoc_type_aliases)
if self.objpath[-1] in annotations:
self.object = UNINITIALIZED_ATTR
return True
except ImportError:
pass
return False
def update_annotations(self, parent: Any) -> None:
"""Update __annotations__ to support type_comment and so on."""
try:
annotations = dict(inspect.getannotations(parent))
parent.__annotations__ = annotations
for cls in inspect.getmro(parent):
try:
module = safe_getattr(cls, '__module__')
qualname = safe_getattr(cls, '__qualname__')
analyzer = ModuleAnalyzer.for_module(module)
analyzer.analyze()
for (classname, attrname), annotation in analyzer.annotations.items():
if classname == qualname and attrname not in annotations:
annotations[attrname] = annotation
except (AttributeError, PycodeError):
pass
except (AttributeError, TypeError):
# Failed to set __annotations__ (built-in, extensions, etc.)
pass
def import_object(self, raiseerror: bool = False) -> bool:
ret = super().import_object(raiseerror)
if inspect.isenumattribute(self.object):
self.object = self.object.value
if self.parent:
self.update_annotations(self.parent)
return ret
def get_real_modname(self) -> str:
return self.get_attr(self.parent or self.object, '__module__', None) \
or self.modname
def should_suppress_value_header(self) -> bool:
if super().should_suppress_value_header():
return True
else:
doc = self.get_doc()
if doc:
metadata = extract_metadata('\n'.join(sum(doc, [])))
if 'hide-value' in metadata:
return True
return False
def add_directive_header(self, sig: str) -> None:
super().add_directive_header(sig)
sourcename = self.get_sourcename()
if self.options.annotation is SUPPRESS or self.should_suppress_directive_header():
pass
elif self.options.annotation:
self.add_line(' :annotation: %s' % self.options.annotation, sourcename)
else:
# obtain type annotation for this attribute
annotations = get_type_hints(self.parent, None, self.config.autodoc_type_aliases)
if self.objpath[-1] in annotations:
objrepr = stringify_typehint(annotations.get(self.objpath[-1]))
self.add_line(' :type: ' + objrepr, sourcename)
try:
if self.options.no_value or self.should_suppress_value_header():
pass
else:
objrepr = object_description(self.object)
self.add_line(' :value: ' + objrepr, sourcename)
except ValueError:
pass
def get_attribute_comment(self, parent: Any, attrname: str) -> Optional[List[str]]:
for cls in inspect.getmro(parent):
try:
module = safe_getattr(cls, '__module__')
qualname = safe_getattr(cls, '__qualname__')
analyzer = ModuleAnalyzer.for_module(module)
analyzer.analyze()
if qualname and self.objpath:
key = (qualname, attrname)
if key in analyzer.attr_docs:
return list(analyzer.attr_docs[key])
except (AttributeError, PycodeError):
pass
return None
def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
# Check the attribute has a docstring-comment
comment = self.get_attribute_comment(self.parent, self.objpath[-1])
if comment:
return [comment]
try:
# Disable `autodoc_inherit_docstring` temporarily to avoid to obtain
# a docstring from the value which descriptor returns unexpectedly.
# ref: https://github.com/sphinx-doc/sphinx/issues/7805
orig = self.config.autodoc_inherit_docstrings
self.config.autodoc_inherit_docstrings = False # type: ignore
return super().get_doc(ignore)
finally:
self.config.autodoc_inherit_docstrings = orig # type: ignore
def add_content(self, more_content: Optional[StringList], no_docstring: bool = False
) -> None:
# Disable analyzing attribute comment on Documenter.add_content() to control it on
# AttributeDocumenter.add_content()
self.analyzer = None
if more_content is None:
more_content = StringList()
self.update_content(more_content)
super().add_content(more_content, no_docstring)
class PropertyDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): # type: ignore
"""
Specialized Documenter subclass for properties.
"""
objtype = 'property'
member_order = 60
# before AttributeDocumenter
priority = AttributeDocumenter.priority + 1
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
return inspect.isproperty(member) and isinstance(parent, ClassDocumenter)
def document_members(self, all_members: bool = False) -> None:
pass
def get_real_modname(self) -> str:
return self.get_attr(self.parent or self.object, '__module__', None) \
or self.modname
def add_directive_header(self, sig: str) -> None:
super().add_directive_header(sig)
sourcename = self.get_sourcename()
if inspect.isabstractmethod(self.object):
self.add_line(' :abstractmethod:', sourcename)
if safe_getattr(self.object, 'fget', None):
try:
signature = inspect.signature(self.object.fget,
type_aliases=self.config.autodoc_type_aliases)
if signature.return_annotation is not Parameter.empty:
objrepr = stringify_typehint(signature.return_annotation)
self.add_line(' :type: ' + objrepr, sourcename)
except TypeError as exc:
logger.warning(__("Failed to get a function signature for %s: %s"),
self.fullname, exc)
return None
except ValueError:
raise
class NewTypeAttributeDocumenter(AttributeDocumenter):
"""
Specialized Documenter subclass for NewTypes.
Note: This must be invoked before MethodDocumenter because NewType is a kind of
function object.
"""
objtype = 'newvarattribute'
directivetype = 'attribute'
priority = MethodDocumenter.priority + 1
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
return not isinstance(parent, ModuleDocumenter) and inspect.isNewType(member)
def get_documenters(app: Sphinx) -> Dict[str, Type[Documenter]]:
"""Returns registered Documenter classes"""
warnings.warn("get_documenters() is deprecated.", RemovedInSphinx50Warning, stacklevel=2)
return app.registry.documenters
def autodoc_attrgetter(app: Sphinx, obj: Any, name: str, *defargs: Any) -> Any:
"""Alternative getattr() for types"""
for typ, func in app.registry.autodoc_attrgettrs.items():
if isinstance(obj, typ):
return func(obj, name, *defargs)
return safe_getattr(obj, name, *defargs)
def migrate_autodoc_member_order(app: Sphinx, config: Config) -> None:
if config.autodoc_member_order == 'alphabetic':
# RemovedInSphinx50Warning
logger.warning(__('autodoc_member_order now accepts "alphabetical" '
'instead of "alphabetic". Please update your setting.'))
config.autodoc_member_order = 'alphabetical' # type: ignore
# for compatibility
from sphinx.ext.autodoc.deprecated import DataDeclarationDocumenter # NOQA
from sphinx.ext.autodoc.deprecated import GenericAliasDocumenter # NOQA
from sphinx.ext.autodoc.deprecated import InstanceAttributeDocumenter # NOQA
from sphinx.ext.autodoc.deprecated import SingledispatchFunctionDocumenter # NOQA
from sphinx.ext.autodoc.deprecated import SingledispatchMethodDocumenter # NOQA
from sphinx.ext.autodoc.deprecated import SlotsAttributeDocumenter # NOQA
from sphinx.ext.autodoc.deprecated import TypeVarDocumenter # NOQA
def setup(app: Sphinx) -> Dict[str, Any]:
app.add_autodocumenter(ModuleDocumenter)
app.add_autodocumenter(ClassDocumenter)
app.add_autodocumenter(ExceptionDocumenter)
app.add_autodocumenter(DataDocumenter)
app.add_autodocumenter(NewTypeDataDocumenter)
app.add_autodocumenter(FunctionDocumenter)
app.add_autodocumenter(DecoratorDocumenter)
app.add_autodocumenter(MethodDocumenter)
app.add_autodocumenter(AttributeDocumenter)
app.add_autodocumenter(PropertyDocumenter)
app.add_autodocumenter(NewTypeAttributeDocumenter)
app.add_config_value('autoclass_content', 'class', True, ENUM('both', 'class', 'init'))
app.add_config_value('autodoc_member_order', 'alphabetical', True,
ENUM('alphabetic', 'alphabetical', 'bysource', 'groupwise'))
app.add_config_value('autodoc_default_options', {}, True)
app.add_config_value('autodoc_docstring_signature', True, True)
app.add_config_value('autodoc_mock_imports', [], True)
app.add_config_value('autodoc_typehints', "signature", True,
ENUM("signature", "description", "none"))
app.add_config_value('autodoc_typehints_description_target', 'all', True,
ENUM('all', 'documented'))
app.add_config_value('autodoc_type_aliases', {}, True)
app.add_config_value('autodoc_warningiserror', True, True)
app.add_config_value('autodoc_inherit_docstrings', True, True)
app.add_event('autodoc-before-process-signature')
app.add_event('autodoc-process-docstring')
app.add_event('autodoc-process-signature')
app.add_event('autodoc-skip-member')
app.connect('config-inited', migrate_autodoc_member_order, priority=800)
app.setup_extension('sphinx.ext.autodoc.preserve_defaults')
app.setup_extension('sphinx.ext.autodoc.type_comment')
app.setup_extension('sphinx.ext.autodoc.typehints')
return {'version': sphinx.__display_version__, 'parallel_read_safe': True}