diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 17:25:40 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 17:25:40 +0000 |
commit | cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a (patch) | |
tree | 18dcde1a8d1f5570a77cd0c361de3b490d02c789 /sphinx/ext/autodoc/__init__.py | |
parent | Initial commit. (diff) | |
download | sphinx-cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a.tar.xz sphinx-cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a.zip |
Adding upstream version 7.2.6.upstream/7.2.6
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'sphinx/ext/autodoc/__init__.py')
-rw-r--r-- | sphinx/ext/autodoc/__init__.py | 2850 |
1 files changed, 2850 insertions, 0 deletions
diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py new file mode 100644 index 0000000..8d68f72 --- /dev/null +++ b/sphinx/ext/autodoc/__init__.py @@ -0,0 +1,2850 @@ +"""Extension to create automatic documentation from code docstrings. + +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. +""" + +from __future__ import annotations + +import re +import sys +import warnings +from inspect import Parameter, Signature +from typing import TYPE_CHECKING, Any, Callable, TypeVar + +from docutils.statemachine import StringList + +import sphinx +from sphinx.config import ENUM, Config +from sphinx.deprecation import RemovedInSphinx80Warning +from sphinx.ext.autodoc.importer import get_class_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, stringify_annotation + +if TYPE_CHECKING: + from collections.abc import Iterator, Sequence + from types import ModuleType + + from sphinx.application import Sphinx + from sphinx.environment import BuildEnvironment + 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 + (?: \[\s*(.*)\s*])? # optional: type parameters list + (?: \((.*)\) # 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) -> 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 exclude_members_option(arg: Any) -> 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) -> set[str]: + """Used to convert the :inherited-members: option to auto directives.""" + if arg in (None, True): + return {'object'} + elif arg: + return {x.strip() for x in arg.split(',')} + else: + return set() + + +def member_order_option(arg: Any) -> str | None: + """Used to convert the :member-order: 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) -> str | None: + """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_members_option(options: dict) -> None: + """Merge :private-members: and :special-members: options 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 = 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 = 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: + """A member of object. + + This is used for the result of `Documenter.get_module_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 __init__(self, name: str, obj: Any, *, docstring: str | None = None, + class_: Any = None, skipped: bool = False) -> None: + self.__name__ = name + self.object = obj + self.docstring = docstring + self.skipped = skipped + self.class_ = class_ + + def __getitem__(self, index): + warnings.warn('The tuple interface of ObjectMember is deprecated. ' + 'Use (obj.__name__, obj.object) instead.', + RemovedInSphinx80Warning, stacklevel=2) + return (self.__name__, self.object)[index] + + +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 = True + + option_spec: OptionSpec = { + 'no-index': bool_option, + '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.""" + msg = 'must be implemented in subclasses' + raise NotImplementedError(msg) + + 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 = '' + self.module: ModuleType | None = None + self.objpath: list[str] = [] + self.fullname = '' + # extra signature items (arguments and return annotation, + # also set after resolve_name succeeds) + self.args: str | None = None + self.retann: str = '' + # the object to document (set after import_object succeeds) + self.object: Any = None + self.object_name = '' + # 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 = 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 | None, parents: Any, path: str, base: str, + ) -> tuple[str | None, 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. + """ + msg = 'must be implemented in subclasses' + raise NotImplementedError(msg) + + 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 + matched = py_ext_sig_re.match(self.name) + if matched is None: + logger.warning(__('invalid signature for auto%s (%r)') % (self.objtype, self.name), + type='autodoc') + return False + explicit_modname, path, base, tp_list, args, retann = matched.groups() + + # 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): + modname, self.objpath = self.resolve_name(modname, parents, path, base) + + if not modname: + return False + + self.modname = modname + 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 + 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 '' + + 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 = f'.. {domain}:{directive}:: ' + for i, sig_line in enumerate(sig.split("\n")): + self.add_line(f'{prefix}{name}{sig_line}', + sourcename) + if i == 0: + prefix = " " * len(prefix) + + if self.options.no_index or self.options.noindex: + self.add_line(' :no-index:', 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) -> list[list[str]] | None: + """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. + """ + 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, 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 = f'{self.object.__module__}.{self.object.__qualname__}' + else: + fullname = self.fullname + + if self.analyzer: + return f'{self.analyzer.srcname}:docstring of {fullname}' + else: + return 'docstring of %s' % fullname + + def add_content(self, more_content: StringList | None) -> None: + """Add content from docstrings, attribute documentation and user.""" + docstring = True + + # 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: + docstring = False + # make a copy of docstring for attributes to avoid cache + # the change of autodoc-process-docstring event. + attribute_docstrings = [list(attr_docs[key])] + + for i, line in enumerate(self.process_doc(attribute_docstrings)): + self.add_line(line, sourcename, i) + + # add content from docstrings + if 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, list[ObjectMember]]: + """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). + """ + msg = 'must be implemented in subclasses' + raise NotImplementedError(msg) + + def filter_members(self, members: list[ObjectMember], 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: + inherited_members = self.options.inherited_members or set() + + if inspect.isclass(self.object): + for cls in self.object.__mro__: + if cls.__name__ in inherited_members and cls != self.object: + # given member is a member of specified *super class* + return True + if name in cls.__dict__: + return False + if name in self.get_attr(cls, '__annotations__', {}): + return False + if 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 = obj.__name__ + member = obj.object + except AttributeError: + if isinstance(obj, ObjectMember): + raise + # To be removed, retained for compatibility. + # See https://github.com/sphinx-doc/sphinx/issues/11631 + membername, member = obj + warnings.warn( + 'Returning tuples of (name, object) as ' + 'the second return value from get_object_members() is deprecated. ' + 'Return ObjectMember(name, object) instances instead.', + RemovedInSphinx80Warning, stacklevel=2, + ) + + # if isattr is True, the member is documented as an attribute + isattr = member is INSTANCEATTR or (namespace, membername) in attr_docs + + try: + 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__': # NoQA: SIM114 + 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: # NoQA: SIM114 + 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 = f'{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': + # By default, member discovery order matches source order, + # as dicts are insertion-ordered from Python 3.7. + 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: # alphabetical + documenters.sort(key=lambda e: e[0].name) + + return documenters + + def generate( + self, + more_content: StringList | None = None, + real_modname: str | None = 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 module___file__ := getattr(self.module, '__file__', ''): + self.directive.record_dependencies.add(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 = '' + _extra_indent = ' ' + + option_spec: OptionSpec = { + 'members': members_option, 'undoc-members': bool_option, + 'no-index': 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, + 'no-value': bool_option, + 'noindex': bool_option, + } + + def __init__(self, *args: Any) -> None: + super().__init__(*args) + merge_members_option(self.options) + self.__all__: Sequence[str] | None = None + + def add_content(self, more_content: StringList | None) -> None: + old_indent = self.indent + self.indent += self._extra_indent + super().add_content(None) + self.indent = old_indent + if more_content: + for line, src in zip(more_content.data, more_content.items): + self.add_line(line, src[0], src[1]) + + @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 | None, parents: Any, path: str, base: str, + ) -> tuple[str | None, 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, list[ObjectMember]]: + 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__: + assert self.__all__ is not None + module_all = self.__all__ + module_all_set = set(module_all) + module_all_len = len(module_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 name in module_all_set: + return module_all.index(name) + else: + return module_all_len + 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 | None, parents: Any, path: str, base: str, + ) -> tuple[str | None, list[str]]: + if modname is not None: + return modname, parents + [base] + if path: + modname = path.rstrip('.') + return modname, parents + [base] + + # 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 | None, parents: Any, path: str, base: str, + ) -> tuple[str | None, list[str]]: + if modname is not None: + return modname, parents + [base] + + if path: + mod_cls = path.rstrip('.') + else: + # 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, [] + mod_cls = mod_cls_ + 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 = None + _signatures: list[str] = [] + + def _find_signature(self) -> tuple[str | None, str | None] | None: + # candidates of the object name + valid_names = [self.objpath[-1]] # type: ignore[attr-defined] + 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, tp_list, 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 + directive = self.directive # type: ignore[attr-defined] + tab_width = directive.state.document.settings.tab_width + self._new_docstrings[i] = prepare_docstring('\n'.join(doclines[j + 1:]), + tab_width) + + if result is None: + # first signature + result = args, retann + else: + # subsequent signatures + self._signatures.append(f"({args}) -> {retann}") + + if result is not None: + # finish the loop when signature found + break + + return result + + def get_doc(self) -> list[list[str]] | None: + if self._new_docstrings is not None: + return self._new_docstrings + return super().get_doc() # type: ignore[misc] + + def format_signature(self, **kwargs: Any) -> str: + self.args: str | None + if (self.args is None + and self.config.autodoc_docstring_signature): # type: ignore[attr-defined] + # 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[misc] + 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[attr-defined] + ): + # 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[misc] + """ + 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 '' + 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 = [''] + sigs.append(documenter.format_signature()) + if overloaded and self.analyzer is not None: + actual = inspect.signature(self.object, + type_aliases=self.config.autodoc_type_aliases) + __globals__ = safe_getattr(self.object, '__globals__', {}) + for overload in self.analyzer.overloads['.'.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) -> Callable | None: + """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( # type: ignore[attr-defined] + parameters=params) + return dummy + except (AttributeError, TypeError): + # failed to update signature (ex. built-in or extension types) + return None + + return func + + +class DecoratorDocumenter(FunctionDocumenter): + """ + Specialized Documenter subclass for decorator functions. + """ + objtype = 'decorator' + + # must be lower than FunctionDocumenter + priority = -1 + + def format_args(self, **kwargs: Any) -> str: + args = super().format_args(**kwargs) + if ',' in args: + return args + else: + return '' + + +# 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[misc] + """ + Specialized Documenter subclass for classes. + """ + objtype = 'class' + member_order = 20 + option_spec: OptionSpec = { + 'members': members_option, 'undoc-members': bool_option, + 'no-index': 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, + 'noindex': bool_option, + } + + # Must be higher than FunctionDocumenter, ClassDocumenter, and + # AttributeDocumenter as NewType can be an attribute and is a class + # after Python 3.10. Before 3.10 it is a kind of function object + priority = 15 + + _signature_class: Any = None + _signature_method_name: str = '' + + 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) or ( + isattr and (inspect.isNewType(member) or isinstance(member, TypeVar))) + + 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 + if inspect.isNewType(self.object) or isinstance(self.object, TypeVar): + modname = getattr(self.object, '__module__', self.modname) + if modname != self.modname and self.modname.startswith(modname): + bases = self.modname[len(modname):].strip('.').split('.') + self.objpath = bases + self.objpath + self.modname = modname + return ret + + def _get_signature(self) -> tuple[Any | None, str | None, Signature | None]: + if inspect.isNewType(self.object) or isinstance(self.object, TypeVar): + # Suppress signature + return None, None, None + + 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 f"{call.__module__}.{call.__qualname__}" 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 f"{new.__module__}.{new.__qualname__}" 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, _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 '' + self._signature_method_name = _signature_method_name or '' + + if sig is None: + return '' + + return stringify_signature(sig, show_return_annotation=False, **kwargs) + + def _find_signature(self) -> tuple[str | None, str | None] | None: + result = super()._find_signature() + if result is not None: + # Strip a return value from signature of constructor in docstring (first entry) + result = (result[0], None) + + for i, sig in enumerate(self._signatures): + if sig.endswith(' -> None'): + # Strip a return value from signatures of constructor in docstring (subsequent + # entries) + self._signatures[i] = sig[:-8] + + return result + + 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) -> str | None: + __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 inspect.isNewType(self.object) or isinstance(self.object, TypeVar): + return + + 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 not inspect.isNewType(self.object) + 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 <https://peps.python.org/pep-0560/> + 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, list[ObjectMember]]: + members = get_class_members(self.object, self.objpath, self.get_attr, + self.config.autodoc_inherit_docstrings) + if not want_all: + if not self.options.members: + return False, [] + # specific members given + selected = [] + for name in self.options.members: + 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) -> list[list[str]] | None: + if isinstance(self.object, TypeVar): + if self.object.__doc__ == TypeVar.__doc__: + return [] + if sys.version_info[:2] < (3, 10): + if inspect.isNewType(self.object) or isinstance(self.object, TypeVar): + parts = self.modname.strip('.').split('.') + orig_objpath = self.objpath + for i in range(len(parts)): + new_modname = '.'.join(parts[:len(parts) - i]) + new_objpath = parts[len(parts) - i:] + orig_objpath + try: + analyzer = ModuleAnalyzer.for_module(new_modname) + analyzer.analyze() + key = ('', new_objpath[-1]) + comment = list(analyzer.attr_docs.get(key, [])) + if comment: + self.objpath = new_objpath + self.modname = new_modname + return [comment] + except PycodeError: + pass + if self.doc_as_attr: + # Don't show the docstring of the class when it is an alias. + if self.get_variable_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, tab_width) for docstring in docstrings] + + def get_variable_comment(self) -> list[str] | None: + 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: StringList | None) -> 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 = StringList([_('alias of %s') % supertype, ''], source='') + if isinstance(self.object, TypeVar): + attrs = [repr(self.object.__name__)] + for constraint in self.object.__constraints__: + if self.config.autodoc_typehints_format == "short": + attrs.append(stringify_annotation(constraint, "smart")) + else: + attrs.append(stringify_annotation(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 = StringList( + [_('alias of TypeVar(%s)') % ", ".join(attrs), ''], + source='', + ) + if self.doc_as_attr and self.modname != self.get_real_modname(): + try: + # override analyzer to obtain doccomment around its definition. + self.analyzer = ModuleAnalyzer.for_module(self.modname) + self.analyzer.analyze() + except PycodeError: + pass + + 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: StringList | None = None, + real_modname: str | None = 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 = ClassDocumenter.priority + 5 + + @classmethod + def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any, + ) -> bool: + try: + return isinstance(member, type) and issubclass(member, BaseException) + except TypeError as exc: + # It's possible for a member to be considered a type, but fail + # issubclass checks due to not being a class. For example: + # https://github.com/sphinx-doc/sphinx/issues/11654#issuecomment-1696790436 + msg = ( + f'{cls.__name__} failed to discern if member {member} with' + f' membername {membername} is a BaseException subclass.' + ) + raise ValueError(msg) from exc + + +class DataDocumenterMixinBase: + # define types of instance variables + config: Config + env: BuildEnvironment + modname: str + parent: Any + object: Any + objpath: list[str] + + 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 example with TypeVar variance.""" + 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 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[misc] + 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 + 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) -> list[list[str]] | None: + if self.object is UNINITIALIZED_ATTR: + return [] + else: + return super().get_doc() # type: ignore[misc] + + +class DataDocumenter(GenericAliasMixin, + 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() or [] + 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: + if self.config.autodoc_typehints_format == "short": + objrepr = stringify_annotation(annotations.get(self.objpath[-1]), + "smart") + else: + objrepr = stringify_annotation(annotations.get(self.objpath[-1]), + "fully-qualified-except-typing") + 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) -> list[str] | None: + 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) -> list[list[str]] | None: + # Check the variable has a docstring-comment + comment = self.get_module_comment(self.objpath[-1]) + if comment: + return [comment] + else: + return super().get_doc() + + def add_content(self, more_content: StringList | None) -> 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) + + +class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: ignore[misc] + """ + 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 '' + 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 = [''] + sigs.append(documenter.format_signature()) + if overloaded and self.analyzer is not None: + 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['.'.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) -> Callable | None: + """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( # type: ignore[attr-defined] + parameters=params) + return dummy + except (AttributeError, TypeError): + # failed to update signature (ex. built-in or extension types) + return None + + return func + + def get_doc(self) -> list[list[str]] | None: + 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[misc] + 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) -> list[list[str]] | None: + 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() # type: ignore[misc] + + +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: + if parent___slots__ := inspect.getslots(self.parent): + return self.objpath[-1] in parent___slots__ + else: + return False + except (ValueError, TypeError): + return False + + def import_object(self, raiseerror: bool = False) -> bool: + ret = super().import_object(raiseerror) # type: ignore[misc] + 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) -> list[list[str]] | None: + if self.object is SLOTSATTR: + try: + parent___slots__ = inspect.getslots(self.parent) + if parent___slots__ and parent___slots__.get(self.objpath[-1]): + docstring = prepare_docstring(parent___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() # type: ignore[misc] + + +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[attr-defined] + return True + if self.is_runtime_instance_attribute_not_commented(parent): + return True + 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 False + + 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[misc] + 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[attr-defined] + 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 + 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) -> list[list[str]] | None: + 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() # type: ignore[misc] + + +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) + return self.objpath[-1] in annotations + + 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[misc] + except ImportError as exc: + try: + ret = import_object(self.modname, self.objpath[:-1], 'class', + attrgetter=self.get_attr, # type: ignore[attr-defined] + 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 + 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) -> list[list[str]] | None: + if self.object is UNINITIALIZED_ATTR: + return None + return super().get_doc() # type: ignore[misc] + + +class AttributeDocumenter(GenericAliasMixin, SlotsMixin, # type: ignore[misc] + 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 + if inspect.isattributedescriptor(member): + return True + if not inspect.isroutine(member) and not isinstance(member, type): + return True + return False + + def document_members(self, all_members: bool = False) -> None: + pass + + 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: + if self.config.autodoc_typehints_format == "short": + objrepr = stringify_annotation(annotations.get(self.objpath[-1]), + "smart") + else: + objrepr = stringify_annotation(annotations.get(self.objpath[-1]), + "fully-qualified-except-typing") + 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) -> list[str] | None: + 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) -> list[list[str]] | None: + # 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[attr-defined] + return super().get_doc() + finally: + self.config.autodoc_inherit_docstrings = orig # type: ignore[attr-defined] + + def add_content(self, more_content: StringList | None) -> 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) + + +class PropertyDocumenter(DocstringStripSignatureMixin, # type: ignore[misc] + ClassLevelDocumenter): + """ + 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 format_args(self, **kwargs: Any) -> str: + func = self._get_property_getter() + if func is None: + return '' + + # update the annotations of the property getter + self.env.app.emit('autodoc-before-process-signature', func, False) + # correctly format the arguments for a property + return super().format_args(**kwargs) + + 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) + + func = self._get_property_getter() + if func is None or self.config.autodoc_typehints == 'none': + return + + try: + signature = inspect.signature(func, + type_aliases=self.config.autodoc_type_aliases) + if signature.return_annotation is not Parameter.empty: + if self.config.autodoc_typehints_format == "short": + objrepr = stringify_annotation(signature.return_annotation, "smart") + else: + objrepr = stringify_annotation(signature.return_annotation, + "fully-qualified-except-typing") + self.add_line(' :type: ' + objrepr, sourcename) + except TypeError as exc: + logger.warning(__("Failed to get a function signature for %s: %s"), + self.fullname, exc) + pass + except ValueError: + pass + + def _get_property_getter(self): + if safe_getattr(self.object, 'fget', None): # property + return self.object.fget + if safe_getattr(self.object, 'func', None): # cached_property + return self.object.func + return None + + +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 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(FunctionDocumenter) + app.add_autodocumenter(DecoratorDocumenter) + app.add_autodocumenter(MethodDocumenter) + app.add_autodocumenter(AttributeDocumenter) + app.add_autodocumenter(PropertyDocumenter) + + app.add_config_value('autoclass_content', 'class', True, ENUM('both', 'class', 'init')) + app.add_config_value('autodoc_member_order', 'alphabetical', True, + ENUM('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', 'documented_params')) + app.add_config_value('autodoc_type_aliases', {}, True) + app.add_config_value('autodoc_typehints_format', "short", '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.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} |