diff --git a/build/pkgs/roman_numerals_py/SPKG.rst b/build/pkgs/roman_numerals_py/SPKG.rst new file mode 100644 index 00000000000..b7ed999c753 --- /dev/null +++ b/build/pkgs/roman_numerals_py/SPKG.rst @@ -0,0 +1,26 @@ +Roman-numerals-py: Manipulate well-formed Roman numerals +======================================================== + +Description +----------- + +A library for manipulating well-formed Roman numerals. + +Integers between 1 and 3,999 (inclusive) are supported. Numbers beyond +this range will return an OutOfRangeError. + +The classical system of roman numerals requires that the same character +may not appear more than thrice consecutively, meaning that ‘MMMCMXCIX’ +(3,999) is the largest well-formed Roman numeral. The smallest is ‘I’ (1), +as there is no symbol for zero in Roman numerals. + +License +------- + +This project is licenced under the terms of either the Zero-Clause +BSD licence or the CC0 1.0 Universal licence + +Upstream Contact +---------------- + +https://pypi.org/project/roman-numerals-py/ diff --git a/build/pkgs/roman_numerals_py/checksums.ini b/build/pkgs/roman_numerals_py/checksums.ini new file mode 100644 index 00000000000..d75dc72c4cd --- /dev/null +++ b/build/pkgs/roman_numerals_py/checksums.ini @@ -0,0 +1,4 @@ +tarball=roman_numerals_py-VERSION-py3-none-any.whl +sha1=1d5e23a0ba8e244bf800597eaef15741d4cfeb0b +sha256=9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c +upstream_url=https://files.pythonhosted.org/packages/py3/r/roman_numerals_py/roman_numerals_py-VERSION-py3-none-any.whl diff --git a/build/pkgs/roman_numerals_py/dependencies b/build/pkgs/roman_numerals_py/dependencies new file mode 100644 index 00000000000..47296a7bace --- /dev/null +++ b/build/pkgs/roman_numerals_py/dependencies @@ -0,0 +1,4 @@ + | $(PYTHON_TOOLCHAIN) $(PYTHON) + +---------- +All lines of this file are ignored except the first. diff --git a/build/pkgs/roman_numerals_py/distros/arch.txt b/build/pkgs/roman_numerals_py/distros/arch.txt new file mode 100644 index 00000000000..b2ef9701eaa --- /dev/null +++ b/build/pkgs/roman_numerals_py/distros/arch.txt @@ -0,0 +1 @@ +python-roman-numerals-py diff --git a/build/pkgs/roman_numerals_py/distros/conda.txt b/build/pkgs/roman_numerals_py/distros/conda.txt new file mode 100644 index 00000000000..d303e56002e --- /dev/null +++ b/build/pkgs/roman_numerals_py/distros/conda.txt @@ -0,0 +1 @@ +roman-numerals-py diff --git a/build/pkgs/roman_numerals_py/distros/fedora.txt b/build/pkgs/roman_numerals_py/distros/fedora.txt new file mode 100644 index 00000000000..b2ef9701eaa --- /dev/null +++ b/build/pkgs/roman_numerals_py/distros/fedora.txt @@ -0,0 +1 @@ +python-roman-numerals-py diff --git a/build/pkgs/roman_numerals_py/distros/freebsd.txt b/build/pkgs/roman_numerals_py/distros/freebsd.txt new file mode 100644 index 00000000000..ecf05970180 --- /dev/null +++ b/build/pkgs/roman_numerals_py/distros/freebsd.txt @@ -0,0 +1 @@ +textproc/py-roman-numerals-py diff --git a/build/pkgs/roman_numerals_py/distros/gentoo.txt b/build/pkgs/roman_numerals_py/distros/gentoo.txt new file mode 100644 index 00000000000..3d486edbbff --- /dev/null +++ b/build/pkgs/roman_numerals_py/distros/gentoo.txt @@ -0,0 +1 @@ +dev-python/roman-numerals-py diff --git a/build/pkgs/roman_numerals_py/distros/macports.txt b/build/pkgs/roman_numerals_py/distros/macports.txt new file mode 100644 index 00000000000..55992ca1dfa --- /dev/null +++ b/build/pkgs/roman_numerals_py/distros/macports.txt @@ -0,0 +1 @@ +py-roman-_numerals_py diff --git a/build/pkgs/roman_numerals_py/distros/opensuse.txt b/build/pkgs/roman_numerals_py/distros/opensuse.txt new file mode 100644 index 00000000000..15821954064 --- /dev/null +++ b/build/pkgs/roman_numerals_py/distros/opensuse.txt @@ -0,0 +1 @@ +python-roman-numerals diff --git a/build/pkgs/roman_numerals_py/package-version.txt b/build/pkgs/roman_numerals_py/package-version.txt new file mode 100644 index 00000000000..fd2a01863fd --- /dev/null +++ b/build/pkgs/roman_numerals_py/package-version.txt @@ -0,0 +1 @@ +3.1.0 diff --git a/build/pkgs/roman_numerals_py/spkg-configure.m4 b/build/pkgs/roman_numerals_py/spkg-configure.m4 new file mode 100644 index 00000000000..303ca75e7bb --- /dev/null +++ b/build/pkgs/roman_numerals_py/spkg-configure.m4 @@ -0,0 +1,3 @@ +SAGE_SPKG_CONFIGURE([roman-numericals-py], [ + SAGE_PYTHON_PACKAGE_CHECK([roman-numericals-py]) +]) diff --git a/build/pkgs/roman_numerals_py/type b/build/pkgs/roman_numerals_py/type new file mode 100644 index 00000000000..a6a7b9cd726 --- /dev/null +++ b/build/pkgs/roman_numerals_py/type @@ -0,0 +1 @@ +standard diff --git a/build/pkgs/roman_numerals_py/version_requirements.txt b/build/pkgs/roman_numerals_py/version_requirements.txt new file mode 100644 index 00000000000..d303e56002e --- /dev/null +++ b/build/pkgs/roman_numerals_py/version_requirements.txt @@ -0,0 +1 @@ +roman-numerals-py diff --git a/build/pkgs/sphinx/checksums.ini b/build/pkgs/sphinx/checksums.ini index d6d0de6b17a..25f987a30ea 100644 --- a/build/pkgs/sphinx/checksums.ini +++ b/build/pkgs/sphinx/checksums.ini @@ -1,4 +1,4 @@ tarball=sphinx-VERSION-py3-none-any.whl -sha1=67dc18611c44f712539585db41aaee4b0a7ec646 -sha256=09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2 +sha1=6c55f99af821939e006411bf4a6ea35ecf6fd5a0 +sha256=4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3 upstream_url=https://files.pythonhosted.org/packages/py3/s/sphinx/sphinx-VERSION-py3-none-any.whl diff --git a/build/pkgs/sphinx/dependencies b/build/pkgs/sphinx/dependencies index 9b7505c9aec..dd3e43e2d67 100644 --- a/build/pkgs/sphinx/dependencies +++ b/build/pkgs/sphinx/dependencies @@ -1,4 +1,4 @@ - | $(PYTHON_TOOLCHAIN) docutils jinja2 pygments snowballstemmer imagesize babel alabaster requests sphinxcontrib_websupport sphinxcontrib_applehelp sphinxcontrib_devhelp sphinxcontrib_htmlhelp sphinxcontrib_jsmath sphinxcontrib_qthelp sphinxcontrib_serializinghtml packaging importlib_metadata $(PYTHON) + | $(PYTHON_TOOLCHAIN) docutils jinja2 pygments snowballstemmer imagesize babel alabaster requests roman_numerals_py sphinxcontrib_websupport sphinxcontrib_applehelp sphinxcontrib_devhelp sphinxcontrib_htmlhelp sphinxcontrib_jsmath sphinxcontrib_qthelp sphinxcontrib_serializinghtml packaging importlib_metadata $(PYTHON) ---------- All lines of this file are ignored except the first. diff --git a/build/pkgs/sphinx/package-version.txt b/build/pkgs/sphinx/package-version.txt index 406b8982770..1365b9236b1 100644 --- a/build/pkgs/sphinx/package-version.txt +++ b/build/pkgs/sphinx/package-version.txt @@ -1 +1 @@ -8.1.3 +8.2.3 diff --git a/build/pkgs/sphinx/version_requirements.txt b/build/pkgs/sphinx/version_requirements.txt index d311e5262f2..39f7ccd4230 100644 --- a/build/pkgs/sphinx/version_requirements.txt +++ b/build/pkgs/sphinx/version_requirements.txt @@ -1 +1 @@ -sphinx >=7.4.7, <9 +sphinx >=8.2.0, <9 diff --git a/src/sage_docbuild/ext/sage_autodoc.py b/src/sage_docbuild/ext/sage_autodoc.py index d62ff8b87d1..c62cddaf486 100644 --- a/src/sage_docbuild/ext/sage_autodoc.py +++ b/src/sage_docbuild/ext/sage_autodoc.py @@ -39,6 +39,8 @@ - François Bissey (2024-11-12): rebased on Sphinx 8.1.3 (while trying to keep python 3.9 compatibility) - François Bissey (2025-02-24): Remove python 3.9 support hacks, making us closer to upstream + +- François Bissey (2025-03-18): rebased on sphinx 8.2.3 """ from __future__ import annotations @@ -46,13 +48,14 @@ import functools import operator import re +import sys from inspect import Parameter, Signature from typing import TYPE_CHECKING, Any, NewType, TypeVar from docutils.statemachine import StringList import sphinx -from sphinx.config import ENUM, Config +from sphinx.config import ENUM from sphinx.errors import PycodeError from sphinx.ext.autodoc.importer import get_class_members, import_module, import_object from sphinx.ext.autodoc.mock import ismock, mock, undecorate @@ -67,13 +70,7 @@ safe_getattr, stringify_signature, ) -from sphinx.util.typing import ( - ExtensionMetadata, - OptionSpec, - get_type_hints, - restify, - stringify_annotation, -) +from sphinx.util.typing import get_type_hints, restify, stringify_annotation # ------------------------------------------------------------------ from sage.misc.sageinspect import (sage_getdoc_original, @@ -96,8 +93,12 @@ def getdoc(obj, *args, **kwargs): from typing import ClassVar, Literal, TypeAlias from sphinx.application import Sphinx - from sphinx.environment import BuildEnvironment + 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 ExtensionMetadata, OptionSpec, _RestifyMode _AutodocObjType = Literal[ 'module', 'class', 'exception', 'function', 'method', 'attribute' @@ -117,26 +118,35 @@ def getdoc(obj, *args, **kwargs): # As of Sphinx 7.2.6, Sphinx is confused with unquoted "::" in the comment # below. # ------------------------------------------------------------------------ - #: extended signature RE: with explicit module name separated by "::" py_ext_sig_re = re.compile( - r'''^ ([\w.]+::)? # explicit module name + r"""^ ([\w.]+::)? # explicit module name ([\w.]+\.)? # module and/or class name(s) (\w+) \s* # thing name - (?: \[\s*(.*)\s*])? # optional: type parameters list + (?: \[\s*(.*?)\s*])? # optional: type parameters list (?: \((.*)\) # optional: arguments (?:\s* -> \s* (.*))? # return annotation )? $ # and nothing more - ''', re.VERBOSE) + """, + 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' + + def identity(x: Any) -> Any: return x class _All: - r"""A special value for :\*-members: that matches to any member.""" + """A special value for :*-members: that matches to any member.""" def __contains__(self, item: Any) -> bool: return True @@ -161,7 +171,7 @@ def __contains__(self, item: Any) -> bool: def members_option(arg: Any) -> object | list[str]: """Used to convert the :members: option to auto directives.""" - if arg in (None, True): + if arg in {None, True}: return ALL elif arg is False: return None @@ -171,14 +181,14 @@ def members_option(arg: Any) -> object | list[str]: def exclude_members_option(arg: Any) -> object | set[str]: """Used to convert the :exclude-members: option.""" - if arg in (None, True): + 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): + if arg in {None, True}: return {'object'} elif arg: return {x.strip() for x in arg.split(',')} @@ -188,9 +198,9 @@ def inherited_members_option(arg: Any) -> set[str]: def member_order_option(arg: Any) -> str | None: """Used to convert the :member-order: option to auto directives.""" - if arg in (None, True): + if arg in {None, True}: return None - elif arg in ('alphabetical', 'bysource', 'groupwise'): + elif arg in {'alphabetical', 'bysource', 'groupwise'}: return arg else: raise ValueError(__('invalid value for member-order option: %s') % arg) @@ -198,7 +208,7 @@ def member_order_option(arg: Any) -> str | None: 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'): + if arg in {'both', 'class', 'init'}: return arg else: raise ValueError(__('invalid value for class-doc-from option: %s') % arg) @@ -208,7 +218,7 @@ def class_doc_from_option(arg: Any) -> str | None: def annotation_option(arg: Any) -> Any: - if arg in (None, True): + if arg in {None, True}: # suppress showing the representation of the object return SUPPRESS else: @@ -222,7 +232,7 @@ def bool_option(arg: Any) -> bool: return True -def merge_members_option(options: dict) -> None: +def merge_members_option(options: dict[str, Any]) -> None: """Merge :private-members: and :special-members: options to the :members: option. """ @@ -232,14 +242,16 @@ def merge_members_option(options: dict) -> None: members = options.setdefault('members', []) for key in ('private-members', 'special-members'): - if key in options and options[key] not in (ALL, None): - for member in options[key]: + 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) # Some useful event listener factories for autodoc-process-docstring. + def cut_lines( pre: int, post: int = 0, what: Sequence[str] | None = None ) -> _AutodocProcessDocstringListener: @@ -250,6 +262,7 @@ def cut_lines( Use like this (e.g. in the ``setup()`` function of :file:`conf.py`):: from sphinx.ext.autodoc import cut_lines + app.connect('autodoc-process-docstring', cut_lines(4, what={'module'})) This can (and should) be used in place of ``automodule_skip_lines``. @@ -293,6 +306,7 @@ def process( # make sure there is a blank line at the end if lines and lines[-1]: lines.append('') + return process @@ -339,12 +353,14 @@ def process( # make sure there is a blank line at the end if lines and lines[-1]: lines.append('') + return process # This class is used only in ``sphinx.ext.autodoc.directive``, -# But we define this class here to keep compatibility (see #4538) -class Options(dict[str, Any]): +# But we define this class here to keep compatibility +# See: https://github.com/sphinx-doc/sphinx/issues/4538 +class Options(dict[str, Any]): # NoQA: FURB189 """A dict/attribute hybrid that returns None on nonexisting keys.""" def copy(self) -> Options: @@ -364,18 +380,43 @@ class ObjectMember: represent each member of the object. """ - def __init__(self, name: str, obj: Any, *, docstring: str | None = None, - class_: Any = None, skipped: bool = False) -> None: + __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.skipped = skipped 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 + """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. @@ -403,25 +444,30 @@ class Documenter: 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(self.env.app, obj, name, *defargs) + 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, + 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: + 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 @@ -446,7 +492,7 @@ def __init__(self, directive: DocumenterBridge, name: str, indent: str = '') -> @property def documenters(self) -> dict[str, type[Documenter]]: """Returns registered Documenter classes""" - return self.env.app.registry.documenters + 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.""" @@ -455,8 +501,9 @@ def add_line(self, line: str, source: str, *lineno: int) -> None: 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]]: + 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. @@ -478,8 +525,12 @@ def parse_name(self) -> bool: # 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') + 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() @@ -500,8 +551,7 @@ def parse_name(self) -> bool: self.modname = modname self.args = args self.retann = retann - self.fullname = ((self.modname or '') + - ('.' + '.'.join(self.objpath) if self.objpath else '')) + self.fullname = '.'.join((self.modname or '', *self.objpath)) return True def import_object(self, raiseerror: bool = False) -> bool: @@ -513,8 +563,7 @@ def import_object(self, raiseerror: bool = False) -> bool: with mock(self.config.autodoc_mock_imports): try: ret = import_object( - self.modname, self.objpath, self.objtype, - attrgetter=self.get_attr, + self.modname, self.objpath, self.objtype, attrgetter=self.get_attr ) self.module, self.parent, self.object_name, self.object = ret if ismock(self.object): @@ -582,7 +631,7 @@ def format_signature(self, **kwargs: Any) -> str: """ if self.args is not None: # signature given explicitly - args = "(%s)" % self.args + args = f'({self.args})' retann = self.retann else: # try to introspect the signature @@ -595,13 +644,23 @@ def format_signature(self, **kwargs: Any) -> str: 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') + logger.warning( + __('error while formatting arguments for %s: %s'), + self.fullname, + exc, + type='autodoc', + ) args = None - result = self.env.events.emit_firstresult('autodoc-process-signature', - self.objtype, self.fullname, - self.object, self.options, args, retann) + result = self._events.emit_firstresult( + 'autodoc-process-signature', + self.objtype, + self.fullname, + self.object, + self.options, + args, + retann, + ) if result: args, retann = result @@ -619,14 +678,15 @@ def add_directive_header(self, sig: str) -> None: # 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) + for i, sig_line in enumerate(sig.split('\n')): + self.add_line(f'{prefix}{name}{sig_line}', sourcename) if i == 0: - prefix = " " * len(prefix) + 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 @@ -638,8 +698,13 @@ def get_doc(self) -> list[list[str]] | None: 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) + 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)] @@ -648,21 +713,27 @@ def get_doc(self) -> list[list[str]] | None: def process_doc(self, docstrings: list[list[str]]) -> Iterator[str]: """Let the user process the docstrings before adding them.""" for docstringlines in docstrings: - if self.env.app: + if self._events is not None: # let extensions preprocess docstrings - self.env.app.emit('autodoc-process-docstring', - self.objtype, self.fullname, self.object, - self.options, docstringlines) + self._events.emit( + 'autodoc-process-docstring', + self.objtype, + self.fullname, + self.object, + self.options, + docstringlines, + ) - if docstringlines and docstringlines[-1] != '': + if docstringlines and docstringlines[-1]: # append a blank line to the end of the docstring docstringlines.append('') yield from docstringlines def get_sourcename(self) -> str: - if (inspect.safe_getattr(self.object, '__module__', None) and - inspect.safe_getattr(self.object, '__qualname__', None)): + 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__}' @@ -723,8 +794,9 @@ def get_object_members(self, want_all: bool) -> tuple[bool, list[ObjectMember]]: msg = 'must be implemented in subclasses' raise NotImplementedError(msg) - def filter_members(self, members: list[ObjectMember], want_all: bool, - ) -> list[tuple[str, Any, bool]]: + def filter_members( + self, members: list[ObjectMember], want_all: bool + ) -> list[tuple[str, Any, bool]]: """Filter the given member list. Members are skipped if @@ -738,12 +810,22 @@ def filter_members(self, members: list[ObjectMember], want_all: bool, 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 cls.__name__ in inherited_members and cls != self.object: + 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__: @@ -782,8 +864,13 @@ def is_filtered_inherited_member(name: str, obj: Any) -> bool: 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) + 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 @@ -816,14 +903,18 @@ def is_filtered_inherited_member(name: str, obj: Any) -> bool: 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): + 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 ( + 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): @@ -852,8 +943,9 @@ def is_filtered_inherited_member(name: str, obj: Any) -> bool: else: keep = False else: - if (self.options.members is ALL and - is_filtered_inherited_member(membername, obj)): + 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 @@ -865,17 +957,30 @@ def is_filtered_inherited_member(name: str, obj: Any) -> bool: # give the user a chance to decide whether this member # should be skipped - if self.env.app: + if self._events is not None: # let extensions preprocess docstrings - skip_user = self.env.app.emit_firstresult( - 'autodoc-skip-member', self.objtype, membername, member, - not keep, self.options) + 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') + 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: @@ -890,21 +995,24 @@ def document_members(self, all_members: bool = False) -> None: *self.options.members*. """ # set current namespace for finding members - self.env.temp_data['autodoc:module'] = self.modname + self._current_document.autodoc_module = self.modname if self.objpath: - self.env.temp_data['autodoc:class'] = self.objpath[0] + self._current_document.autodoc_class = self.objpath[0] - want_all = (all_members or - self.options.inherited_members or - self.options.members is ALL) + want_all = ( + all_members or self.options.inherited_members or self.options.members is ALL + ) # find out which members are documentable members_check_module, members = self.get_object_members(want_all) # document non-skipped members - memberdocumenters: list[tuple[Documenter, bool]] = [] - for (mname, member, isattr) in self.filter_members(members, want_all): - classes = [cls for cls in self.documenters.values() - if cls.can_document_member(member, mname, isattr, self)] + 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 @@ -914,22 +1022,39 @@ def document_members(self, all_members: bool = False) -> None: # of inner classes can be documented full_mname = f'{self.modname}::' + '.'.join((*self.objpath, mname)) documenter = classes[-1](self.directive, full_mname, self.indent) - memberdocumenters.append((documenter, isattr)) + member_documenters.append((documenter, isattr)) member_order = self.options.member_order or self.config.autodoc_member_order - memberdocumenters = self.sort_members(memberdocumenters, member_order) - - for documenter, isattr in memberdocumenters: - documenter.generate( - all_members=True, real_modname=self.real_modname, - check_module=members_check_module and not isattr) + # 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.env.temp_data['autodoc:module'] = None - self.env.temp_data['autodoc:class'] = None + 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]]: + 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 @@ -944,6 +1069,7 @@ def sort_members(self, documenters: list[tuple[Documenter, bool]], 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) @@ -968,16 +1094,29 @@ def generate( 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') + __( + "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. @@ -1010,10 +1149,16 @@ def generate( except PycodeError: pass - docstrings: list[str] = functools.reduce(operator.iadd, 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') + 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: @@ -1031,8 +1176,12 @@ def generate( try: sig = self.format_signature() except Exception as exc: - logger.warning(__('error while formatting signature for %s: %s'), - self.fullname, exc, type='autodoc') + logger.warning( + __('error while formatting signature for %s: %s'), + self.fullname, + exc, + type='autodoc', + ) return # generate the directive header and options, if applicable @@ -1050,22 +1199,28 @@ def generate( class ModuleDocumenter(Documenter): - """ - Specialized Documenter subclass for modules. - """ + """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, '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, + '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, } @@ -1086,24 +1241,28 @@ def add_content(self, more_content: StringList | None) -> None: @classmethod def can_document_member( - cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any, + 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]]: + 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') + 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') + 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: @@ -1114,9 +1273,15 @@ def import_object(self, raiseerror: bool = False) -> bool: 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') + 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 @@ -1132,6 +1297,8 @@ def add_directive_header(self, sig: str) -> None: 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.""" @@ -1147,7 +1314,9 @@ def get_module_members(self) -> dict[str, ObjectMember]: if ismock(value): value = undecorate(value) docstring = attr_docs.get(('', name), []) - members[name] = ObjectMember(name, value, docstring="\n".join(docstring)) + members[name] = ObjectMember( + name, value, docstring='\n'.join(docstring) + ) except AttributeError: continue @@ -1155,8 +1324,9 @@ def get_module_members(self) -> dict[str, ObjectMember]: 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)) + members[name] = ObjectMember( + name, INSTANCEATTR, docstring='\n'.join(docstring) + ) return members @@ -1180,15 +1350,20 @@ def get_object_members(self, want_all: bool) -> tuple[bool, list[ObjectMember]]: 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') + 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]]: + 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__ @@ -1205,6 +1380,7 @@ def keyfunc(entry: tuple[Documenter, bool]) -> int: return module_all.index(name) else: return module_all_len + documenters.sort(key=keyfunc) return documenters @@ -1213,13 +1389,13 @@ def keyfunc(entry: tuple[Documenter, bool]) -> int: class ModuleLevelDocumenter(Documenter): - """ - Specialized Documenter subclass for objects on module level (functions, + """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]]: + 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: @@ -1228,7 +1404,7 @@ def resolve_name(self, modname: str | None, parents: Any, path: str, base: str, # if documenting a toplevel object without explicit module, # it can be contained in another auto directive ... - modname = self.env.temp_data.get('autodoc:module') + 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') @@ -1237,13 +1413,13 @@ def resolve_name(self, modname: str | None, parents: Any, path: str, base: str, class ClassLevelDocumenter(Documenter): - """ - Specialized Documenter subclass for objects on class level (methods, + """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]]: + 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] @@ -1253,19 +1429,18 @@ def resolve_name(self, modname: str | None, parents: Any, path: str, base: str, # if documenting a class-level object without path, # there must be a current class, either from a parent # auto directive ... - mod_cls_ = self.env.temp_data.get('autodoc:class') + mod_cls = self._current_document.autodoc_class # ... or from a class directive - if mod_cls_ is None: - mod_cls_ = self.env.ref_context.get('py:class') - # ... if still None, there's no way to know - if mod_cls_ is None: + 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, [] - mod_cls = mod_cls_ modname, sep, cls = mod_cls.rpartition('.') parents = [cls] # if the module name is still missing, get it like above if not modname: - modname = self.env.temp_data.get('autodoc:module') + modname = self._current_document.autodoc_module if not modname: modname = self.env.ref_context.get('py:module') # ... else, it stays None, which means invalid @@ -1273,8 +1448,7 @@ def resolve_name(self, modname: str | None, parents: Any, path: str, base: str, class DocstringSignatureMixin: - """ - Mixin for FunctionDocumenter and MethodDocumenter to provide the + """Mixin for FunctionDocumenter and MethodDocumenter to provide the feature of reading the signature from the docstring. """ @@ -1317,15 +1491,16 @@ def _find_signature(self) -> tuple[str | None, str | None] | None: # 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) + 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}") + self._signatures.append(f'({args}) -> {retann}') if result is not None: # finish the loop when signature found @@ -1340,8 +1515,7 @@ def get_doc(self) -> list[list[str]] | None: 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] + 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() @@ -1349,22 +1523,18 @@ def format_signature(self, **kwargs: Any) -> str: 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 class DocstringStripSignatureMixin(DocstringSignatureMixin): - """ - Mixin for AttributeDocumenter to provide the + """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] - ): + 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() @@ -1377,16 +1547,14 @@ def format_signature(self, **kwargs: Any) -> str: class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: ignore[misc] - """ - Specialized Documenter subclass for functions. - """ + """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, + cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any ) -> bool: # -------------------------------------------------------------------- # supports functions, builtins but, unlike Sphinx' autodoc, @@ -1405,13 +1573,15 @@ def can_document_member( # -------------------------------------------------------------------- def format_args(self, **kwargs: Any) -> str: - if self.config.autodoc_typehints in ('none', 'description'): + if self.config.autodoc_typehints in {'none', 'description'}: kwargs.setdefault('show_annotation', False) - if self.config.autodoc_typehints_format == "short": + 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.env.app.emit('autodoc-before-process-signature', self.object, False) + self._events.emit('autodoc-before-process-signature', self.object, False) # ---------------------------------------------------------------- # Issue #9976: Support the _sage_argspec_ attribute which makes it # possible to get argument specification of decorated callables in @@ -1438,8 +1608,9 @@ def format_args(self, **kwargs: Any) -> str: args = sage_formatargspec(*argspec) # ---------------------------------------------------------------- except TypeError as exc: - logger.warning(__("Failed to get a function signature for %s: %s"), - self.fullname, exc) + logger.warning( + __('Failed to get a function signature for %s: %s'), self.fullname, exc + ) return '' except ValueError: args = '' @@ -1456,17 +1627,23 @@ def add_directive_header(self, sig: str) -> None: sourcename = self.get_sourcename() super().add_directive_header(sig) - if inspect.iscoroutinefunction(self.object) or inspect.isasyncgenfunction(self.object): + 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": + 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'): + 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: @@ -1487,18 +1664,20 @@ def format_signature(self, **kwargs: Any) -> str: 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) + 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) + overload = evaluate_signature( + overload, __globals__, self.config.autodoc_type_aliases + ) sig = stringify_signature(overload, **kwargs) sigs.append(sig) - return "\n".join(sigs) + return '\n'.join(sigs) def merge_default_value(self, actual: Signature, overload: Signature) -> Signature: """Merge default values of actual implementation to the overload variants.""" @@ -1510,13 +1689,16 @@ def merge_default_value(self, actual: Signature, overload: Signature) -> Signatu return overload.replace(parameters=parameters) - def annotate_to_first_argument(self, func: Callable, typ: type) -> Callable | None: + 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) + logger.warning( + __('Failed to get a function signature for %s: %s'), self.fullname, exc + ) return None except ValueError: return None @@ -1531,8 +1713,7 @@ def dummy(): # type: ignore[no-untyped-def] # NoQA: ANN202 if params[0].annotation is Parameter.empty: params[0] = params[0].replace(annotation=typ) try: - dummy.__signature__ = sig.replace( # type: ignore[attr-defined] - parameters=params) + 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) @@ -1542,9 +1723,7 @@ def dummy(): # type: ignore[no-untyped-def] # NoQA: ANN202 class DecoratorDocumenter(FunctionDocumenter): - """ - Specialized Documenter subclass for decorator functions. - """ + """Specialized Documenter subclass for decorator functions.""" objtype = 'decorator' @@ -1562,30 +1741,33 @@ def format_args(self, **kwargs: Any) -> str: # Types which have confusing metaclass signatures it would be best not to show. # These are listed by name, rather than storing the objects themselves, to avoid # needing to import the modules. -_METACLASS_CALL_BLACKLIST = [ - 'enum.EnumMeta.__call__', -] +_METACLASS_CALL_BLACKLIST = frozenset({ + 'enum.EnumType.__call__', +}) # Types whose __new__ signature is a pass-through. -_CLASS_NEW_BLACKLIST = [ +_CLASS_NEW_BLACKLIST = frozenset({ 'typing.Generic.__new__', -] +}) class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: ignore[misc] - """ - Specialized Documenter subclass for classes. - """ + """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, 'inherited-members': inherited_members_option, - 'show-inheritance': bool_option, 'member-order': member_order_option, + '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, + 'private-members': members_option, + 'special-members': members_option, 'class-doc-from': class_doc_from_option, 'noindex': bool_option, } @@ -1615,10 +1797,11 @@ def __init__(self, *args: Any) -> None: @classmethod def can_document_member( - cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any, + cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any ) -> bool: return isinstance(member, type) or ( - isattr and isinstance(member, NewType | TypeVar)) + isattr and isinstance(member, NewType | TypeVar) + ) def import_object(self, raiseerror: bool = False) -> bool: ret = super().import_object(raiseerror) @@ -1626,7 +1809,7 @@ def import_object(self, raiseerror: bool = False) -> bool: # as data/attribute if ret: if hasattr(self.object, '__name__'): - self.doc_as_attr = (self.objpath[-1] != self.object.__name__) + self.doc_as_attr = self.objpath[-1] != self.object.__name__ # ------------------------------------------------------------------- # Issue #27692, #7448: The original goal of this was that if some # class is aliased, the alias is generated as a link rather than @@ -1694,7 +1877,7 @@ def import_object(self, raiseerror: bool = False) -> bool: 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('.') + bases = self.modname[len(modname) :].strip('.').split('.') self.objpath = bases + self.objpath self.modname = modname return ret @@ -1716,24 +1899,32 @@ def get_user_defined_function_or_method(obj: Any, attr: str) -> Any: # This sequence is copied from inspect._signature_from_callable. # ValueError means that no signature could be found, so we keep going. - # First, we check the obj has a __signature__ attribute - if (hasattr(self.object, '__signature__') and - isinstance(self.object.__signature__, Signature)): - return None, None, self.object.__signature__ + # 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: + if f'{call.__module__}.{call.__qualname__}' in _METACLASS_CALL_BLACKLIST: call = None if call is not None: - self.env.app.emit('autodoc-before-process-signature', call, True) + self._events.emit('autodoc-before-process-signature', call, True) try: - sig = inspect.signature(call, bound_method=True, - type_aliases=self.config.autodoc_type_aliases) + sig = inspect.signature( + call, + bound_method=True, + type_aliases=self.config.autodoc_type_aliases, + ) return type(self.object), '__call__', sig except ValueError: pass @@ -1742,14 +1933,17 @@ def get_user_defined_function_or_method(obj: Any, attr: str) -> Any: 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: + if f'{new.__module__}.{new.__qualname__}' in _CLASS_NEW_BLACKLIST: new = None if new is not None: - self.env.app.emit('autodoc-before-process-signature', new, True) + self._events.emit('autodoc-before-process-signature', new, True) try: - sig = inspect.signature(new, bound_method=True, - type_aliases=self.config.autodoc_type_aliases) + sig = inspect.signature( + new, + bound_method=True, + type_aliases=self.config.autodoc_type_aliases, + ) return self.object, '__new__', sig except ValueError: pass @@ -1757,10 +1951,13 @@ def get_user_defined_function_or_method(obj: Any, attr: str) -> Any: # Finally, we should have at least __init__ implemented init = get_user_defined_function_or_method(self.object, '__init__') if init is not None: - self.env.app.emit('autodoc-before-process-signature', init, True) + self._events.emit('autodoc-before-process-signature', init, True) try: - sig = inspect.signature(init, bound_method=True, - type_aliases=self.config.autodoc_type_aliases) + sig = inspect.signature( + init, + bound_method=True, + type_aliases=self.config.autodoc_type_aliases, + ) return self.object, '__init__', sig except ValueError: pass @@ -1769,10 +1966,13 @@ def get_user_defined_function_or_method(obj: Any, attr: str) -> Any: # handle it. # We don't know the exact method that inspect.signature will read # the signature from, so just pass the object itself to our hook. - self.env.app.emit('autodoc-before-process-signature', self.object, False) + 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) + sig = inspect.signature( + self.object, + bound_method=False, + type_aliases=self.config.autodoc_type_aliases, + ) return None, None, sig except ValueError: pass @@ -1782,17 +1982,22 @@ def get_user_defined_function_or_method(obj: Any, attr: str) -> Any: return None, None, None def format_args(self, **kwargs: Any) -> str: - if self.config.autodoc_typehints in ('none', 'description'): + if self.config.autodoc_typehints in {'none', 'description'}: kwargs.setdefault('show_annotation', False) - if self.config.autodoc_typehints_format == "short": + 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) + logger.warning( + __('Failed to get a constructor signature for %s: %s'), + self.fullname, + exc, + ) return '' self._signature_method_name = _signature_method_name or '' @@ -1822,8 +2027,10 @@ def format_signature(self, **kwargs: Any) -> str: # do not show signatures return '' - if self.config.autodoc_typehints_format == "short": + 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 = [] @@ -1831,21 +2038,25 @@ def format_signature(self, **kwargs: Any) -> str: 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) + 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) + 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) + 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) + return '\n'.join(sigs) def get_overloaded_signatures(self) -> list[Signature]: if self._signature_class and self._signature_method_name: @@ -1892,8 +2103,12 @@ def add_directive_header(self, sig: str) -> None: 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): + 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 @@ -1908,21 +2123,24 @@ def add_directive_header(self, sig: str) -> None: else: bases = [] - self.env.events.emit('autodoc-process-bases', - self.fullname, self.object, self.options, bases) + self._events.emit( + 'autodoc-process-bases', self.fullname, self.object, self.options, bases + ) - if self.config.autodoc_typehints_format == "short": - base_classes = [restify(cls, "smart") for cls in bases] - else: - base_classes = [restify(cls) for cls in bases] + 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) + 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, [] @@ -1932,8 +2150,12 @@ def get_object_members(self, want_all: bool) -> tuple[bool, list[ObjectMember]]: if name in members: selected.append(members[name]) else: - logger.warning(__('missing attribute %s in object %s'), - name, self.fullname, type='autodoc') + 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()) @@ -1955,7 +2177,9 @@ def get_doc(self) -> list[list[str]] | None: if lines is not None: return lines - classdoc_from = self.options.get('class-doc-from', self.config.autoclass_content) + classdoc_from = self.options.get( + 'class-doc-from', self.config.autoclass_content + ) docstrings = [] attrdocstring = getdoc(self.object, self.get_attr) @@ -1964,26 +2188,36 @@ def get_doc(self) -> list[list[str]] | None: # 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'): + 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__') + initdocstring = getdoc( + __init__, + self.get_attr, + self.config.autodoc_inherit_docstrings, + self.object, + '__init__', + ) # for new-style classes, no __init__ means default __init__ - if (initdocstring is not None and - (initdocstring == object.__init__.__doc__ or # for pypy - initdocstring.strip() == object.__init__.__doc__)): # for !pypy + 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__') + initdocstring = getdoc( + __new__, + self.get_attr, + self.config.autodoc_inherit_docstrings, + self.object, + '__new__', + ) # for new-style classes, no __new__ means default __new__ - if (initdocstring is not None and - (initdocstring == object.__new__.__doc__ or # for pypy - initdocstring.strip() == object.__new__.__doc__)): # for !pypy + 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': @@ -2007,34 +2241,29 @@ def get_variable_comment(self) -> list[str] | None: 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): - if self.config.autodoc_typehints_format == "short": - supertype = restify(self.object.__supertype__, "smart") - else: - supertype = restify(self.object.__supertype__) + 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__)] - for constraint in self.object.__constraints__: - if self.config.autodoc_typehints_format == "short": - attrs.append(stringify_annotation(constraint, "smart")) - else: - attrs.append(stringify_annotation(constraint)) + attrs.extend( + stringify_annotation(constraint, mode, short_literals=short_literals) + for constraint in self.object.__constraints__ + ) if self.object.__bound__: - if self.config.autodoc_typehints_format == "short": - bound = restify(self.object.__bound__, "smart") - else: - bound = restify(self.object.__bound__) - attrs.append(r"bound=\ " + bound) + bound = restify(self.object.__bound__, mode=mode) + attrs.append(r'bound=\ ' + bound) if self.object.__covariant__: - attrs.append("covariant=True") + attrs.append('covariant=True') if self.object.__contravariant__: - attrs.append("contravariant=True") + attrs.append('contravariant=True') more_content = StringList( - [_('alias of TypeVar(%s)') % ", ".join(attrs), ''], - source='', + [_('alias of TypeVar(%s)') % ', '.join(attrs), ''], source='' ) if self.doc_as_attr and self.modname != self.get_real_modname(): try: @@ -2046,10 +2275,7 @@ def add_content(self, more_content: StringList | None) -> None: if self.doc_as_attr and not self.get_variable_comment(): try: - if self.config.autodoc_typehints_format == "short": - alias = restify(self.object, "smart") - else: - alias = restify(self.object) + alias = restify(self.object, mode=mode) more_content = StringList([_('alias of %s') % alias], source='') except AttributeError: pass # Invalid class object is passed. @@ -2073,15 +2299,15 @@ def generate( # 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) + return super().generate( + more_content=more_content, + check_module=check_module, + all_members=all_members, + ) class ExceptionDocumenter(ClassDocumenter): - """ - Specialized ClassDocumenter subclass for exceptions. - """ + """Specialized ClassDocumenter subclass for exceptions.""" objtype = 'exception' member_order = 10 @@ -2091,7 +2317,7 @@ class ExceptionDocumenter(ClassDocumenter): @classmethod def can_document_member( - cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any, + cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any ) -> bool: try: return isinstance(member, type) and issubclass(member, BaseException) @@ -2129,21 +2355,20 @@ def update_content(self, more_content: StringList) -> None: class GenericAliasMixin(DataDocumenterMixinBase): - """ - Mixin for DataDocumenter and AttributeDocumenter to provide the feature for + """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()) + return ( + inspect.isgenericalias(self.object) + or super().should_suppress_directive_header() + ) def update_content(self, more_content: StringList) -> None: if inspect.isgenericalias(self.object): - if self.config.autodoc_typehints_format == "short": - alias = restify(self.object, "smart") - else: - alias = restify(self.object) + 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('', '') @@ -2152,8 +2377,7 @@ def update_content(self, more_content: StringList) -> None: class UninitializedGlobalVariableMixin(DataDocumenterMixinBase): - """ - Mixin for DataDocumenter to provide the feature for supporting uninitialized + """Mixin for DataDocumenter to provide the feature for supporting uninitialized (type annotation only) global variables. """ @@ -2165,9 +2389,12 @@ def import_object(self, raiseerror: bool = False) -> bool: 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) + 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 @@ -2182,8 +2409,9 @@ def import_object(self, raiseerror: bool = False) -> bool: return False def should_suppress_value_header(self) -> bool: - return (self.object is UNINITIALIZED_ATTR or - super().should_suppress_value_header()) + 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: @@ -2192,22 +2420,21 @@ def get_doc(self) -> list[list[str]] | None: return super().get_doc() # type: ignore[misc] -class DataDocumenter(GenericAliasMixin, - UninitializedGlobalVariableMixin, ModuleLevelDocumenter): - """ - Specialized Documenter subclass for data items. - """ +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 + 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, + cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any ) -> bool: return isinstance(parent, ModuleDocumenter) and isattr @@ -2220,7 +2447,7 @@ def update_annotations(self, parent: Any) -> None: analyzer = ModuleAnalyzer.for_module(self.modname) analyzer.analyze() for (classname, attrname), annotation in analyzer.annotations.items(): - if classname == '' and attrname not in annotations: + if not classname and attrname not in annotations: annotations[attrname] = annotation except PycodeError: pass @@ -2238,7 +2465,8 @@ def should_suppress_value_header(self) -> bool: else: doc = self.get_doc() or [] docstring, metadata = separate_metadata( - '\n'.join(functools.reduce(operator.iadd, doc, []))) + '\n'.join(functools.reduce(operator.iadd, doc, [])) + ) if 'hide-value' in metadata: return True @@ -2247,29 +2475,38 @@ def should_suppress_value_header(self) -> bool: 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(): + 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) + 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) + annotations = get_type_hints( + self.parent, + None, + self.config.autodoc_type_aliases, + include_extras=True, + ) if self.objpath[-1] in annotations: - if self.config.autodoc_typehints_format == "short": - objrepr = stringify_annotation(annotations.get(self.objpath[-1]), - "smart") - else: - objrepr = stringify_annotation(annotations.get(self.objpath[-1]), - "fully-qualified-except-typing") + 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)): + if ( + self.options.no_value + or self.should_suppress_value_header() + or ismock(self.object) + ): pass else: objrepr = object_description(self.object) @@ -2317,9 +2554,7 @@ def add_content(self, more_content: StringList | None) -> None: class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: ignore[misc] - """ - Specialized Documenter subclass for methods (normal, static and class). - """ + """Specialized Documenter subclass for methods (normal, static and class).""" objtype = 'method' directivetype = 'method' @@ -2328,7 +2563,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: @classmethod def can_document_member( - cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any, + cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any ) -> bool: return inspect.isroutine(member) and not isinstance(parent, ModuleDocumenter) @@ -2338,22 +2573,23 @@ def import_object(self, raiseerror: bool = False) -> bool: return ret # to distinguish classmethod/staticmethod - obj = self.parent.__dict__.get(self.object_name) - if obj is None: - obj = self.object - - if (inspect.isclassmethod(obj) or - inspect.isstaticmethod(obj, cls=self.parent, name=self.object_name)): - # document class and static members before ordinary ones - self.member_order = self.member_order - 1 - + 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 + elif inspect.isclassmethod(obj): + # document class methods before static methods as + # they usually behave as alternative constructors + self.member_order -= 2 return ret def format_args(self, **kwargs: Any) -> str: - if self.config.autodoc_typehints in ('none', 'description'): + if self.config.autodoc_typehints in {'none', 'description'}: kwargs.setdefault('show_annotation', False) - if self.config.autodoc_typehints_format == "short": + 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) # ----------------------------------------------------------------- # Issue #9976: Support the _sage_argspec_ attribute which makes it @@ -2396,8 +2632,11 @@ def add_directive_header(self, sig: str) -> None: self.add_line(' :abstractmethod:', sourcename) if inspect.iscoroutinefunction(obj) or inspect.isasyncgenfunction(obj): self.add_line(' :async:', sourcename) - if (inspect.isclassmethod(obj) or - inspect.is_singledispatch_method(obj) and inspect.isclassmethod(obj.func)): + 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) @@ -2423,13 +2662,16 @@ def merge_default_value(self, actual: Signature, overload: Signature) -> Signatu return overload.replace(parameters=parameters) - def annotate_to_first_argument(self, func: Callable, typ: type) -> Callable | None: + 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) + logger.warning( + __('Failed to get a method signature for %s: %s'), self.fullname, exc + ) return None except ValueError: return None @@ -2445,7 +2687,8 @@ def dummy(): # type: ignore[no-untyped-def] # NoQA: ANN202 params[1] = params[1].replace(annotation=typ) try: dummy.__signature__ = sig.replace( # type: ignore[attr-defined] - parameters=params) + parameters=params + ) return dummy except (AttributeError, TypeError): # failed to update signature (ex. built-in or extension types) @@ -2461,12 +2704,17 @@ def get_doc(self) -> list[list[str]] | None: # `DocstringSignatureMixin`. return self._new_docstrings if self.objpath[-1] == '__init__': - docstring = getdoc(self.object, self.get_attr, - self.config.autodoc_inherit_docstrings, - self.parent, self.object_name) - if (docstring is not None and - (docstring == object.__init__.__doc__ or # for pypy - docstring.strip() == object.__init__.__doc__)): # for !pypy + docstring = 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 @@ -2474,12 +2722,17 @@ def get_doc(self) -> list[list[str]] | None: else: return [] elif self.objpath[-1] == '__new__': - docstring = getdoc(self.object, self.get_attr, - self.config.autodoc_inherit_docstrings, - self.parent, self.object_name) - if (docstring is not None and - (docstring == object.__new__.__doc__ or # for pypy - docstring.strip() == object.__new__.__doc__)): # for !pypy + docstring = 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 @@ -2491,8 +2744,7 @@ def get_doc(self) -> list[list[str]] | None: class NonDataDescriptorMixin(DataDocumenterMixinBase): - """ - Mixin for AttributeDocumenter to provide the feature for supporting non + """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 @@ -2509,8 +2761,10 @@ def import_object(self, raiseerror: bool = False) -> bool: return ret def should_suppress_value_header(self) -> bool: - return (not getattr(self, 'non_data_descriptor', False) or - super().should_suppress_directive_header()) + 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): @@ -2522,9 +2776,7 @@ def get_doc(self) -> list[list[str]] | None: class SlotsMixin(DataDocumenterMixinBase): - """ - Mixin for AttributeDocumenter to provide the feature for supporting __slots__. - """ + """Mixin for AttributeDocumenter to provide the feature for supporting __slots__.""" def isslotsattribute(self) -> bool: """Check the subject is an attribute in __slots__.""" @@ -2553,25 +2805,29 @@ 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])): + 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') + 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 + """Mixin for AttributeDocumenter to provide the feature for supporting runtime instance attributes (that are defined in __init__() methods with doc-comments). - Example: + Example:: class Foo: def __init__(self): @@ -2615,7 +2871,9 @@ def import_object(self, raiseerror: bool = False) -> bool: try: with mock(self.config.autodoc_mock_imports): ret = import_object( - self.modname, self.objpath[:-1], 'class', + self.modname, + self.objpath[:-1], + 'class', attrgetter=self.get_attr, # type: ignore[attr-defined] ) parent = ret[3] @@ -2633,23 +2891,26 @@ def import_object(self, raiseerror: bool = False) -> bool: return False def should_suppress_value_header(self) -> bool: - return (self.object is self.RUNTIME_INSTANCE_ATTRIBUTE or - super().should_suppress_value_header()) + 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)): + 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 + """Mixin for AttributeDocumenter to provide the feature for supporting uninitialized instance attributes (PEP-526 styled, annotation only attributes). - Example: + Example:: class Foo: attr: int #: This is a target of this mix-in. @@ -2657,8 +2918,9 @@ class Foo: 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) + 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: @@ -2670,7 +2932,9 @@ def import_object(self, raiseerror: bool = False) -> bool: except ImportError as exc: try: ret = import_object( - self.modname, self.objpath[:-1], 'class', + self.modname, + self.objpath[:-1], + 'class', attrgetter=self.get_attr, # type: ignore[attr-defined] ) parent = ret[3] @@ -2688,8 +2952,9 @@ def import_object(self, raiseerror: bool = False) -> bool: return False def should_suppress_value_header(self) -> bool: - return (self.object is UNINITIALIZED_ATTR or - super().should_suppress_value_header()) + 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: @@ -2697,19 +2962,22 @@ def get_doc(self) -> list[list[str]] | None: return super().get_doc() # type: ignore[misc] -class AttributeDocumenter(GenericAliasMixin, SlotsMixin, # type: ignore[misc] - RuntimeInstanceAttributeMixin, - UninitializedInstanceAttributeMixin, NonDataDescriptorMixin, - DocstringStripSignatureMixin, ClassLevelDocumenter): - """ - Specialized Documenter subclass for attributes. - """ +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 + 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 @@ -2717,11 +2985,13 @@ class AttributeDocumenter(GenericAliasMixin, SlotsMixin, # type: ignore[misc] @staticmethod def is_function_or_method(obj: Any) -> bool: - return inspect.isfunction(obj) or inspect.isbuiltin(obj) or inspect.ismethod(obj) + 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, + cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any ) -> bool: if isinstance(parent, ModuleDocumenter): return False @@ -2765,7 +3035,8 @@ def update_annotations(self, parent: Any) -> None: analyzer = ModuleAnalyzer.for_module(module) analyzer.analyze() - for (classname, attrname), annotation in analyzer.annotations.items(): + anns = analyzer.annotations + for (classname, attrname), annotation in anns.items(): if classname == qualname and attrname not in annotations: annotations[attrname] = annotation except (AttributeError, PycodeError): @@ -2794,7 +3065,8 @@ def should_suppress_value_header(self) -> bool: doc = self.get_doc() if doc: docstring, metadata = separate_metadata( - '\n'.join(functools.reduce(operator.iadd, doc, []))) + '\n'.join(functools.reduce(operator.iadd, doc, [])) + ) if 'hide-value' in metadata: return True @@ -2803,28 +3075,38 @@ def should_suppress_value_header(self) -> bool: 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(): + 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) + annotations = get_type_hints( + self.parent, + None, + self.config.autodoc_type_aliases, + include_extras=True, + ) if self.objpath[-1] in annotations: - if self.config.autodoc_typehints_format == "short": - objrepr = stringify_annotation(annotations.get(self.objpath[-1]), - "smart") - else: - objrepr = stringify_annotation(annotations.get(self.objpath[-1]), - "fully-qualified-except-typing") + 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)): + if ( + self.options.no_value + or self.should_suppress_value_header() + or ismock(self.object) + ): pass else: objrepr = object_description(self.object) @@ -2858,7 +3140,7 @@ def get_doc(self) -> list[list[str]] | None: try: # Disable `autodoc_inherit_docstring` temporarily to avoid to obtain # a docstring from the value which descriptor returns unexpectedly. - # ref: https://github.com/sphinx-doc/sphinx/issues/7805 + # 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() @@ -2876,11 +3158,8 @@ def add_content(self, more_content: StringList | None) -> None: super().add_content(more_content) -class PropertyDocumenter(DocstringStripSignatureMixin, # type: ignore[misc] - ClassLevelDocumenter): - """ - Specialized Documenter subclass for properties. - """ +class PropertyDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): # type: ignore[misc] + """Specialized Documenter subclass for properties.""" objtype = 'property' member_order = 60 @@ -2890,7 +3169,7 @@ class PropertyDocumenter(DocstringStripSignatureMixin, # type: ignore[misc] @classmethod def can_document_member( - cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any, + cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any ) -> bool: if isinstance(parent, ClassDocumenter): if inspect.isproperty(member): @@ -2926,7 +3205,7 @@ def format_args(self, **kwargs: Any) -> str: return '' # update the annotations of the property getter - self.env.app.emit('autodoc-before-process-signature', func, False) + self._events.emit('autodoc-before-process-signature', func, False) # correctly format the arguments for a property return super().format_args(**kwargs) @@ -2950,23 +3229,25 @@ def add_directive_header(self, sig: str) -> None: return try: - signature = inspect.signature(func, - type_aliases=self.config.autodoc_type_aliases) + signature = inspect.signature( + func, type_aliases=self.config.autodoc_type_aliases + ) if signature.return_annotation is not Parameter.empty: - if self.config.autodoc_typehints_format == "short": - objrepr = stringify_annotation(signature.return_annotation, "smart") - else: - objrepr = stringify_annotation(signature.return_annotation, - "fully-qualified-except-typing") + 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) + logger.warning( + __('Failed to get a function signature for %s: %s'), self.fullname, exc + ) pass except ValueError: pass - def _get_property_getter(self) -> Callable | None: + 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 @@ -2974,9 +3255,11 @@ def _get_property_getter(self) -> Callable | None: return None -def autodoc_attrgetter(app: Sphinx, obj: Any, name: str, *defargs: Any) -> Any: +def autodoc_attrgetter( + obj: Any, name: str, *defargs: Any, registry: SphinxComponentRegistry +) -> Any: """Alternative getattr() for types""" - for typ, func in app.registry.autodoc_attrgetters.items(): + for typ, func in registry.autodoc_attrgetters.items(): if isinstance(obj, typ): return func(obj, name, *defargs) @@ -2994,22 +3277,54 @@ def setup(app: Sphinx) -> ExtensionMetadata: app.add_autodocumenter(AttributeDocumenter) app.add_autodocumenter(PropertyDocumenter) - 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', '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', 'env', - ENUM('all', 'documented', 'documented_params')) - 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, 'env') - app.add_config_value('autodoc_inherit_docstrings', True, 'env') + app.add_config_value( + 'autoclass_content', + 'class', + 'env', + types=ENUM('both', 'class', 'init'), + ) + app.add_config_value( + 'autodoc_member_order', + 'alphabetical', + 'env', + types=ENUM('alphabetical', 'bysource', 'groupwise'), + ) + app.add_config_value( + 'autodoc_class_signature', + 'mixed', + 'env', + types=ENUM('mixed', 'separated'), + ) + app.add_config_value('autodoc_default_options', {}, 'env', types=frozenset({dict})) + app.add_config_value( + 'autodoc_docstring_signature', True, 'env', types=frozenset({bool}) + ) + app.add_config_value( + 'autodoc_mock_imports', [], 'env', types=frozenset({list, tuple}) + ) + app.add_config_value( + 'autodoc_typehints', + 'signature', + 'env', + types=ENUM('signature', 'description', 'none', 'both'), + ) + app.add_config_value( + 'autodoc_typehints_description_target', + 'all', + 'env', + types=ENUM('all', 'documented', 'documented_params'), + ) + app.add_config_value('autodoc_type_aliases', {}, 'env', types=frozenset({dict})) + app.add_config_value( + 'autodoc_typehints_format', + 'short', + 'env', + types=ENUM('fully-qualified', 'short'), + ) + app.add_config_value('autodoc_warningiserror', True, 'env', types=frozenset({bool})) + app.add_config_value( + 'autodoc_inherit_docstrings', True, 'env', types=frozenset({bool}) + ) app.add_event('autodoc-before-process-signature') app.add_event('autodoc-process-docstring') app.add_event('autodoc-process-signature') @@ -3020,4 +3335,7 @@ def setup(app: Sphinx) -> ExtensionMetadata: app.setup_extension('sphinx.ext.autodoc.type_comment') app.setup_extension('sphinx.ext.autodoc.typehints') - return {'version': sphinx.__display_version__, 'parallel_read_safe': True} + return { + 'version': sphinx.__display_version__, + 'parallel_read_safe': True, + }