""" 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-2022 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 prepare_docstring, separate_metadata 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 def append(self, item: Any) -> None: pass # nothing 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 copy(self) -> "Options": return Options(super().copy()) 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: matched = py_ext_sig_re.match(self.name) explicit_modname, path, base, args, retann = matched.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, 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 (inspect.safe_getattr(self.object, '__module__', None) and inspect.safe_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: try: membername, member = obj # if isattr is True, the member is documented as an attribute if member is INSTANCEATTR: isattr = True elif (namespace, membername) in attr_docs: isattr = True else: isattr = False doc = getdoc(member, self.get_attr, self.config.autodoc_inherit_docstrings, self.object, membername) 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 doc, metadata = separate_metadata(doc) has_doc = bool(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) and (namespace, membername) not in attr_docs: # 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 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 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 %s.%s (%r) to be documented, ' 'the following exception was raised:\n%s'), self.name, membername, 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, document 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 docstrings: List[str] = sum(self.get_doc() or [], []) if ismock(self.object) and not docstrings: logger.warning(__('A mocked object is detected: %r'), self.name, type='autodoc') # 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) if self.config.autodoc_typehints_format == "short": kwargs.setdefault('unqualified_typehints', True) 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) or inspect.isasyncgenfunction(self.object): self.add_line(' :async:', sourcename) def format_signature(self, **kwargs: Any) -> str: if self.config.autodoc_typehints_format == "short": kwargs.setdefault('unqualified_typehints', True) sigs = [] if (self.analyzer and '.'.join(self.objpath) in self.analyzer.overloads and self.config.autodoc_typehints != 'none'): # 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-through. _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) if self.config.autodoc_class_signature == 'separated': self.options = self.options.copy() # show __init__() method if self.options.special_members is None: self.options['special-members'] = ['__new__', '__init__'] else: self.options.special_members.append('__new__') self.options.special_members.append('__init__') 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) if self.config.autodoc_typehints_format == "short": kwargs.setdefault('unqualified_typehints', True) 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 '' if self.config.autodoc_class_signature == 'separated': # do not show signatures return '' if self.config.autodoc_typehints_format == "short": kwargs.setdefault('unqualified_typehints', True) sig = super().format_signature() sigs = [] overloads = self.get_overloaded_signatures() if overloads and self.config.autodoc_typehints != 'none': # 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 overridden. 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 '' 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: if inspect.getorigbases(self.object): # A subclass of generic types # refs: PEP-560 bases = list(self.object.__orig_bases__) elif hasattr(self.object, '__bases__') and len(self.object.__bases__): # A normal class bases = list(self.object.__bases__) else: bases = [] self.env.events.emit('autodoc-process-bases', self.fullname, self.object, self.options, bases) if self.config.autodoc_typehints_format == "short": base_classes = [restify(cls, "smart") for cls in bases] else: base_classes = [restify(cls) for cls in bases] sourcename = self.get_sourcename() self.add_line('', sourcename) self.add_line(' ' + _('Bases: %s') % ', '.join(base_classes), 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. comment = self.get_variable_comment() if comment: return [] else: 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 = getdoc(self.object, self.get_attr) 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.object, '__init__') # 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.object, '__new__') # 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 get_variable_comment(self) -> Optional[List[str]]: try: key = ('', '.'.join(self.objpath)) if self.doc_as_attr: analyzer = ModuleAnalyzer.for_module(self.modname) else: analyzer = ModuleAnalyzer.for_module(self.get_real_modname()) analyzer.analyze() return list(analyzer.attr_docs.get(key, [])) except PycodeError: return None def add_content(self, more_content: Optional[StringList], no_docstring: bool = False ) -> None: if self.doc_as_attr and self.modname != self.get_real_modname(): # override analyzer to obtain doccomment around its definition. self.analyzer = ModuleAnalyzer.for_module(self.modname) self.analyzer.analyze() if self.doc_as_attr and not self.get_variable_comment(): try: if self.config.autodoc_typehints_format == "short": alias = restify(self.object, "smart") else: alias = restify(self.object) more_content = StringList([_('alias of %s') % alias], 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): if self.config.autodoc_typehints_format == "short": alias = restify(self.object, "smart") else: alias = restify(self.object) more_content.append(_('alias of %s') % alias, '') 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): if self.config.autodoc_typehints_format == "short": supertype = restify(self.object.__supertype__, "smart") else: 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__: if self.config.autodoc_typehints_format == "short": bound = restify(self.object.__bound__, "smart") else: bound = restify(self.object.__bound__) attrs.append(r"bound=\ " + 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() docstring, metadata = separate_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: if self.config.autodoc_typehints != 'none': # 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() or ismock(self.object)): 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: real_modname = self.get_attr(self.parent or self.object, '__module__', None) return real_modname 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) if self.config.autodoc_typehints_format == "short": kwargs.setdefault('unqualified_typehints', True) 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) or inspect.isasyncgenfunction(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: if self.config.autodoc_typehints_format == "short": kwargs.setdefault('unqualified_typehints', True) sigs = [] if (self.analyzer and '.'.join(self.objpath) in self.analyzer.overloads and self.config.autodoc_typehints != 'none'): # 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 def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]: if self._new_docstrings is not None: # docstring already returned previously, then modified by # `DocstringSignatureMixin`. Just return the previously-computed # result, so that we don't lose the processing done by # `DocstringSignatureMixin`. return self._new_docstrings if self.objpath[-1] == '__init__': docstring = getdoc(self.object, self.get_attr, self.config.autodoc_inherit_docstrings, self.parent, self.object_name) if (docstring is not None and (docstring == object.__init__.__doc__ or # for pypy docstring.strip() == object.__init__.__doc__)): # for !pypy docstring = None if docstring: tab_width = self.directive.state.document.settings.tab_width return [prepare_docstring(docstring, tabsize=tab_width)] else: return [] elif self.objpath[-1] == '__new__': docstring = getdoc(self.object, self.get_attr, self.config.autodoc_inherit_docstrings, self.parent, self.object_name) if (docstring is not None and (docstring == object.__new__.__doc__ or # for pypy docstring.strip() == object.__new__.__doc__)): # for !pypy docstring = None if docstring: tab_width = self.directive.state.document.settings.tab_width return [prepare_docstring(docstring, tabsize=tab_width)] else: return [] else: return super().get_doc() 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_value_header(self) -> bool: if self.object is SLOTSATTR: return True else: return super().should_suppress_value_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 @property def _datadescriptor(self) -> bool: warnings.warn('AttributeDocumenter._datadescriptor() is deprecated.', RemovedInSphinx60Warning) if self.object is SLOTSATTR: return True else: return False 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 elif self.is_runtime_instance_attribute_not_commented(parent): return True else: return False def is_runtime_instance_attribute_not_commented(self, parent: Any) -> bool: """Check the subject is an attribute defined in __init__() without comment.""" 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 = '.'.join([qualname, self.objpath[-1]]) if key in analyzer.tagorder: return True except (AttributeError, PycodeError): pass return None def import_object(self, raiseerror: bool = False) -> bool: """Check the existence of runtime instance attribute after failing 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()) def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]: if (self.object is self.RUNTIME_INSTANCE_ATTRIBUTE and self.is_runtime_instance_attribute_not_commented(self.parent)): return None else: return super().get_doc(ignore) # type: ignore 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 isinstance(parent, ModuleDocumenter): return False elif inspect.isattributedescriptor(member): return True elif 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: real_modname = self.get_attr(self.parent or self.object, '__module__', None) return real_modname 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: docstring, metadata = separate_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: if self.config.autodoc_typehints != 'none': # 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() or ismock(self.object)): 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: if isinstance(parent, ClassDocumenter): if inspect.isproperty(member): return True else: __dict__ = safe_getattr(parent.object, '__dict__', {}) obj = __dict__.get(membername) return isinstance(obj, classmethod) and inspect.isproperty(obj.__func__) else: return False def import_object(self, raiseerror: bool = False) -> bool: """Check the exisitence of uninitialized instance attribute when failed to import the attribute.""" ret = super().import_object(raiseerror) if ret and not inspect.isproperty(self.object): __dict__ = safe_getattr(self.parent, '__dict__', {}) obj = __dict__.get(self.objpath[-1]) if isinstance(obj, classmethod) and inspect.isproperty(obj.__func__): self.object = obj.__func__ self.isclassmethod = True return True else: return False self.isclassmethod = False return ret def document_members(self, all_members: bool = False) -> None: pass def get_real_modname(self) -> str: real_modname = self.get_attr(self.parent or self.object, '__module__', None) return real_modname 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 self.isclassmethod: self.add_line(' :classmethod:', sourcename) if safe_getattr(self.object, 'fget', None): # property func = self.object.fget elif safe_getattr(self.object, 'func', None): # cached_property func = self.object.func else: func = None if func and self.config.autodoc_typehints != 'none': try: signature = inspect.signature(func, 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: return None 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_class_signature', 'mixed', True, ENUM('mixed', 'separated')) 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", "both")) 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_typehints_format', "fully-qualified", 'env', ENUM("fully-qualified", "short")) 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.add_event('autodoc-process-bases') 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}