diff --git a/CHANGES.rst b/CHANGES.rst index fb6b02ff4f5..378f15c0e94 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,12 @@ Incompatible changes * #13639: :py:meth:`!SphinxComponentRegistry.create_source_parser` no longer has an *app* parameter, instead taking *config* and *env*. Patch by Adam Turner. +* #13751, #14089: :mod:`sphinx.ext.autodoc` has been substantially rewritten, + and there may be some incompatible changes in edge cases, especially when + extensions interact with autodoc internals. + The :confval:`autodoc_use_legacy_class_based` option has been added to + use the legacy (pre-8.3) implementation of autodoc. + Patches by Adam Turner. Deprecated ---------- diff --git a/doc/development/tutorials/examples/autodoc_intenum.py b/doc/development/tutorials/examples/autodoc_intenum.py index bb36ea0e6bf..ebddcbbd4d9 100644 --- a/doc/development/tutorials/examples/autodoc_intenum.py +++ b/doc/development/tutorials/examples/autodoc_intenum.py @@ -3,8 +3,7 @@ from enum import IntEnum from typing import TYPE_CHECKING -from sphinx.ext.autodoc import ClassDocumenter, bool_option -from sphinx.ext.autodoc._generate import _docstring_source_name +from sphinx.ext.autodoc import ClassDocumenter, Documenter, bool_option if TYPE_CHECKING: from typing import Any @@ -12,14 +11,13 @@ from docutils.statemachine import StringList from sphinx.application import Sphinx - from sphinx.ext.autodoc import Documenter from sphinx.util.typing import ExtensionMetadata class IntEnumDocumenter(ClassDocumenter): objtype = 'intenum' directivetype = ClassDocumenter.objtype - priority = 25 + priority = 10 + ClassDocumenter.priority option_spec = dict(ClassDocumenter.option_spec) option_spec['hex'] = bool_option @@ -32,33 +30,28 @@ def can_document_member( except TypeError: return False - def add_line(self, line: str, source: str = '', *lineno: int, indent: str) -> None: - """Append one line of generated reST to the output.""" - analyzer_source = '' if self.analyzer is None else self.analyzer.srcname - source_name = _docstring_source_name(props=self.props, source=analyzer_source) - if line.strip(): # not a blank line - self.result.append(indent + line, source_name, *lineno) - else: - self.result.append('', source_name, *lineno) + def add_directive_header(self, sig: str) -> None: + super().add_directive_header(sig) + self.add_line(' :final:', self.get_sourcename()) - def add_directive_header(self, *, indent: str) -> None: - super().add_directive_header(indent=indent) - self.add_line(' :final:', indent=indent) + def add_content( + self, + more_content: StringList | None, + ) -> None: + super().add_content(more_content) - def add_content(self, more_content: StringList | None, *, indent: str) -> None: - super().add_content(more_content, indent=indent) - - enum_object: IntEnum = self.props._obj + source_name = self.get_sourcename() + enum_object: IntEnum = self.object use_hex = self.options.hex - self.add_line('', indent=indent) + self.add_line('', source_name) for the_member_name, enum_member in enum_object.__members__.items(): # type: ignore[attr-defined] the_member_value = enum_member.value if use_hex: the_member_value = hex(the_member_value) - self.add_line(f'**{the_member_name}**: {the_member_value}', indent=indent) - self.add_line('', indent=indent) + self.add_line(f'**{the_member_name}**: {the_member_value}', source_name) + self.add_line('', source_name) def setup(app: Sphinx) -> ExtensionMetadata: diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index 925d1450acf..1aa87c8e500 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -1003,6 +1003,26 @@ Configuration There are also config values that you can set: +.. confval:: autodoc_use_legacy_class_based + :type: :code-py:`bool` + :default: :code-py:`False` + + If true, autodoc will use the legacy class-based implementation. + This is the behaviour prior to Sphinx 8.3. + It is based on the ``Documenter`` class hierarchy. + + This setting is provided for backwards compatibility if your documentation + or an extension you use uses or monkeypatches the legacy class-based API + in Python code. + If this is the case, set ``autodoc_use_legacy_class_based = True`` + in your :file:`conf.py`. + Please also add a comment to `the tracking issue on GitHub + `__ so that the maintainers + are aware of your use case, for possible future improvements. + + .. note:: The legacy class-based implementation does not support + PEP 695 type aliases. + .. confval:: autoclass_content :type: :code-py:`str` :default: :code-py:`'class'` diff --git a/pyproject.toml b/pyproject.toml index bd3528ee594..3eab7483e02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -173,7 +173,7 @@ exclude = [ [tool.mypy] files = [ "doc/conf.py", -# "doc/development/tutorials/examples/autodoc_intenum.py", + "doc/development/tutorials/examples/autodoc_intenum.py", "doc/development/tutorials/examples/helloworld.py", "sphinx", "tests", diff --git a/sphinx/application.py b/sphinx/application.py index a1026ab2301..d70e9db999a 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -52,12 +52,12 @@ from sphinx.config import ENUM, _ConfigRebuild from sphinx.domains import Domain, Index from sphinx.environment.collectors import EnvironmentCollector - from sphinx.ext.autodoc._documenters import Documenter from sphinx.ext.autodoc._event_listeners import ( _AutodocProcessDocstringListener, _AutodocProcessSignatureListener, _AutodocSkipMemberListener, ) + from sphinx.ext.autodoc._legacy_class_based._documenters import Documenter from sphinx.ext.todo import todo_node from sphinx.extension import Extension from sphinx.registry import ( @@ -1627,9 +1627,9 @@ def add_autodocumenter(self, cls: type[Documenter], override: bool = False) -> N Add *override* keyword. """ logger.debug('[app] adding autodocumenter: %r', cls) - from sphinx.ext.autodoc._directive import AutodocDirective + from sphinx.ext.autodoc.directive import AutodocDirective - objtype = cls.objtype # type: ignore[attr-defined] + objtype = cls.objtype self.registry.add_documenter(objtype, cls) self.add_directive('auto' + objtype, AutodocDirective, override=override) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 6e2d58552ab..9a6985bc337 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -12,7 +12,9 @@ import sphinx from sphinx.config import ENUM from sphinx.ext.autodoc._directive import AutodocDirective -from sphinx.ext.autodoc._directive_options import ( +from sphinx.ext.autodoc._event_listeners import between, cut_lines +from sphinx.ext.autodoc._legacy_class_based._directive_options import ( + Options, annotation_option, bool_option, class_doc_from_option, @@ -23,16 +25,46 @@ members_option, merge_members_option, ) -from sphinx.ext.autodoc._dynamic._member_finder import ObjectMember, special_member_re -from sphinx.ext.autodoc._event_listeners import between, cut_lines -from sphinx.ext.autodoc._names import py_ext_sig_re -from sphinx.ext.autodoc._sentinels import ALL, EMPTY, SUPPRESS, UNINITIALIZED_ATTR -from sphinx.ext.autodoc._sentinels import INSTANCE_ATTR as INSTANCEATTR -from sphinx.ext.autodoc._sentinels import SLOTS_ATTR as SLOTSATTR +from sphinx.ext.autodoc._legacy_class_based._documenters import ( + AttributeDocumenter, + ClassDocumenter, + ClassLevelDocumenter, + DataDocumenter, + DataDocumenterMixinBase, + DecoratorDocumenter, + DocstringSignatureMixin, + DocstringStripSignatureMixin, + Documenter, + ExceptionDocumenter, + FunctionDocumenter, + GenericAliasMixin, + MethodDocumenter, + ModuleDocumenter, + ModuleLevelDocumenter, + NonDataDescriptorMixin, + ObjectMember, + PropertyDocumenter, + RuntimeInstanceAttributeMixin, + SlotsMixin, + UninitializedGlobalVariableMixin, + UninitializedInstanceAttributeMixin, + autodoc_attrgetter, + py_ext_sig_re, + special_member_re, +) +from sphinx.ext.autodoc._legacy_class_based._sentinels import ( + ALL, + EMPTY, + INSTANCEATTR, + SLOTSATTR, + SUPPRESS, + UNINITIALIZED_ATTR, +) from sphinx.ext.autodoc.typehints import _merge_typehints if TYPE_CHECKING: from sphinx.application import Sphinx + from sphinx.config import Config from sphinx.ext.autodoc._property_types import _AutodocObjType from sphinx.util.typing import ExtensionMetadata @@ -40,10 +72,20 @@ # Useful event listener factories for autodoc-process-docstring 'cut_lines', 'between', + # Documenters + 'AttributeDocumenter', + 'ClassDocumenter', + 'DataDocumenter', + 'DecoratorDocumenter', + 'ExceptionDocumenter', + 'FunctionDocumenter', + 'MethodDocumenter', + 'ModuleDocumenter', + 'PropertyDocumenter', # This class is only used in ``sphinx.ext.autodoc.directive``, # but we export it here for compatibility. # See: https://github.com/sphinx-doc/sphinx/issues/4538 - # 'Options', + 'Options', # Option spec functions. # Exported for compatibility. 'annotation_option', @@ -68,26 +110,23 @@ 'ObjectMember', 'py_ext_sig_re', 'special_member_re', + 'ModuleLevelDocumenter', + 'ClassLevelDocumenter', + 'DocstringSignatureMixin', + 'DocstringStripSignatureMixin', + 'DataDocumenterMixinBase', + 'GenericAliasMixin', + 'UninitializedGlobalVariableMixin', + 'NonDataDescriptorMixin', + 'SlotsMixin', + 'RuntimeInstanceAttributeMixin', + 'UninitializedInstanceAttributeMixin', + 'autodoc_attrgetter', + 'Documenter', ) def setup(app: Sphinx) -> ExtensionMetadata: - obj_type: _AutodocObjType - for obj_type in ( - 'module', - 'class', - 'exception', - 'function', - 'decorator', - 'method', - 'property', - 'attribute', - 'data', - 'type', - ): - # register the automodule, autoclass, etc. directives - app.add_directive(f'auto{obj_type}', AutodocDirective) - app.add_config_value( 'autoclass_content', 'class', @@ -142,6 +181,9 @@ def setup(app: Sphinx) -> ExtensionMetadata: app.add_config_value( 'autodoc_use_type_comments', True, 'env', types=frozenset({bool}) ) + app.add_config_value( + 'autodoc_use_legacy_class_based', False, 'env', types=frozenset({bool}) + ) app.add_event('autodoc-before-process-signature') app.add_event('autodoc-process-docstring') @@ -151,7 +193,50 @@ def setup(app: Sphinx) -> ExtensionMetadata: app.connect('object-description-transform', _merge_typehints) + app.connect('config-inited', _register_directives) + return { 'version': sphinx.__display_version__, 'parallel_read_safe': True, } + + +def _register_directives(app: Sphinx, config: Config) -> None: + if not config.autodoc_use_legacy_class_based: + obj_type: _AutodocObjType + for obj_type in ( + 'module', + 'class', + 'exception', + 'function', + 'decorator', + 'method', + 'property', + 'attribute', + 'data', + 'type', + ): + # register the automodule, autoclass, etc. directives + app.add_directive(f'auto{obj_type}', AutodocDirective) + else: + from sphinx.ext.autodoc.preserve_defaults import update_defvalue + from sphinx.ext.autodoc.type_comment import ( + update_annotations_using_type_comments, + ) + from sphinx.ext.autodoc.typehints import record_typehints + + 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.connect('autodoc-before-process-signature', update_defvalue) + app.connect( + 'autodoc-before-process-signature', update_annotations_using_type_comments + ) + app.connect('autodoc-process-signature', record_typehints) diff --git a/sphinx/ext/autodoc/_documenters.py b/sphinx/ext/autodoc/_documenters.py deleted file mode 100644 index d60224ce075..00000000000 --- a/sphinx/ext/autodoc/_documenters.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - - -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. - """ diff --git a/sphinx/ext/autodoc/_dynamic/_preserve_defaults.py b/sphinx/ext/autodoc/_dynamic/_preserve_defaults.py index 7cf61f5db56..3f2157d8e12 100644 --- a/sphinx/ext/autodoc/_dynamic/_preserve_defaults.py +++ b/sphinx/ext/autodoc/_dynamic/_preserve_defaults.py @@ -20,7 +20,6 @@ if TYPE_CHECKING: from typing import Any - logger = logging.getLogger(__name__) _LAMBDA_NAME = (lambda: None).__name__ diff --git a/sphinx/ext/autodoc/_legacy_class_based/__init__.py b/sphinx/ext/autodoc/_legacy_class_based/__init__.py new file mode 100644 index 00000000000..664f631bdda --- /dev/null +++ b/sphinx/ext/autodoc/_legacy_class_based/__init__.py @@ -0,0 +1 @@ +"""The legacy (class-based) implementation of autodoc.""" diff --git a/sphinx/ext/autodoc/_legacy_class_based/_directive_options.py b/sphinx/ext/autodoc/_legacy_class_based/_directive_options.py new file mode 100644 index 00000000000..bc8a206cc17 --- /dev/null +++ b/sphinx/ext/autodoc/_legacy_class_based/_directive_options.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from typing import Any + +from sphinx.ext.autodoc._legacy_class_based._sentinels import ALL, EMPTY, SUPPRESS +from sphinx.locale import __ + + +def identity(x: Any) -> Any: + return x + + +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) + + +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[str, Any]) -> 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'): + other_members = options.get(key) + if other_members is not None and other_members is not ALL: + for member in other_members: + if member not in members: + members.append(member) + + +class Options(dict[str, Any]): # NoQA: FURB189 + """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 diff --git a/sphinx/ext/autodoc/_legacy_class_based/_documenters.py b/sphinx/ext/autodoc/_legacy_class_based/_documenters.py new file mode 100644 index 00000000000..24c5aa7ca73 --- /dev/null +++ b/sphinx/ext/autodoc/_legacy_class_based/_documenters.py @@ -0,0 +1,2929 @@ +from __future__ import annotations + +import functools +import operator +import re +import sys +from inspect import Parameter, Signature +from typing import TYPE_CHECKING, NewType, TypeVar + +from docutils.statemachine import StringList + +from sphinx.errors import PycodeError +from sphinx.ext.autodoc._dynamic._mock import ismock, mock, undecorate +from sphinx.ext.autodoc._legacy_class_based._directive_options import ( + annotation_option, + bool_option, + class_doc_from_option, + exclude_members_option, + identity, + inherited_members_option, + member_order_option, + members_option, + merge_members_option, +) +from sphinx.ext.autodoc._legacy_class_based._sentinels import ( + ALL, + INSTANCEATTR, + SLOTSATTR, + SUPPRESS, + UNINITIALIZED_ATTR, +) +from sphinx.ext.autodoc.importer import ( # type: ignore[attr-defined] + get_class_members, + import_module, + import_object, +) +from sphinx.locale import _, __ +from sphinx.pycode import ModuleAnalyzer +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 get_type_hints, restify, stringify_annotation + +if TYPE_CHECKING: + from collections.abc import Callable, Iterator, Sequence + from types import ModuleType + from typing import Any, ClassVar, Literal + + from sphinx.config import Config + from sphinx.environment import BuildEnvironment, _CurrentDocument + from sphinx.events import EventManager + from sphinx.ext.autodoc.directive import DocumenterBridge + from sphinx.registry import SphinxComponentRegistry + from sphinx.util.typing import OptionSpec, _RestifyMode + +logger = logging.getLogger('sphinx.ext.autodoc') + +#: 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 _get_render_mode( + typehints_format: Literal['fully-qualified', 'short'], +) -> _RestifyMode: + if typehints_format == 'short': + return 'smart' + return 'fully-qualified-except-typing' + + +class ObjectMember: + """A member of object. + + This is used for the result of `Documenter.get_module_members()` to + represent each member of the object. + """ + + __slots__ = '__name__', 'object', 'docstring', 'class_', 'skipped' + + __name__: str + object: Any + docstring: str | None + class_: Any + skipped: bool + + 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.class_ = class_ + self.skipped = skipped + + def __repr__(self) -> str: + return ( + f'ObjectMember(' + f'name={self.__name__!r}, ' + f'obj={self.object!r}, ' + f'docstring={self.docstring!r}, ' + f'class_={self.class_!r}, ' + f'skipped={self.skipped!r}' + f')' + ) + + +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: ClassVar = 'object' + #: indentation by which to indent the directive content + content_indent: ClassVar = ' ' + #: priority if multiple documenters return True from can_document_member + priority: ClassVar = 0 + #: order if autodoc_member_order is set to 'groupwise' + member_order: ClassVar = 0 + #: true if the generated content may contain titles + titles_allowed: ClassVar = True + + option_spec: ClassVar[OptionSpec] = { + 'no-index': bool_option, + 'no-index-entry': 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(obj, name, *defargs, registry=self.env._registry) + + @classmethod + def can_document_member( + cls: type[Documenter], 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._current_document: _CurrentDocument = directive.env.current_document + self._events: EventManager = directive.env.events + 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._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 = '.'.join((self.modname or '', *self.objpath)) + 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 + ) + 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) + return not modname or modname == self.modname + + 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 = f'({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._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.options.no_index_entry: + self.add_line(' :no-index-entry:', 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._events is not None: + # let extensions preprocess docstrings + self._events.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: + obj_module = inspect.safe_getattr(self.object, '__module__', None) + obj_qualname = inspect.safe_getattr(self.object, '__qualname__', None) + if obj_module and obj_qualname: + # 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, strict=True): + 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() + seen = set() + + if inspect.isclass(self.object): + for cls in self.object.__mro__: + if name in cls.__dict__: + seen.add(cls) + if ( + cls.__name__ in inherited_members + and cls != self.object + and any( + issubclass(potential_child, cls) for potential_child in seen + ) + ): + # 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: + membername = obj.__name__ + member = obj.object + + # 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._events is not None: + # let extensions preprocess docstrings + skip_user = self._events.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._current_document.autodoc_module = self.modname + if self.objpath: + self._current_document.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 + member_documenters: 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) + member_documenters.append((documenter, isattr)) + + member_order = self.options.member_order or self.config.autodoc_member_order + # We now try to import all objects before ordering them. This is to + # avoid possible circular imports if we were to import objects after + # their associated documenters have been sorted. + member_documenters = [ + (documenter, isattr) + for documenter, isattr in member_documenters + if documenter.parse_name() and documenter.import_object() + ] + member_documenters = self.sort_members(member_documenters, member_order) + + for documenter, isattr in member_documenters: + assert documenter.modname + # We can directly call ._generate() since the documenters + # already called parse_name() and import_object() before. + # + # Note that those two methods above do not emit events, so + # whatever objects we deduced should not have changed. + documenter._generate( + all_members=True, + real_modname=self.real_modname, + check_module=members_check_module and not isattr, + ) + + # reset current objects + self._current_document.autodoc_module = '' + self._current_document.autodoc_class = '' + + 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 + + self._generate(more_content, real_modname, check_module, all_members) + + def _generate( + self, + more_content: StringList | None = None, + real_modname: str | None = None, + check_module: bool = False, + all_members: bool = False, + ) -> None: + # 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] = functools.reduce( + operator.iadd, self.get_doc() or [], [] + ) + if ismock(self.object) and not docstrings: + logger.warning( + __('A mocked object is detected: %r'), + self.name, + type='autodoc', + subtype='mocked_object', + ) + + # 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: ClassVar[OptionSpec] = { + 'members': members_option, + 'undoc-members': bool_option, + 'no-index': bool_option, + 'no-index-entry': 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, strict=True): + self.add_line(line, src[0], src[1]) + + @classmethod + def can_document_member( + cls: type[Documenter], 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) + if self.options.no_index_entry: + self.add_line(' :no-index-entry:', 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._current_document.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._current_document.autodoc_class + # ... or from a class directive + if not mod_cls: + mod_cls = self.env.ref_context.get('py:class', '') + # ... if still falsy, there's no way to know + if not mod_cls: + return None, [] + modname, _sep, cls = mod_cls.rpartition('.') + parents = [cls] + # if the module name is still missing, get it like above + if not modname: + modname = self._current_document.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: type[Documenter], 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) + if self.config.python_display_short_literal_types: + kwargs.setdefault('short_literals', True) + + try: + self._events.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) + + is_coro = inspect.iscoroutinefunction(self.object) + is_acoro = inspect.isasyncgenfunction(self.object) + if is_coro or is_acoro: + self.add_line(' :async:', sourcename) + + def format_signature(self, **kwargs: Any) -> str: + if self.config.autodoc_typehints_format == 'short': + kwargs.setdefault('unqualified_typehints', True) + if self.config.python_display_short_literal_types: + kwargs.setdefault('short_literals', 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[..., Any], typ: type + ) -> Callable[..., Any] | 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(): # type: ignore[no-untyped-def] # NoQA: ANN202 + pass + + params = list(sig.parameters.values()) + if params[0].annotation is Parameter.empty: + params[0] = params[0].replace(annotation=typ) + try: + dummy.__signature__ = sig.replace(parameters=params) # type: ignore[attr-defined] + 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 = frozenset({ + 'enum.EnumType.__call__', +}) + + +# Types whose __new__ signature is a pass-through. +_CLASS_NEW_BLACKLIST = frozenset({ + 'typing.Generic.__new__', +}) + + +class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: ignore[misc] + """Specialized Documenter subclass for classes.""" + + objtype = 'class' + member_order = 20 + option_spec: ClassVar[OptionSpec] = { + 'members': members_option, + 'undoc-members': bool_option, + 'no-index': bool_option, + 'no-index-entry': 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. + 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: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any + ) -> bool: + return isinstance(member, type) or ( + isattr and isinstance(member, NewType | 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 isinstance(self.object, NewType | 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 isinstance(self.object, NewType | 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 if obj has a __signature__ attribute + if hasattr(self.object, '__signature__'): + object_sig = self.object.__signature__ + if isinstance(object_sig, Signature): + return None, None, object_sig + if sys.version_info[:2] in {(3, 12), (3, 13)} and callable(object_sig): + # Support for enum.Enum.__signature__ in Python 3.12 + if isinstance(object_sig_str := object_sig(), str): + return None, None, inspect.signature_from_str(object_sig_str) + + # 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._events.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._events.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._events.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._events.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) + if self.config.python_display_short_literal_types: + kwargs.setdefault('short_literals', 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) + if self.config.python_display_short_literal_types: + kwargs.setdefault('short_literals', 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 = f'{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 '' in __qualname__: + # No valid qualname found if the object is defined as locals + __qualname__ = None + + if __modname__ and __qualname__: + return f'{__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 isinstance(self.object, NewType | 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 isinstance(self.object, NewType) + and canonical_fullname + and self.fullname != canonical_fullname + ): + self.add_line(' :canonical: %s' % canonical_fullname, sourcename) + + # add inheritance info, if wanted + if not self.doc_as_attr and self.options.show_inheritance: + if inspect.getorigbases(self.object): + # A subclass of generic types + # refs: PEP-560 + bases = list(self.object.__orig_bases__) + elif hasattr(self.object, '__bases__') and len(self.object.__bases__): + # A normal class + bases = list(self.object.__bases__) + else: + bases = [] + + self._events.emit( + 'autodoc-process-bases', self.fullname, self.object, self.options, bases + ) + + mode = _get_render_mode(self.config.autodoc_typehints_format) + base_classes = [restify(cls, mode=mode) 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 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__ # for pypy + or 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__ # for pypy + or 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: + mode = _get_render_mode(self.config.autodoc_typehints_format) + short_literals = self.config.python_display_short_literal_types + + if isinstance(self.object, NewType): + supertype = restify(self.object.__supertype__, mode=mode) + + more_content = StringList([_('alias of %s') % supertype, ''], source='') + if isinstance(self.object, TypeVar): + attrs = [repr(self.object.__name__)] + attrs.extend( + stringify_annotation(constraint, mode, short_literals=short_literals) + for constraint in self.object.__constraints__ + ) + if self.object.__bound__: + bound = restify(self.object.__bound__, mode=mode) + 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: + alias = restify(self.object, mode=mode) + 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: type[Documenter], 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): + mode = _get_render_mode(self.config.autodoc_typehints_format) + alias = restify(self.object, mode=mode) + + 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) + annotations = get_type_hints( + parent, + None, + self.config.autodoc_type_aliases, + include_extras=True, + ) + 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: ClassVar[OptionSpec] = dict(ModuleLevelDocumenter.option_spec) + option_spec['annotation'] = annotation_option + option_spec['no-value'] = bool_option + + @classmethod + def can_document_member( + cls: type[Documenter], 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 not 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(functools.reduce(operator.iadd, 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, + include_extras=True, + ) + if self.objpath[-1] in annotations: + mode = _get_render_mode(self.config.autodoc_typehints_format) + short_literals = self.config.python_display_short_literal_types + objrepr = stringify_annotation( + annotations.get(self.objpath[-1]), + mode, + short_literals=short_literals, + ) + 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: type[Documenter], 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, self.object) + if inspect.isstaticmethod(obj, cls=self.parent, name=self.object_name): + # document static members before regular methods + self.member_order -= 1 # type: ignore[misc] + elif inspect.isclassmethod(obj): + # document class methods before static methods as + # they usually behave as alternative constructors + self.member_order -= 2 # type: ignore[misc] + 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) + if self.config.python_display_short_literal_types: + kwargs.setdefault('short_literals', True) + + try: + if self.object == object.__init__ and self.parent != object: # NoQA: E721 + # 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._events.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._events.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.is_classmethod_like(obj) + or inspect.is_singledispatch_method(obj) + and inspect.is_classmethod_like(obj.func) + ): + 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) + if self.config.python_display_short_literal_types: + kwargs.setdefault('short_literals', 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: + if inspect.isclassmethod(func): + func = func.__func__ + 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[..., Any], typ: type + ) -> Callable[..., Any] | 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(): # type: ignore[no-untyped-def] # NoQA: ANN202 + 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__ # for pypy + or 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__ # for pypy + or 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 ( + docstring := parent___slots__.get(self.objpath[-1]) + ): + docstring = prepare_docstring(docstring) + 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 + return self.is_runtime_instance_attribute_not_commented(parent) + + 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 = f'{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] + ) + 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, include_extras=True + ) + return self.objpath[-1] in annotations + + def import_object(self, raiseerror: bool = False) -> bool: + """Check the existence 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] + ) + 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( # type: ignore[misc] + GenericAliasMixin, + SlotsMixin, + RuntimeInstanceAttributeMixin, + UninitializedInstanceAttributeMixin, + NonDataDescriptorMixin, + DocstringStripSignatureMixin, + ClassLevelDocumenter, +): + """Specialized Documenter subclass for attributes.""" + + objtype = 'attribute' + member_order = 60 + option_spec: ClassVar[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: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any + ) -> bool: + if isinstance(parent, ModuleDocumenter): + return False + if inspect.isattributedescriptor(member): + return True + return not inspect.isroutine(member) and not isinstance(member, type) + + 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() + anns = analyzer.annotations + for (classname, attrname), annotation in anns.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(functools.reduce(operator.iadd, 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, + include_extras=True, + ) + if self.objpath[-1] in annotations: + mode = _get_render_mode(self.config.autodoc_typehints_format) + short_literals = self.config.python_display_short_literal_types + objrepr = stringify_annotation( + annotations.get(self.objpath[-1]), + mode, + short_literals=short_literals, + ) + 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. + # See: https://github.com/sphinx-doc/sphinx/issues/7805 + orig = self.config.autodoc_inherit_docstrings + self.config.autodoc_inherit_docstrings = False + return super().get_doc() + finally: + self.config.autodoc_inherit_docstrings = orig + + 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, ClassLevelDocumenter): # type: ignore[misc] + """Specialized Documenter subclass for properties.""" + + objtype = 'property' + member_order = 60 + + # before AttributeDocumenter + priority = AttributeDocumenter.priority + 1 + + @classmethod + def can_document_member( + cls: type[Documenter], 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 existence 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: bool = 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._events.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: + mode = _get_render_mode(self.config.autodoc_typehints_format) + short_literals = self.config.python_display_short_literal_types + objrepr = stringify_annotation( + signature.return_annotation, mode, short_literals=short_literals + ) + 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) -> Callable[..., Any] | None: + 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( + obj: Any, name: str, *defargs: Any, registry: SphinxComponentRegistry +) -> Any: + """Alternative getattr() for types""" + for typ, func in registry.autodoc_attrgetters.items(): + if isinstance(obj, typ): + return func(obj, name, *defargs) + + return safe_getattr(obj, name, *defargs) diff --git a/sphinx/ext/autodoc/_legacy_class_based/_sentinels.py b/sphinx/ext/autodoc/_legacy_class_based/_sentinels.py new file mode 100644 index 00000000000..8809f1a7be7 --- /dev/null +++ b/sphinx/ext/autodoc/_legacy_class_based/_sentinels.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any + + +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() +SUPPRESS = object() diff --git a/sphinx/ext/autodoc/directive.py b/sphinx/ext/autodoc/directive.py new file mode 100644 index 00000000000..96d5b54ca9d --- /dev/null +++ b/sphinx/ext/autodoc/directive.py @@ -0,0 +1,186 @@ +from __future__ import annotations + +from collections.abc import Callable +from typing import TYPE_CHECKING + +from docutils.statemachine import StringList +from docutils.utils import assemble_option_dict + +from sphinx.ext.autodoc._legacy_class_based._directive_options import Options +from sphinx.util import logging +from sphinx.util.docutils import SphinxDirective + +if TYPE_CHECKING: + from typing import Any + + from docutils.nodes import Node + from docutils.parsers.rst.states import RSTState + from docutils.utils import Reporter + + from sphinx.config import Config + from sphinx.environment import BuildEnvironment + from sphinx.ext.autodoc._legacy_class_based._documenters import Documenter + +logger = logging.getLogger(__name__) + + +# common option names for autodoc directives +# Retained: legacy class-based +AUTODOC_DEFAULT_OPTIONS = [ + 'members', + 'undoc-members', + 'no-index', + 'no-index-entry', + 'inherited-members', + 'show-inheritance', + 'private-members', + 'special-members', + 'ignore-module-all', + 'exclude-members', + 'member-order', + 'imported-members', + 'class-doc-from', + 'no-value', +] + +# Retained: legacy class-based +AUTODOC_EXTENDABLE_OPTIONS = frozenset({ + 'members', + 'private-members', + 'special-members', + 'exclude-members', +}) + + +# Retained: legacy class-based +class DummyOptionSpec(dict[str, Callable[[str], str]]): # NoQA: FURB189 + """An option_spec allows any options.""" + + def __bool__(self) -> bool: + """Behaves like some options are defined.""" + return True + + def __getitem__(self, _key: str) -> Callable[[str], str]: + return lambda x: x + + +# Retained: legacy class-based +class DocumenterBridge: + """A parameters container for Documenters.""" + + def __init__( + self, + env: BuildEnvironment, + reporter: Reporter | None, + options: Options, + lineno: int, + state: Any, + ) -> None: + self.env = env + self._reporter = reporter + self.genopt = options + self.lineno = lineno + self.record_dependencies: set[str] = set() + self.result = StringList() + self.state = state + + +# Retained: legacy class-based +def process_documenter_options( + documenter: type[Documenter], config: Config, options: dict[str, str] +) -> Options: + """Recognize options of Documenter from user input.""" + default_options = config.autodoc_default_options + for name in AUTODOC_DEFAULT_OPTIONS: + if name not in documenter.option_spec: + continue + negated = options.pop('no-' + name, True) is None + if name in default_options and not negated: + if name in options and isinstance(default_options[name], str): + # take value from options if present or extend it + # with autodoc_default_options if necessary + if name in AUTODOC_EXTENDABLE_OPTIONS: + if options[name] is not None and options[name].startswith('+'): + options[name] = f'{default_options[name]},{options[name][1:]}' + else: + options[name] = default_options[name] + + elif options.get(name) is not None: + # remove '+' from option argument if there's nothing to merge it with + options[name] = options[name].lstrip('+') + + return Options(assemble_option_dict(options.items(), documenter.option_spec)) + + +# Retained: legacy class-based +def parse_generated_content( + state: RSTState, content: StringList, documenter: Documenter +) -> list[Node]: + """Parse an item of content generated by Documenter.""" + from sphinx.ext.autodoc._directive import parse_generated_content + + return parse_generated_content(state, content, documenter.titles_allowed) + + +# Retained: legacy class-based +class AutodocDirective(SphinxDirective): + """A directive class for all autodoc directives. It works as a dispatcher of Documenters. + + It invokes a Documenter upon running. After the processing, it parses and returns + the content generated by Documenter. + """ + + option_spec = DummyOptionSpec() + has_content = True + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + + def run(self) -> list[Node]: + reporter = self.state.document.reporter + + try: + source, lineno = reporter.get_source_and_line( # type: ignore[attr-defined] + self.lineno + ) + except AttributeError: + source, lineno = (None, None) + logger.debug('[autodoc] %s:%s: input:\n%s', source, lineno, self.block_text) + + # look up target Documenter + objtype = self.name[4:] # strip prefix (auto-). + doccls = self.env._registry.documenters[objtype] + + # process the options with the selected documenter's option_spec + try: + documenter_options = process_documenter_options( + doccls, self.config, self.options + ) + except (KeyError, ValueError, TypeError) as exc: + # an option is either unknown or has a wrong type + logger.error( # NoQA: TRY400 + 'An option to %s is either unknown or has an invalid value: %s', + self.name, + exc, + location=(self.env.current_document.docname, lineno), + ) + return [] + + # generate the output + params = DocumenterBridge( + self.env, reporter, documenter_options, lineno, self.state + ) + documenter = doccls(params, self.arguments[0]) + documenter.generate(more_content=self.content) + if not params.result: + return [] + + logger.debug('[autodoc] output:\n%s', '\n'.join(params.result)) + + # record all filenames as dependencies -- this will at least + # partially make automatic invalidation possible + for fn in params.record_dependencies: + self.state.document.settings.record_dependencies.add(fn) + + result = parse_generated_content(self.state, params.result, documenter) + return result diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py new file mode 100644 index 00000000000..179c3260e0b --- /dev/null +++ b/sphinx/ext/autodoc/importer.py @@ -0,0 +1,327 @@ +"""Importer utilities for autodoc""" + +from __future__ import annotations + +import traceback +from importlib.machinery import EXTENSION_SUFFIXES +from typing import TYPE_CHECKING, NamedTuple + +from sphinx.errors import PycodeError +from sphinx.ext.autodoc._dynamic._importer import ( + _find_type_stub_spec as _find_type_stub_spec, # NoQA: PLC0414 +) +from sphinx.ext.autodoc._dynamic._importer import ( + _import_module as import_module, +) +from sphinx.ext.autodoc._dynamic._importer import _mangle_name as mangle +from sphinx.ext.autodoc._dynamic._importer import ( + _reload_module as _reload_module, # NoQA: PLC0414 +) +from sphinx.ext.autodoc._dynamic._importer import ( + _StubFileLoader as _StubFileLoader, # NoQA: PLC0414 +) +from sphinx.ext.autodoc._dynamic._member_finder import _filter_enum_dict, unmangle +from sphinx.ext.autodoc._dynamic._member_finder import ( + _is_native_enum_api as _is_native_enum_api, # NoQA: PLC0414 +) +from sphinx.ext.autodoc._dynamic._mock import ismock, undecorate +from sphinx.pycode import ModuleAnalyzer +from sphinx.util import logging +from sphinx.util.inspect import ( + getannotations, + getmro, + getslots, + isenumclass, + safe_getattr, +) + +if TYPE_CHECKING: + from typing import Any, Protocol + + from sphinx.ext.autodoc._legacy_class_based._documenters import ObjectMember + + class _AttrGetter(Protocol): + def __call__(self, obj: Any, name: str, default: Any = ..., /) -> Any: ... + + +_NATIVE_SUFFIXES: frozenset[str] = frozenset({'.pyx', *EXTENSION_SUFFIXES}) +logger = logging.getLogger(__name__) + + +# Retained: legacy class-based +def import_object( + modname: str, + objpath: list[str], + objtype: str = '', + attrgetter: _AttrGetter = safe_getattr, +) -> Any: + if objpath: + logger.debug('[autodoc] from %s import %s', modname, '.'.join(objpath)) + else: + logger.debug('[autodoc] import %s', modname) + + try: + module = None + exc_on_importing = None + objpath = objpath.copy() + while module is None: + try: + module = import_module(modname, try_reload=True) + logger.debug('[autodoc] import %s => %r', modname, module) + except ImportError as exc: + logger.debug('[autodoc] import %s => failed', modname) + exc_on_importing = exc + if '.' in modname: + # retry with parent module + modname, name = modname.rsplit('.', 1) + objpath.insert(0, name) + else: + raise + + obj = module + parent = None + object_name = None + for attrname in objpath: + parent = obj + logger.debug('[autodoc] getattr(_, %r)', attrname) + mangled_name = mangle(obj, attrname) + obj = attrgetter(obj, mangled_name) + + try: + logger.debug('[autodoc] => %r', obj) + except TypeError: + # fallback of failure on logging for broken object + # See: https://github.com/sphinx-doc/sphinx/issues/9095 + logger.debug('[autodoc] => %r', (obj,)) + + object_name = attrname + return [module, parent, object_name, obj] + except (AttributeError, ImportError) as exc: + if isinstance(exc, AttributeError) and exc_on_importing: + # restore ImportError + exc = exc_on_importing + + if objpath: + errmsg = 'autodoc: failed to import %s %r from module %r' % ( + objtype, + '.'.join(objpath), + modname, + ) + else: + errmsg = f'autodoc: failed to import {objtype} {modname!r}' + + if isinstance(exc, ImportError): + # import_module() raises ImportError having real exception obj and + # traceback + real_exc = exc.args[0] + traceback_msg = traceback.format_exception(exc) + if isinstance(real_exc, SystemExit): + errmsg += ( + '; the module executes module level statement ' + 'and it might call sys.exit().' + ) + elif isinstance(real_exc, ImportError) and real_exc.args: + errmsg += '; the following exception was raised:\n%s' % real_exc.args[0] + else: + errmsg += '; the following exception was raised:\n%s' % traceback_msg + else: + errmsg += ( + '; the following exception was raised:\n%s' % traceback.format_exc() + ) + + logger.debug(errmsg) + raise ImportError(errmsg) from exc + + +# Retained: legacy class-based +class Attribute(NamedTuple): + name: str + directly_defined: bool + value: Any + + +# Retained: legacy class-based +def get_object_members( + subject: Any, + objpath: list[str], + attrgetter: _AttrGetter, + analyzer: ModuleAnalyzer | None = None, +) -> dict[str, Attribute]: + """Get members and attributes of target object.""" + from sphinx.ext.autodoc._legacy_class_based._sentinels import INSTANCEATTR + + # the members directly defined in the class + obj_dict = attrgetter(subject, '__dict__', {}) + + members: dict[str, Attribute] = {} + + # enum members + if isenumclass(subject): + for name, defining_class, value in _filter_enum_dict( + subject, attrgetter, obj_dict + ): + # the order of occurrence of *name* matches the subject's MRO, + # allowing inherited attributes to be shadowed correctly + if unmangled := unmangle(defining_class, name): + members[unmangled] = Attribute( + name=unmangled, + directly_defined=defining_class is subject, + value=value, + ) + + # members in __slots__ + try: + subject___slots__ = getslots(subject) + if subject___slots__: + from sphinx.ext.autodoc._legacy_class_based._sentinels import SLOTSATTR + + for name in subject___slots__: + members[name] = Attribute( + name=name, directly_defined=True, value=SLOTSATTR + ) + except (TypeError, ValueError): + pass + + # other members + for name in dir(subject): + try: + value = attrgetter(subject, name) + directly_defined = name in obj_dict + unmangled = unmangle(subject, name) + if unmangled and unmangled not in members: + members[unmangled] = Attribute( + name=unmangled, directly_defined=directly_defined, value=value + ) + except AttributeError: + continue + + # annotation only member (ex. attr: int) + for cls in getmro(subject): + for name in getannotations(cls): + unmangled = unmangle(cls, name) + if unmangled and unmangled not in members: + members[unmangled] = Attribute( + name=unmangled, directly_defined=cls is subject, value=INSTANCEATTR + ) + + if analyzer: + # append instance attributes (cf. self.attr1) if analyzer knows + namespace = '.'.join(objpath) + for ns, name in analyzer.find_attr_docs(): + if namespace == ns and name not in members: + members[name] = Attribute( + name=name, directly_defined=True, value=INSTANCEATTR + ) + + return members + + +# Retained: legacy class-based +def get_class_members( + subject: Any, objpath: Any, attrgetter: _AttrGetter, inherit_docstrings: bool = True +) -> dict[str, ObjectMember]: + """Get members and attributes of target class.""" + from sphinx.ext.autodoc._legacy_class_based._documenters import ObjectMember + from sphinx.ext.autodoc._legacy_class_based._sentinels import INSTANCEATTR + + # the members directly defined in the class + obj_dict = attrgetter(subject, '__dict__', {}) + + members: dict[str, ObjectMember] = {} + + # enum members + if isenumclass(subject): + for name, defining_class, value in _filter_enum_dict( + subject, attrgetter, obj_dict + ): + # the order of occurrence of *name* matches the subject's MRO, + # allowing inherited attributes to be shadowed correctly + if unmangled := unmangle(defining_class, name): + members[unmangled] = ObjectMember( + unmangled, value, class_=defining_class + ) + + # members in __slots__ + try: + subject___slots__ = getslots(subject) + if subject___slots__: + from sphinx.ext.autodoc._legacy_class_based._sentinels import SLOTSATTR + + for name, docstring in subject___slots__.items(): + members[name] = ObjectMember( + name, SLOTSATTR, class_=subject, docstring=docstring + ) + except (TypeError, ValueError): + pass + + # other members + for name in dir(subject): + try: + value = attrgetter(subject, name) + if ismock(value): + value = undecorate(value) + + unmangled = unmangle(subject, name) + if unmangled and unmangled not in members: + if name in obj_dict: + members[unmangled] = ObjectMember(unmangled, value, class_=subject) + else: + members[unmangled] = ObjectMember(unmangled, value) + except AttributeError: + continue + + try: + for cls in getmro(subject): + try: + modname = safe_getattr(cls, '__module__') + qualname = safe_getattr(cls, '__qualname__') + analyzer = ModuleAnalyzer.for_module(modname) + analyzer.analyze() + except AttributeError: + qualname = None + analyzer = None + except PycodeError: + analyzer = None + + # annotation only member (ex. attr: int) + for name in getannotations(cls): + unmangled = unmangle(cls, name) + if unmangled and unmangled not in members: + if analyzer and (qualname, unmangled) in analyzer.attr_docs: + docstring = '\n'.join(analyzer.attr_docs[qualname, unmangled]) + else: + docstring = None + + members[unmangled] = ObjectMember( + unmangled, INSTANCEATTR, class_=cls, docstring=docstring + ) + + # append or complete instance attributes (cf. self.attr1) if analyzer knows + if analyzer: + for (ns, name), docstring in analyzer.attr_docs.items(): + if ns == qualname and name not in members: + # otherwise unknown instance attribute + members[name] = ObjectMember( + name, + INSTANCEATTR, + class_=cls, + docstring='\n'.join(docstring), + ) + elif ( + ns == qualname + and docstring + and isinstance(members[name], ObjectMember) + and not members[name].docstring + ): + if cls != subject and not inherit_docstrings: + # If we are in the MRO of the class and not the class itself, + # and we do not want to inherit docstrings, then skip setting + # the docstring below + continue + # attribute is already known, because dir(subject) enumerates it. + # But it has no docstring yet + members[name].docstring = '\n'.join(docstring) + except AttributeError: + pass + + return members diff --git a/sphinx/ext/autodoc/preserve_defaults.py b/sphinx/ext/autodoc/preserve_defaults.py new file mode 100644 index 00000000000..6d7c77fcc27 --- /dev/null +++ b/sphinx/ext/autodoc/preserve_defaults.py @@ -0,0 +1,46 @@ +"""Preserve function defaults. + +Preserve the default argument values of function signatures in source code +and keep them not evaluated for readability. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from sphinx.ext.autodoc._dynamic._preserve_defaults import ( + DefaultValue as DefaultValue, # NoQA: PLC0414 +) +from sphinx.ext.autodoc._dynamic._preserve_defaults import ( + _get_arguments as _get_arguments, # NoQA: PLC0414 +) +from sphinx.ext.autodoc._dynamic._preserve_defaults import ( + _get_arguments_inner as _get_arguments_inner, # NoQA: PLC0414 +) +from sphinx.ext.autodoc._dynamic._preserve_defaults import ( + _is_lambda as _is_lambda, # NoQA: PLC0414 +) +from sphinx.ext.autodoc._dynamic._preserve_defaults import ( + get_default_value as get_default_value, # NoQA: PLC0414 +) +from sphinx.ext.autodoc._dynamic._preserve_defaults import ( + get_function_def as get_function_def, # NoQA: PLC0414 +) +from sphinx.ext.autodoc._dynamic._preserve_defaults import update_default_value +from sphinx.util import logging + +if TYPE_CHECKING: + from typing import Any + + from sphinx.application import Sphinx + +logger = logging.getLogger(__name__) + + +# Retained: legacy class-based +def update_defvalue(app: Sphinx, obj: Any, bound_method: bool) -> None: + """Update defvalue info of *obj* using type_comments.""" + if not app.config.autodoc_preserve_defaults: + return + + update_default_value(obj, bound_method) diff --git a/sphinx/ext/autodoc/type_comment.py b/sphinx/ext/autodoc/type_comment.py new file mode 100644 index 00000000000..9eaef58f187 --- /dev/null +++ b/sphinx/ext/autodoc/type_comment.py @@ -0,0 +1,33 @@ +"""Update annotations info of living objects using type_comments.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from sphinx.ext.autodoc._dynamic._type_comments import ( + _update_annotations_using_type_comments, +) +from sphinx.ext.autodoc._dynamic._type_comments import ( + not_suppressed as not_suppressed, # NoQA: PLC0414 +) +from sphinx.ext.autodoc._dynamic._type_comments import ( + signature_from_ast as signature_from_ast, # NoQA: PLC0414 +) +from sphinx.util import logging + +if TYPE_CHECKING: + from typing import Any + + from sphinx.application import Sphinx + +logger = logging.getLogger(__name__) + + +# Retained: legacy class-based +def update_annotations_using_type_comments( + app: Sphinx, obj: Any, bound_method: bool +) -> None: + if not app.config.autodoc_use_type_comments: + return None + + return _update_annotations_using_type_comments(obj, bound_method) diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py index 46e11d96bf5..0ddd906051c 100644 --- a/sphinx/ext/autodoc/typehints.py +++ b/sphinx/ext/autodoc/typehints.py @@ -8,16 +8,40 @@ from docutils import nodes from sphinx import addnodes +from sphinx.ext.autodoc._dynamic._type_annotations import _record_typehints if TYPE_CHECKING: from collections.abc import Iterable + from typing import Any from docutils.nodes import Element from sphinx.application import Sphinx + from sphinx.ext.autodoc._legacy_class_based._directive_options import Options from sphinx.ext.autodoc._property_types import _AutodocObjType +# Retained: legacy class-based +def record_typehints( + app: Sphinx, + objtype: str, + name: str, + obj: Any, + options: Options, + args: str, + retann: str, +) -> None: + """Record type hints to env object.""" + _record_typehints( + autodoc_annotations=app.env.current_document.autodoc_annotations, + name=name, + obj=obj, + short_literals=app.config.python_display_short_literal_types, + type_aliases=app.config.autodoc_type_aliases, + unqualified_typehints=app.config.autodoc_typehints_format == 'short', + ) + + def _merge_typehints( app: Sphinx, domain: str, obj_type: _AutodocObjType, contentnode: Element ) -> None: diff --git a/sphinx/registry.py b/sphinx/registry.py index 5ccc6c5ddb0..35eee5c7a93 100644 --- a/sphinx/registry.py +++ b/sphinx/registry.py @@ -36,7 +36,7 @@ from sphinx.config import Config from sphinx.domains import Domain, Index from sphinx.environment import BuildEnvironment - from sphinx.ext.autodoc._documenters import Documenter + from sphinx.ext.autodoc._legacy_class_based._documenters import Documenter from sphinx.util.docfields import Field from sphinx.util.typing import ( ExtensionMetadata, diff --git a/tests/test_ext_autodoc/test_ext_autodoc.py b/tests/test_ext_autodoc/test_ext_autodoc.py index 6c73627fda1..49542d50fd0 100644 --- a/tests/test_ext_autodoc/test_ext_autodoc.py +++ b/tests/test_ext_autodoc/test_ext_autodoc.py @@ -20,9 +20,9 @@ _AutoDocumenterOptions, inherited_members_option, ) -from sphinx.ext.autodoc._documenters import Documenter from sphinx.ext.autodoc._dynamic._docstrings import _get_docstring_lines from sphinx.ext.autodoc._generate import _auto_document_object +from sphinx.ext.autodoc._legacy_class_based._documenters import Documenter from sphinx.ext.autodoc._property_types import _ItemProperties from sphinx.ext.autodoc._sentinels import ALL from sphinx.ext.autodoc._shared import _AutodocAttrGetter, _AutodocConfig