summaryrefslogtreecommitdiffstats
path: root/sphinx/ext/autodoc
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-05 16:20:58 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-05 16:20:58 +0000
commit5bb0bb4be543fd5eca41673696a62ed80d493591 (patch)
treead2c464f140e86c7f178a6276d7ea4a93e3e6c92 /sphinx/ext/autodoc
parentAdding upstream version 7.2.6. (diff)
downloadsphinx-upstream.tar.xz
sphinx-upstream.zip
Adding upstream version 7.3.7.upstream/7.3.7upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'sphinx/ext/autodoc')
-rw-r--r--sphinx/ext/autodoc/__init__.py176
-rw-r--r--sphinx/ext/autodoc/directive.py13
-rw-r--r--sphinx/ext/autodoc/importer.py138
-rw-r--r--sphinx/ext/autodoc/mock.py12
-rw-r--r--sphinx/ext/autodoc/preserve_defaults.py7
-rw-r--r--sphinx/ext/autodoc/type_comment.py3
-rw-r--r--sphinx/ext/autodoc/typehints.py9
7 files changed, 232 insertions, 126 deletions
diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py
index 8d68f72..45e4cad 100644
--- a/sphinx/ext/autodoc/__init__.py
+++ b/sphinx/ext/autodoc/__init__.py
@@ -7,11 +7,13 @@ for those who like elaborate docstrings.
from __future__ import annotations
+import functools
+import operator
import re
import sys
import warnings
from inspect import Parameter, Signature
-from typing import TYPE_CHECKING, Any, Callable, TypeVar
+from typing import TYPE_CHECKING, Any, Callable, ClassVar, TypeVar
from docutils.statemachine import StringList
@@ -31,7 +33,13 @@ from sphinx.util.inspect import (
safe_getattr,
stringify_signature,
)
-from sphinx.util.typing import OptionSpec, get_type_hints, restify, stringify_annotation
+from sphinx.util.typing import (
+ ExtensionMetadata,
+ OptionSpec,
+ get_type_hints,
+ restify,
+ stringify_annotation,
+)
if TYPE_CHECKING:
from collections.abc import Iterator, Sequence
@@ -221,7 +229,7 @@ def between(
return
deleted = 0
delete = not exclude
- orig_lines = lines[:]
+ orig_lines = lines.copy()
for i, line in enumerate(orig_lines):
if delete:
lines.pop(i - deleted)
@@ -243,6 +251,7 @@ def between(
# But we define this class here to keep compatibility (see #4538)
class Options(dict):
"""A dict/attribute hybrid that returns None on nonexisting keys."""
+
def copy(self) -> Options:
return Options(super().copy())
@@ -275,7 +284,7 @@ class ObjectMember:
self.skipped = skipped
self.class_ = class_
- def __getitem__(self, index):
+ def __getitem__(self, index: int) -> Any:
warnings.warn('The tuple interface of ObjectMember is deprecated. '
'Use (obj.__name__, obj.object) instead.',
RemovedInSphinx80Warning, stacklevel=2)
@@ -297,6 +306,7 @@ class Documenter:
in fact, it will be used to parse an auto directive's options that matches
the Documenter.
"""
+
#: name by which the directive is called (auto...) and the default
#: generated directive name
objtype = 'object'
@@ -309,7 +319,7 @@ class Documenter:
#: true if the generated content may contain titles
titles_allowed = True
- option_spec: OptionSpec = {
+ option_spec: ClassVar[OptionSpec] = {
'no-index': bool_option,
'noindex': bool_option,
}
@@ -319,8 +329,9 @@ class Documenter:
return autodoc_attrgetter(self.env.app, obj, name, *defargs)
@classmethod
- def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any,
- ) -> bool:
+ 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)
@@ -450,9 +461,7 @@ class Documenter:
subject = inspect.unpartial(self.object)
modname = self.get_attr(subject, '__module__', None)
- if modname and modname != self.modname:
- return False
- return True
+ return not modname or modname == self.modname
def format_args(self, **kwargs: Any) -> str:
"""Format the argument signature of *self.object*.
@@ -923,7 +932,7 @@ class Documenter:
except PycodeError:
pass
- docstrings: list[str] = sum(self.get_doc() or [], [])
+ 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')
@@ -966,11 +975,12 @@ class ModuleDocumenter(Documenter):
"""
Specialized Documenter subclass for modules.
"""
+
objtype = 'module'
content_indent = ''
_extra_indent = ' '
- option_spec: OptionSpec = {
+ option_spec: ClassVar[OptionSpec] = {
'members': members_option, 'undoc-members': bool_option,
'no-index': bool_option, 'inherited-members': inherited_members_option,
'show-inheritance': bool_option, 'synopsis': identity,
@@ -997,8 +1007,9 @@ class ModuleDocumenter(Documenter):
self.add_line(line, src[0], src[1])
@classmethod
- def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any,
- ) -> bool:
+ def can_document_member(
+ cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any,
+ ) -> bool:
# don't document submodules automatically
return False
@@ -1127,13 +1138,14 @@ 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]
+ return modname, [*parents, base]
if path:
modname = path.rstrip('.')
- return modname, parents + [base]
+ return modname, [*parents, base]
# if documenting a toplevel object without explicit module,
# it can be contained in another auto directive ...
@@ -1142,7 +1154,7 @@ class ModuleLevelDocumenter(Documenter):
if not modname:
modname = self.env.ref_context.get('py:module')
# ... else, it stays None, which means invalid
- return modname, parents + [base]
+ return modname, [*parents, base]
class ClassLevelDocumenter(Documenter):
@@ -1150,10 +1162,11 @@ 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]
+ return modname, [*parents, base]
if path:
mod_cls = path.rstrip('.')
@@ -1177,7 +1190,7 @@ class ClassLevelDocumenter(Documenter):
if not modname:
modname = self.env.ref_context.get('py:module')
# ... else, it stays None, which means invalid
- return modname, parents + [base]
+ return modname, [*parents, base]
class DocstringSignatureMixin:
@@ -1185,6 +1198,7 @@ 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] = []
@@ -1256,7 +1270,7 @@ class DocstringSignatureMixin:
self.args, self.retann = result
sig = super().format_signature(**kwargs) # type: ignore[misc]
if self._signatures:
- return "\n".join([sig] + self._signatures)
+ return "\n".join((sig, *self._signatures))
else:
return sig
@@ -1266,6 +1280,7 @@ 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
@@ -1286,12 +1301,14 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
"""
Specialized Documenter subclass for functions.
"""
+
objtype = 'function'
member_order = 30
@classmethod
- def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any,
- ) -> bool:
+ 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)))
@@ -1393,7 +1410,7 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
if len(sig.parameters) == 0:
return None
- def dummy():
+ def dummy(): # NoQA: ANN202
pass
params = list(sig.parameters.values())
@@ -1414,6 +1431,7 @@ class DecoratorDocumenter(FunctionDocumenter):
"""
Specialized Documenter subclass for decorator functions.
"""
+
objtype = 'decorator'
# must be lower than FunctionDocumenter
@@ -1445,9 +1463,10 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
"""
Specialized Documenter subclass for classes.
"""
+
objtype = 'class'
member_order = 20
- option_spec: OptionSpec = {
+ option_spec: ClassVar[OptionSpec] = {
'members': members_option, 'undoc-members': bool_option,
'no-index': bool_option, 'inherited-members': inherited_members_option,
'show-inheritance': bool_option, 'member-order': member_order_option,
@@ -1481,8 +1500,9 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
merge_members_option(self.options)
@classmethod
- def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any,
- ) -> bool:
+ def can_document_member(
+ cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any,
+ ) -> bool:
return isinstance(member, type) or (
isattr and (inspect.isNewType(member) or isinstance(member, TypeVar)))
@@ -1509,7 +1529,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
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. """
+ """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)
@@ -1657,7 +1677,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
try:
analyzer = ModuleAnalyzer.for_module(cls.__module__)
analyzer.analyze()
- qualname = '.'.join([cls.__qualname__, self._signature_method_name])
+ qualname = f'{cls.__qualname__}.{self._signature_method_name}'
if qualname in analyzer.overloads:
return analyzer.overloads.get(qualname, [])
elif qualname in analyzer.tagorder:
@@ -1678,7 +1698,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
__qualname__ = None
if __modname__ and __qualname__:
- return '.'.join([__modname__, __qualname__])
+ return f'{__modname__}.{__qualname__}'
else:
return None
@@ -1904,6 +1924,7 @@ class ExceptionDocumenter(ClassDocumenter):
"""
Specialized ClassDocumenter subclass for exceptions.
"""
+
objtype = 'exception'
member_order = 10
@@ -1911,8 +1932,9 @@ class ExceptionDocumenter(ClassDocumenter):
priority = ClassDocumenter.priority + 5
@classmethod
- def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any,
- ) -> bool:
+ 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:
@@ -2016,16 +2038,18 @@ class DataDocumenter(GenericAliasMixin,
"""
Specialized Documenter subclass for data items.
"""
+
objtype = 'data'
member_order = 40
priority = -10
- option_spec: OptionSpec = dict(ModuleLevelDocumenter.option_spec)
+ 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, member: Any, membername: str, isattr: bool, parent: Any,
- ) -> bool:
+ 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:
@@ -2054,7 +2078,8 @@ class DataDocumenter(GenericAliasMixin,
return True
else:
doc = self.get_doc() or []
- docstring, metadata = separate_metadata('\n'.join(sum(doc, [])))
+ docstring, metadata = separate_metadata(
+ '\n'.join(functools.reduce(operator.iadd, doc, [])))
if 'hide-value' in metadata:
return True
@@ -2135,14 +2160,16 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
"""
Specialized Documenter subclass for methods (normal, static and class).
"""
+
objtype = 'method'
directivetype = 'method'
member_order = 50
priority = 1 # must be more than FunctionDocumenter
@classmethod
- def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any,
- ) -> bool:
+ 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:
@@ -2169,7 +2196,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
kwargs.setdefault('unqualified_typehints', True)
try:
- if self.object == object.__init__ and self.parent != object:
+ 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).
@@ -2206,7 +2233,8 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
self.add_line(' :abstractmethod:', sourcename)
if inspect.iscoroutinefunction(obj) or inspect.isasyncgenfunction(obj):
self.add_line(' :async:', sourcename)
- if inspect.isclassmethod(obj):
+ if (inspect.isclassmethod(obj) or
+ inspect.is_singledispatch_method(obj) and inspect.isclassmethod(obj.func)):
self.add_line(' :classmethod:', sourcename)
if inspect.isstaticmethod(obj, cls=self.parent, name=self.object_name):
self.add_line(' :staticmethod:', sourcename)
@@ -2238,6 +2266,8 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
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, '')
@@ -2292,7 +2322,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
if len(sig.parameters) == 1:
return None
- def dummy():
+ def dummy(): # NoQA: ANN202
pass
params = list(sig.parameters.values())
@@ -2408,8 +2438,8 @@ class SlotsMixin(DataDocumenterMixinBase):
if self.object is SLOTSATTR:
try:
parent___slots__ = inspect.getslots(self.parent)
- if parent___slots__ and parent___slots__.get(self.objpath[-1]):
- docstring = prepare_docstring(parent___slots__[self.objpath[-1]])
+ if parent___slots__ and (docstring := parent___slots__.get(self.objpath[-1])):
+ docstring = prepare_docstring(docstring)
return [docstring]
else:
return []
@@ -2440,9 +2470,7 @@ class RuntimeInstanceAttributeMixin(DataDocumenterMixinBase):
# An instance variable defined in __init__().
if self.get_attribute_comment(parent, self.objpath[-1]): # type: ignore[attr-defined]
return True
- if self.is_runtime_instance_attribute_not_commented(parent):
- return True
- return False
+ 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."""
@@ -2454,7 +2482,7 @@ class RuntimeInstanceAttributeMixin(DataDocumenterMixinBase):
analyzer = ModuleAnalyzer.for_module(module)
analyzer.analyze()
if qualname and self.objpath:
- key = '.'.join([qualname, self.objpath[-1]])
+ key = f'{qualname}.{self.objpath[-1]}'
if key in analyzer.tagorder:
return True
except (AttributeError, PycodeError):
@@ -2464,7 +2492,8 @@ class RuntimeInstanceAttributeMixin(DataDocumenterMixinBase):
def import_object(self, raiseerror: bool = False) -> bool:
"""Check the existence of runtime instance attribute after failing to import the
- attribute."""
+ attribute.
+ """
try:
return super().import_object(raiseerror=True) # type: ignore[misc]
except ImportError as exc:
@@ -2517,7 +2546,8 @@ class UninitializedInstanceAttributeMixin(DataDocumenterMixinBase):
def import_object(self, raiseerror: bool = False) -> bool:
"""Check the exisitence of uninitialized instance attribute when failed to import
- the attribute."""
+ the attribute.
+ """
try:
return super().import_object(raiseerror=True) # type: ignore[misc]
except ImportError as exc:
@@ -2556,9 +2586,10 @@ class AttributeDocumenter(GenericAliasMixin, SlotsMixin, # type: ignore[misc]
"""
Specialized Documenter subclass for attributes.
"""
+
objtype = 'attribute'
member_order = 60
- option_spec: OptionSpec = dict(ModuleLevelDocumenter.option_spec)
+ option_spec: ClassVar[OptionSpec] = dict(ModuleLevelDocumenter.option_spec)
option_spec["annotation"] = annotation_option
option_spec["no-value"] = bool_option
@@ -2571,15 +2602,14 @@ class AttributeDocumenter(GenericAliasMixin, SlotsMixin, # type: ignore[misc]
return inspect.isfunction(obj) or inspect.isbuiltin(obj) or inspect.ismethod(obj)
@classmethod
- def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any,
- ) -> bool:
+ 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
- if not inspect.isroutine(member) and not isinstance(member, type):
- return True
- return False
+ return not inspect.isroutine(member) and not isinstance(member, type)
def document_members(self, all_members: bool = False) -> None:
pass
@@ -2625,7 +2655,8 @@ class AttributeDocumenter(GenericAliasMixin, SlotsMixin, # type: ignore[misc]
else:
doc = self.get_doc()
if doc:
- docstring, metadata = separate_metadata('\n'.join(sum(doc, [])))
+ docstring, metadata = separate_metadata(
+ '\n'.join(functools.reduce(operator.iadd, doc, [])))
if 'hide-value' in metadata:
return True
@@ -2711,6 +2742,7 @@ class PropertyDocumenter(DocstringStripSignatureMixin, # type: ignore[misc]
"""
Specialized Documenter subclass for properties.
"""
+
objtype = 'property'
member_order = 60
@@ -2718,8 +2750,9 @@ class PropertyDocumenter(DocstringStripSignatureMixin, # type: ignore[misc]
priority = AttributeDocumenter.priority + 1
@classmethod
- def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any,
- ) -> bool:
+ 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
@@ -2732,7 +2765,8 @@ class PropertyDocumenter(DocstringStripSignatureMixin, # type: ignore[misc]
def import_object(self, raiseerror: bool = False) -> bool:
"""Check the exisitence of uninitialized instance attribute when failed to import
- the attribute."""
+ the attribute.
+ """
ret = super().import_object(raiseerror)
if ret and not inspect.isproperty(self.object):
__dict__ = safe_getattr(self.parent, '__dict__', {})
@@ -2793,7 +2827,7 @@ class PropertyDocumenter(DocstringStripSignatureMixin, # type: ignore[misc]
except ValueError:
pass
- def _get_property_getter(self):
+ def _get_property_getter(self) -> Callable | None:
if safe_getattr(self.object, 'fget', None): # property
return self.object.fget
if safe_getattr(self.object, 'func', None): # cached_property
@@ -2810,7 +2844,7 @@ def autodoc_attrgetter(app: Sphinx, obj: Any, name: str, *defargs: Any) -> Any:
return safe_getattr(obj, name, *defargs)
-def setup(app: Sphinx) -> dict[str, Any]:
+def setup(app: Sphinx) -> ExtensionMetadata:
app.add_autodocumenter(ModuleDocumenter)
app.add_autodocumenter(ClassDocumenter)
app.add_autodocumenter(ExceptionDocumenter)
@@ -2821,22 +2855,22 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_autodocumenter(AttributeDocumenter)
app.add_autodocumenter(PropertyDocumenter)
- app.add_config_value('autoclass_content', 'class', True, ENUM('both', 'class', 'init'))
- app.add_config_value('autodoc_member_order', 'alphabetical', True,
+ app.add_config_value('autoclass_content', 'class', 'env', ENUM('both', 'class', 'init'))
+ app.add_config_value('autodoc_member_order', 'alphabetical', 'env',
ENUM('alphabetical', 'bysource', 'groupwise'))
- app.add_config_value('autodoc_class_signature', 'mixed', True, ENUM('mixed', 'separated'))
- app.add_config_value('autodoc_default_options', {}, True)
- app.add_config_value('autodoc_docstring_signature', True, True)
- app.add_config_value('autodoc_mock_imports', [], True)
- app.add_config_value('autodoc_typehints', "signature", True,
+ app.add_config_value('autodoc_class_signature', 'mixed', 'env', ENUM('mixed', 'separated'))
+ app.add_config_value('autodoc_default_options', {}, 'env')
+ app.add_config_value('autodoc_docstring_signature', True, 'env')
+ app.add_config_value('autodoc_mock_imports', [], 'env')
+ app.add_config_value('autodoc_typehints', "signature", 'env',
ENUM("signature", "description", "none", "both"))
- app.add_config_value('autodoc_typehints_description_target', 'all', True,
+ app.add_config_value('autodoc_typehints_description_target', 'all', 'env',
ENUM('all', 'documented', 'documented_params'))
- app.add_config_value('autodoc_type_aliases', {}, True)
+ app.add_config_value('autodoc_type_aliases', {}, 'env')
app.add_config_value('autodoc_typehints_format', "short", 'env',
ENUM("fully-qualified", "short"))
- app.add_config_value('autodoc_warningiserror', True, True)
- app.add_config_value('autodoc_inherit_docstrings', True, True)
+ app.add_config_value('autodoc_warningiserror', True, 'env')
+ app.add_config_value('autodoc_inherit_docstrings', True, 'env')
app.add_event('autodoc-before-process-signature')
app.add_event('autodoc-process-docstring')
app.add_event('autodoc-process-signature')
diff --git a/sphinx/ext/autodoc/directive.py b/sphinx/ext/autodoc/directive.py
index 64cbc9b..130e347 100644
--- a/sphinx/ext/autodoc/directive.py
+++ b/sphinx/ext/autodoc/directive.py
@@ -59,20 +59,20 @@ class DocumenterBridge:
def process_documenter_options(documenter: type[Documenter], config: Config, options: dict,
) -> 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 config.autodoc_default_options and not negated:
- if name in options and isinstance(config.autodoc_default_options[name], str):
+ 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] = ','.join([config.autodoc_default_options[name],
- options[name][1:]])
+ options[name] = f'{default_options[name]},{options[name][1:]}'
else:
- options[name] = config.autodoc_default_options[name]
+ options[name] = default_options[name]
elif options.get(name) is not None:
# remove '+' from option argument if there's nothing to merge it with
@@ -104,6 +104,7 @@ class AutodocDirective(SphinxDirective):
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
@@ -114,7 +115,7 @@ class AutodocDirective(SphinxDirective):
reporter = self.state.document.reporter
try:
- source, lineno = reporter.get_source_and_line( # type: ignore[attr-defined]
+ source, lineno = reporter.get_source_and_line(
self.lineno)
except AttributeError:
source, lineno = (None, None)
diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py
index 84bfee5..784fa71 100644
--- a/sphinx/ext/autodoc/importer.py
+++ b/sphinx/ext/autodoc/importer.py
@@ -8,7 +8,8 @@ import os
import sys
import traceback
import typing
-from typing import TYPE_CHECKING, Any, Callable, NamedTuple
+from enum import Enum
+from typing import TYPE_CHECKING, NamedTuple
from sphinx.ext.autodoc.mock import ismock, undecorate
from sphinx.pycode import ModuleAnalyzer, PycodeError
@@ -20,16 +21,91 @@ from sphinx.util.inspect import (
isclass,
isenumclass,
safe_getattr,
+ unwrap_all,
)
if TYPE_CHECKING:
+ from collections.abc import Callable, Iterator, Mapping
from types import ModuleType
+ from typing import Any
from sphinx.ext.autodoc import ObjectMember
logger = logging.getLogger(__name__)
+def _filter_enum_dict(
+ enum_class: type[Enum],
+ attrgetter: Callable[[Any, str, Any], Any],
+ enum_class_dict: Mapping[str, object],
+) -> Iterator[tuple[str, type, Any]]:
+ """Find the attributes to document of an enumeration class.
+
+ The output consists of triplets ``(attribute name, defining class, value)``
+ where the attribute name can appear more than once during the iteration
+ but with different defining class. The order of occurrence is guided by
+ the MRO of *enum_class*.
+ """
+ # attributes that were found on a mixin type or the data type
+ candidate_in_mro: set[str] = set()
+ # sunder names that were picked up (and thereby allowed to be redefined)
+ # see: https://docs.python.org/3/howto/enum.html#supported-dunder-names
+ sunder_names = {'_name_', '_value_', '_missing_', '_order_', '_generate_next_value_'}
+ # attributes that can be picked up on a mixin type or the enum's data type
+ public_names = {'name', 'value', *object.__dict__, *sunder_names}
+ # names that are ignored by default
+ ignore_names = Enum.__dict__.keys() - public_names
+
+ def is_native_api(obj: object, name: str) -> bool:
+ """Check whether *obj* is the same as ``Enum.__dict__[name]``."""
+ return unwrap_all(obj) is unwrap_all(Enum.__dict__[name])
+
+ def should_ignore(name: str, value: Any) -> bool:
+ if name in sunder_names:
+ return is_native_api(value, name)
+ return name in ignore_names
+
+ sentinel = object()
+
+ def query(name: str, defining_class: type) -> tuple[str, type, Any] | None:
+ value = attrgetter(enum_class, name, sentinel)
+ if value is not sentinel:
+ return (name, defining_class, value)
+ return None
+
+ # attributes defined on a parent type, possibly shadowed later by
+ # the attributes defined directly inside the enumeration class
+ for parent in enum_class.__mro__:
+ if parent in {enum_class, Enum, object}:
+ continue
+
+ parent_dict = attrgetter(parent, '__dict__', {})
+ for name, value in parent_dict.items():
+ if should_ignore(name, value):
+ continue
+
+ candidate_in_mro.add(name)
+ if (item := query(name, parent)) is not None:
+ yield item
+
+ # exclude members coming from the native Enum unless
+ # they were redefined on a mixin type or the data type
+ excluded_members = Enum.__dict__.keys() - candidate_in_mro
+ yield from filter(None, (query(name, enum_class) for name in enum_class_dict
+ if name not in excluded_members))
+
+ # check if allowed members from ``Enum`` were redefined at the enum level
+ special_names = sunder_names | public_names
+ special_names &= enum_class_dict.keys()
+ special_names &= Enum.__dict__.keys()
+ for name in special_names:
+ if (
+ not is_native_api(enum_class_dict[name], name)
+ and (item := query(name, enum_class)) is not None
+ ):
+ yield item
+
+
def mangle(subject: Any, name: str) -> str:
"""Mangle the given name."""
try:
@@ -61,9 +137,7 @@ def unmangle(subject: Any, name: str) -> str | None:
def import_module(modname: str, warningiserror: bool = False) -> Any:
- """
- Call importlib.import_module(modname), convert exceptions to ImportError
- """
+ """Call importlib.import_module(modname), convert exceptions to ImportError."""
try:
with logging.skip_warningiserror(not warningiserror):
return importlib.import_module(modname)
@@ -97,7 +171,7 @@ def import_object(modname: str, objpath: list[str], objtype: str = '',
try:
module = None
exc_on_importing = None
- objpath = list(objpath)
+ objpath = objpath.copy()
while module is None:
try:
original_module_names = frozenset(sys.modules)
@@ -194,15 +268,11 @@ def get_object_members(
# enum members
if isenumclass(subject):
- for name, value in subject.__members__.items():
- if name not in members:
- members[name] = Attribute(name, True, value)
-
- superclass = subject.__mro__[1]
- for name in obj_dict:
- if name not in superclass.__dict__:
- value = safe_getattr(subject, name)
- members[name] = Attribute(name, True, value)
+ 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(unmangled, defining_class is subject, value)
# members in __slots__
try:
@@ -220,18 +290,18 @@ def get_object_members(
try:
value = attrgetter(subject, name)
directly_defined = name in obj_dict
- name = unmangle(subject, name)
- if name and name not in members:
- members[name] = Attribute(name, directly_defined, value)
+ unmangled = unmangle(subject, name)
+ if unmangled and unmangled not in members:
+ members[unmangled] = Attribute(unmangled, directly_defined, value)
except AttributeError:
continue
# annotation only member (ex. attr: int)
- for i, cls in enumerate(getmro(subject)):
+ for cls in getmro(subject):
for name in getannotations(cls):
- name = unmangle(cls, name)
- if name and name not in members:
- members[name] = Attribute(name, i == 0, INSTANCEATTR)
+ unmangled = unmangle(cls, name)
+ if unmangled and unmangled not in members:
+ members[unmangled] = Attribute(unmangled, cls is subject, INSTANCEATTR)
if analyzer:
# append instance attributes (cf. self.attr1) if analyzer knows
@@ -255,15 +325,11 @@ def get_class_members(subject: Any, objpath: Any, attrgetter: Callable,
# enum members
if isenumclass(subject):
- for name, value in subject.__members__.items():
- if name not in members:
- members[name] = ObjectMember(name, value, class_=subject)
-
- superclass = subject.__mro__[1]
- for name in obj_dict:
- if name not in superclass.__dict__:
- value = safe_getattr(subject, name)
- members[name] = ObjectMember(name, value, class_=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:
@@ -308,15 +374,15 @@ def get_class_members(subject: Any, objpath: Any, attrgetter: Callable,
# annotation only member (ex. attr: int)
for name in getannotations(cls):
- name = unmangle(cls, name)
- if name and name not in members:
- if analyzer and (qualname, name) in analyzer.attr_docs:
- docstring = '\n'.join(analyzer.attr_docs[qualname, name])
+ 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[name] = ObjectMember(name, INSTANCEATTR, class_=cls,
- docstring=docstring)
+ members[unmangled] = ObjectMember(unmangled, INSTANCEATTR, class_=cls,
+ docstring=docstring)
# append or complete instance attributes (cf. self.attr1) if analyzer knows
if analyzer:
diff --git a/sphinx/ext/autodoc/mock.py b/sphinx/ext/autodoc/mock.py
index 7034977..c2ab0fe 100644
--- a/sphinx/ext/autodoc/mock.py
+++ b/sphinx/ext/autodoc/mock.py
@@ -14,7 +14,7 @@ from sphinx.util import logging
from sphinx.util.inspect import isboundmethod, safe_getattr
if TYPE_CHECKING:
- from collections.abc import Generator, Iterator, Sequence
+ from collections.abc import Iterator, Sequence
logger = logging.getLogger(__name__)
@@ -80,6 +80,7 @@ def _make_subclass(name: str, module: str, superclass: Any = _MockObject,
class _MockModule(ModuleType):
"""Used by autodoc_mock_imports."""
+
__file__ = os.devnull
__sphinx_mock__ = True
@@ -97,6 +98,7 @@ class _MockModule(ModuleType):
class MockLoader(Loader):
"""A loader for mocking."""
+
def __init__(self, finder: MockFinder) -> None:
super().__init__()
self.finder = finder
@@ -135,12 +137,12 @@ class MockFinder(MetaPathFinder):
@contextlib.contextmanager
-def mock(modnames: list[str]) -> Generator[None, None, None]:
+def mock(modnames: list[str]) -> Iterator[None]:
"""Insert mock modules during context::
- with mock(['target.module.name']):
- # mock modules are enabled here
- ...
+ with mock(['target.module.name']):
+ # mock modules are enabled here
+ ...
"""
try:
finder = MockFinder(modnames)
diff --git a/sphinx/ext/autodoc/preserve_defaults.py b/sphinx/ext/autodoc/preserve_defaults.py
index 5f957ce..b0b3243 100644
--- a/sphinx/ext/autodoc/preserve_defaults.py
+++ b/sphinx/ext/autodoc/preserve_defaults.py
@@ -22,6 +22,7 @@ if TYPE_CHECKING:
from typing import Any
from sphinx.application import Sphinx
+ from sphinx.util.typing import ExtensionMetadata
logger = logging.getLogger(__name__)
_LAMBDA_NAME = (lambda: None).__name__
@@ -96,7 +97,7 @@ def _get_arguments(obj: Any, /) -> ast.arguments | None:
return _get_arguments_inner(subject)
-def _is_lambda(x, /):
+def _is_lambda(x: Any, /) -> bool:
return isinstance(x, types.LambdaType) and x.__name__ == _LAMBDA_NAME
@@ -189,8 +190,8 @@ def update_defvalue(app: Sphinx, obj: Any, bound_method: bool) -> None:
logger.warning(__("Failed to parse a default argument value for %r: %s"), obj, exc)
-def setup(app: Sphinx) -> dict[str, Any]:
- app.add_config_value('autodoc_preserve_defaults', False, True)
+def setup(app: Sphinx) -> ExtensionMetadata:
+ app.add_config_value('autodoc_preserve_defaults', False, 'env')
app.connect('autodoc-before-process-signature', update_defvalue)
return {
diff --git a/sphinx/ext/autodoc/type_comment.py b/sphinx/ext/autodoc/type_comment.py
index e2c9ae2..e0a5a63 100644
--- a/sphinx/ext/autodoc/type_comment.py
+++ b/sphinx/ext/autodoc/type_comment.py
@@ -15,6 +15,7 @@ if TYPE_CHECKING:
from collections.abc import Sequence
from sphinx.application import Sphinx
+ from sphinx.util.typing import ExtensionMetadata
logger = logging.getLogger(__name__)
@@ -134,7 +135,7 @@ def update_annotations_using_type_comments(app: Sphinx, obj: Any, bound_method:
logger.warning(__("Failed to parse type_comment for %r: %s"), obj, exc)
-def setup(app: Sphinx) -> dict[str, Any]:
+def setup(app: Sphinx) -> ExtensionMetadata:
app.connect('autodoc-before-process-signature', update_annotations_using_type_comments)
return {'version': sphinx.__display_version__, 'parallel_read_safe': True}
diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py
index 79906fb..df0c468 100644
--- a/sphinx/ext/autodoc/typehints.py
+++ b/sphinx/ext/autodoc/typehints.py
@@ -11,16 +11,17 @@ from docutils import nodes
import sphinx
from sphinx import addnodes
from sphinx.util import inspect
-from sphinx.util.typing import stringify_annotation
+from sphinx.util.typing import ExtensionMetadata, stringify_annotation
if TYPE_CHECKING:
from docutils.nodes import Element
from sphinx.application import Sphinx
+ from sphinx.ext.autodoc import Options
def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any,
- options: dict, args: str, retann: str) -> None:
+ options: Options, args: str, retann: str) -> None:
"""Record type hints to env object."""
if app.config.autodoc_typehints_format == 'short':
mode = 'smart'
@@ -50,7 +51,7 @@ def merge_typehints(app: Sphinx, domain: str, objtype: str, contentnode: Element
try:
signature = cast(addnodes.desc_signature, contentnode.parent[0])
if signature['module']:
- fullname = '.'.join([signature['module'], signature['fullname']])
+ fullname = f'{signature["module"]}.{signature["fullname"]}'
else:
fullname = signature['fullname']
except KeyError:
@@ -208,7 +209,7 @@ def augment_descriptions_with_types(
node += field
-def setup(app: Sphinx) -> dict[str, Any]:
+def setup(app: Sphinx) -> ExtensionMetadata:
app.connect('autodoc-process-signature', record_typehints)
app.connect('object-description-transform', merge_typehints)