From 5bb0bb4be543fd5eca41673696a62ed80d493591 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 5 Jun 2024 18:20:58 +0200 Subject: Adding upstream version 7.3.7. Signed-off-by: Daniel Baumann --- sphinx/ext/autodoc/__init__.py | 176 +++++++++++++++++++------------- sphinx/ext/autodoc/directive.py | 13 +-- sphinx/ext/autodoc/importer.py | 138 ++++++++++++++++++------- sphinx/ext/autodoc/mock.py | 12 ++- sphinx/ext/autodoc/preserve_defaults.py | 7 +- sphinx/ext/autodoc/type_comment.py | 3 +- sphinx/ext/autodoc/typehints.py | 9 +- 7 files changed, 232 insertions(+), 126 deletions(-) (limited to 'sphinx/ext/autodoc') 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) -- cgit v1.2.3