from __future__ import annotations
import warnings
from typing import TYPE_CHECKING
from docutils.utils import assemble_option_dict
from sphinx.deprecation import RemovedInSphinx11Warning
from sphinx.ext.autodoc._sentinels import ALL, EMPTY, SUPPRESS
from sphinx.locale import __
if TYPE_CHECKING:
from collections.abc import Iterable, Iterator, Mapping, Set
from typing import Any, Final, Literal, Self
from sphinx.ext.autodoc._property_types import _AutodocObjType
from sphinx.ext.autodoc._sentinels import ALL_T, EMPTY_T, SUPPRESS_T
from sphinx.util.typing import OptionSpec
# common option names for autodoc directives
AUTODOC_DEFAULT_OPTIONS = (
'members',
'undoc-members',
'no-index',
'no-index-entry',
'inherited-members',
'show-inheritance',
'private-members',
'special-members',
'ignore-module-all',
'exclude-members',
'member-order',
'imported-members',
'class-doc-from',
'no-value',
)
AUTODOC_EXTENDABLE_OPTIONS = frozenset({
'members',
'private-members',
'special-members',
'exclude-members',
})
class _AutoDocumenterOptions:
# TODO: make immutable.
no_index: Literal[True] | None = None
no_index_entry: Literal[True] | None = None
_tab_width: int = 8
# module-like options
members: ALL_T | list[str] | None = None
undoc_members: Literal[True] | None = None
inherited_members: Set[str] | None = None
show_inheritance: Literal[True] | None = None
synopsis: str | None = None
platform: str | None = None
deprecated: Literal[True] | None = None
member_order: Literal['alphabetical', 'bysource', 'groupwise'] | None = None
exclude_members: EMPTY_T | set[str] | None = None
private_members: ALL_T | list[str] | None = None
special_members: ALL_T | list[str] | None = None
imported_members: Literal[True] | None = None
ignore_module_all: Literal[True] | None = None
no_value: Literal[True] | None = None
# class-like options (class, exception)
class_doc_from: Literal['both', 'class', 'init'] | None = None
# assignment-like (data, attribute)
annotation: SUPPRESS_T | str | None = None
noindex: Literal[True] | None = None
def __init__(self, **kwargs: Any) -> None:
vars(self).update(kwargs)
def __repr__(self) -> str:
args = ', '.join(f'{k}={v!r}' for k, v in vars(self).items())
return f'_AutoDocumenterOptions({args})'
def __getattr__(self, name: str) -> object:
return None # return None for missing attributes
def copy(self) -> Self:
return self.__class__(**vars(self))
@classmethod
def from_directive_options(cls, opts: Mapping[str, Any], /) -> Self:
return cls(**{k.replace('-', '_'): v for k, v in opts.items() if v is not None})
# Mapping interface:
def __getitem__(self, item: str) -> Any:
warnings.warn(
'The mapping interface for autodoc options objects is deprecated, '
'and will be removed in Sphinx 11. Use attribute access instead.',
RemovedInSphinx11Warning,
stacklevel=2,
)
try:
return getattr(self, item)
except AttributeError:
raise KeyError(item) from None
def __setitem__(self, key: str, value: Any) -> None:
msg = f'{self.__class__.__name__!r} object does not support indexed assignment'
raise TypeError(msg)
def __delitem__(self, key: str) -> None:
msg = f'{self.__class__.__name__!r} object does not support indexed deletion'
raise TypeError(msg)
def __contains__(self, item: str) -> bool:
warnings.warn(
'The mapping interface for autodoc options objects is deprecated, '
'and will be removed in Sphinx 11. Use attribute access instead.',
RemovedInSphinx11Warning,
stacklevel=2,
)
return hasattr(self, item)
def __keys(self) -> list[str]:
return [key for key in dir(self) if not key.startswith('_')]
def __iter__(self) -> Iterator[str]:
warnings.warn(
'The mapping interface for autodoc options objects is deprecated, '
'and will be removed in Sphinx 11. Use attribute access instead.',
RemovedInSphinx11Warning,
stacklevel=2,
)
yield from self.__keys()
def __len__(self) -> int:
warnings.warn(
'The mapping interface for autodoc options objects is deprecated, '
'and will be removed in Sphinx 11. Use attribute access instead.',
RemovedInSphinx11Warning,
stacklevel=2,
)
return len(self.__keys())
def keys(self) -> Iterable[str]:
warnings.warn(
'The mapping interface for autodoc options objects is deprecated, '
'and will be removed in Sphinx 11. Use attribute access instead.',
RemovedInSphinx11Warning,
stacklevel=2,
)
yield from self.__keys()
def items(self) -> Iterable[tuple[str, Any]]:
warnings.warn(
'The mapping interface for autodoc options objects is deprecated, '
'and will be removed in Sphinx 11. Use attribute access instead.',
RemovedInSphinx11Warning,
stacklevel=2,
)
for key in self.__keys():
yield key, getattr(self, key)
def values(self) -> Iterable[Any]:
warnings.warn(
'The mapping interface for autodoc options objects is deprecated, '
'and will be removed in Sphinx 11. Use attribute access instead.',
RemovedInSphinx11Warning,
stacklevel=2,
)
for key in self.__keys():
yield getattr(self, key)
def get(self, key: str, default: Any | None = None) -> Any | None:
warnings.warn(
'The mapping interface for autodoc options objects is deprecated, '
'and will be removed in Sphinx 11. Use attribute access instead.',
RemovedInSphinx11Warning,
stacklevel=2,
)
try:
return getattr(self, key)
except AttributeError:
return default
def identity(x: Any) -> Any:
return x
def members_option(arg: str | None) -> ALL_T | list[str] | None:
"""Used to convert the :members: option to auto directives."""
if arg is None or arg is True:
return ALL
if arg is False:
return None
return [stripped for x in arg.split(',') if (stripped := x.strip())]
def exclude_members_option(arg: str | None) -> EMPTY_T | set[str]:
"""Used to convert the :exclude-members: option."""
if arg is None or arg is True:
return EMPTY
return {stripped for x in arg.split(',') if (stripped := x.strip())}
def inherited_members_option(arg: str | None) -> set[str]:
"""Used to convert the :inherited-members: option to auto directives."""
if arg is None or arg is True:
return {'object'}
if arg:
return {x.strip() for x in arg.split(',')}
return set()
def member_order_option(
arg: str | None,
) -> Literal['alphabetical', 'bysource', 'groupwise'] | None:
"""Used to convert the :member-order: option to auto directives."""
if arg is None or arg is True:
return None
if arg in {'alphabetical', 'bysource', 'groupwise'}:
return arg # type: ignore[return-value]
raise ValueError(__('invalid value for member-order option: %s') % arg)
def class_doc_from_option(arg: str | None) -> Literal['both', 'class', 'init']:
"""Used to convert the :class-doc-from: option to autoclass directives."""
if arg in {'both', 'class', 'init'}:
return arg # type: ignore[return-value]
raise ValueError(__('invalid value for class-doc-from option: %s') % arg)
def annotation_option(arg: str | None) -> SUPPRESS_T | str | Literal[False]:
if arg is None or arg is True:
# suppress showing the representation of the object
return SUPPRESS
return arg
def bool_option(arg: str | None) -> bool:
"""Used to convert flag options to auto directives. (Instead of
directives.flag(), which returns None).
"""
return True
def merge_members_option(options: dict[str, Any]) -> None:
"""Merge :private-members: and :special-members: options to the
:members: option.
"""
if options.get('members') is ALL:
# merging is not needed when members: ALL
return
members = options.setdefault('members', [])
for key in ('private-members', 'special-members'):
other_members = options.get(key)
if other_members is not None and other_members is not ALL:
for member in other_members:
if member not in members:
members.append(member)
_OPTION_SPEC_COMMON: Final[OptionSpec] = {
'no-index': bool_option,
'no-index-entry': bool_option,
}
_OPTION_SPEC_HAS_MEMBERS: Final[OptionSpec] = _OPTION_SPEC_COMMON | {
'members': members_option,
'exclude-members': exclude_members_option,
'undoc-members': bool_option,
'private-members': members_option,
'special-members': members_option,
'member-order': member_order_option,
}
_OPTION_SPEC_MODULE_SPECIFIC: Final[OptionSpec] = {
'ignore-module-all': bool_option,
'imported-members': bool_option,
'deprecated': bool_option,
'platform': identity,
'synopsis': identity,
}
_OPTION_SPEC_CLASS_SPECIFIC: Final[OptionSpec] = {
'class-doc-from': class_doc_from_option,
'show-inheritance': bool_option,
'inherited-members': inherited_members_option,
}
_OPTION_SPEC_ASSIGNMENT: Final[OptionSpec] = _OPTION_SPEC_COMMON | {
'annotation': annotation_option,
'no-value': bool_option,
}
_OPTION_SPEC_DEPRECATED: Final[OptionSpec] = {
'noindex': bool_option,
}
_OPTION_SPEC_FUNCTION_DEF: Final = _OPTION_SPEC_COMMON | _OPTION_SPEC_DEPRECATED
_OPTION_SPECS: Final[Mapping[_AutodocObjType, OptionSpec]] = {
'module': _OPTION_SPEC_HAS_MEMBERS
| _OPTION_SPEC_MODULE_SPECIFIC
| {'show-inheritance': bool_option} # special case
| {'inherited-members': inherited_members_option} # special case
| {'no-value': bool_option} # special case
| _OPTION_SPEC_DEPRECATED,
'class': _OPTION_SPEC_HAS_MEMBERS
| _OPTION_SPEC_CLASS_SPECIFIC
| _OPTION_SPEC_DEPRECATED,
'exception': _OPTION_SPEC_HAS_MEMBERS
| _OPTION_SPEC_CLASS_SPECIFIC
| _OPTION_SPEC_DEPRECATED,
'function': _OPTION_SPEC_FUNCTION_DEF,
'decorator': _OPTION_SPEC_FUNCTION_DEF,
'method': _OPTION_SPEC_FUNCTION_DEF,
'property': _OPTION_SPEC_FUNCTION_DEF,
'attribute': _OPTION_SPEC_ASSIGNMENT | _OPTION_SPEC_DEPRECATED,
'data': _OPTION_SPEC_ASSIGNMENT | _OPTION_SPEC_DEPRECATED,
'type': _OPTION_SPEC_ASSIGNMENT,
}
def _process_documenter_options(
*,
obj_type: _AutodocObjType,
default_options: Mapping[str, str | bool],
options: dict[str, str | None],
) -> _AutoDocumenterOptions:
"""Recognize options of object type from user input."""
option_spec = _OPTION_SPECS[obj_type]
for name in AUTODOC_DEFAULT_OPTIONS:
if name not in option_spec:
continue
negated = options.pop(f'no-{name}', True) is None
if name in default_options and not negated:
if name in options and isinstance(default_options[name], str):
# take value from options if present or extend it
# with autodoc_default_options if necessary
if name in AUTODOC_EXTENDABLE_OPTIONS:
opt_value = options[name]
if opt_value is not None and opt_value.startswith('+'):
options[name] = f'{default_options[name]},{opt_value[1:]}'
else:
options[name] = default_options[name] # type: ignore[assignment]
elif (opt_value := options.get(name)) is not None:
# remove '+' from option argument if there's nothing to merge it with
options[name] = opt_value.removeprefix('+')
opts = assemble_option_dict(options.items(), option_spec) # type: ignore[arg-type]
return _AutoDocumenterOptions.from_directive_options(opts)