Extract `_generate_directives() from Documenter._generate()` (#13985)

This commit is contained in:
Adam Turner authored and GitHub committed 2025-10-22 00:23:22 +01:00
1 parent fdf5411c1c
commit 465b758a35
10 files changed
+530 -546

No files matched your search

@@ -4,7 +4,7 @@ from enum import IntEnum
from typing import TYPE_CHECKING
from sphinx.ext.autodoc import ClassDocumenter, bool_option
from sphinx.ext.autodoc._documenters import _docstring_source_name
from sphinx.ext.autodoc._generate import _docstring_source_name
if TYPE_CHECKING:
from typing import Any
+1 -1
View File
@@ -173,7 +173,7 @@ exclude = [
[tool.mypy]
files = [
"doc/conf.py",
"doc/development/tutorials/examples/autodoc_intenum.py",
# "doc/development/tutorials/examples/autodoc_intenum.py",
"doc/development/tutorials/examples/helloworld.py",
"sphinx",
"tests",
+1 -2
View File
@@ -77,7 +77,6 @@ def _process_docstrings(
*,
events: EventManager,
props: _ItemProperties,
obj: Any,
options: _AutoDocumenterOptions,
) -> Iterator[str]:
"""Let the user process the docstrings before adding them."""
@@ -89,7 +88,7 @@ def _process_docstrings(
'autodoc-process-docstring',
props.obj_type,
props.full_name,
obj,
props._obj,
options,
docstring_lines,
)
+3 -367
View File
@@ -1,11 +1,7 @@
from __future__ import annotations
import sys
from typing import TYPE_CHECKING, NewType, TypeVar
from typing import TYPE_CHECKING
from docutils.statemachine import StringList
from sphinx.errors import PycodeError
from sphinx.ext.autodoc._directive_options import (
annotation_option,
bool_option,
@@ -16,24 +12,10 @@ from sphinx.ext.autodoc._directive_options import (
member_order_option,
members_option,
)
from sphinx.ext.autodoc._docstrings import _prepare_docstrings, _process_docstrings
from sphinx.ext.autodoc._member_finder import _document_members
from sphinx.ext.autodoc._renderer import _add_content, _directive_header_lines
from sphinx.ext.autodoc.mock import ismock
from sphinx.locale import _, __
from sphinx.pycode import ModuleAnalyzer
from sphinx.util import inspect, logging
from sphinx.util.inspect import safe_getattr
from sphinx.util.typing import restify, stringify_annotation
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
from typing import Any, ClassVar, Final, Literal, NoReturn
from typing import ClassVar
from sphinx.config import Config
from sphinx.environment import BuildEnvironment, _CurrentDocument
from sphinx.events import EventManager
from sphinx.ext.autodoc._directive_options import _AutoDocumenterOptions
from sphinx.ext.autodoc._property_types import (
_AssignStatementProperties,
_ClassDefProperties,
@@ -42,18 +24,7 @@ if TYPE_CHECKING:
_ModuleProperties,
_TypeStatementProperties,
)
from sphinx.ext.autodoc.importer import _AttrGetter
from sphinx.util.typing import OptionSpec, _RestifyMode
logger = logging.getLogger('sphinx.ext.autodoc')
def _get_render_mode(
typehints_format: Literal['fully-qualified', 'short'],
) -> _RestifyMode:
if typehints_format == 'short':
return 'smart'
return 'fully-qualified-except-typing'
from sphinx.util.typing import OptionSpec
class Documenter:
@@ -85,295 +56,6 @@ class Documenter:
'noindex': bool_option,
}
def __init__(
self,
*,
env: BuildEnvironment,
options: _AutoDocumenterOptions,
get_attr: _AttrGetter,
record_dependencies: set[str] | None = None,
result: StringList | None = None,
indent: str = '',
) -> None:
self.config: Config = env.config
self.env: BuildEnvironment = env
self._current_document: _CurrentDocument = env.current_document
self._events: EventManager = env.events
self.options: _AutoDocumenterOptions = options
self.get_attr = get_attr
if record_dependencies is not None:
self.record_dependencies: set[str] = record_dependencies
else:
self.record_dependencies = set()
if result is not None:
self.result: StringList = result
else:
self.result = StringList()
self.indent: Final = indent
# the module analyzer to get at attribute docs, or None
self.analyzer: ModuleAnalyzer | None = None
def add_line(self, line: str, source: str, *lineno: int, indent: str) -> None:
"""Append one line of generated reST to the output."""
if line.strip(): # not a blank line
self.result.append(indent + line, source, *lineno)
else:
self.result.append('', source, *lineno)
def add_directive_header(self, *, indent: str) -> None:
"""Add the directive header and options to the generated content."""
domain_name = getattr(self, 'domain', 'py')
if self.objtype in {'class', 'exception'} and self.props.doc_as_attr: # type: ignore[attr-defined]
directive_name = 'attribute'
else:
directive_name = getattr(self, 'directivetype', self.objtype)
directive_name = f'{domain_name}:{directive_name}'
if self.analyzer:
is_final = self.props.dotted_parts in self.analyzer.finals
else:
is_final = False
result = self.result
analyzer_source = '' if self.analyzer is None else self.analyzer.srcname
source_name = _docstring_source_name(props=self.props, source=analyzer_source)
for line in _directive_header_lines(
autodoc_typehints=self.config.autodoc_typehints,
directive_name=directive_name,
is_final=is_final,
options=self.options,
props=self.props,
):
if line.strip(): # not a blank line
result.append(indent + line, source_name)
else:
result.append('', source_name)
def add_content(self, more_content: StringList | None, *, indent: str) -> None:
"""Add content from docstrings, attribute documentation and user."""
# add content from docstrings
attr_docs = {} if self.analyzer is None else self.analyzer.attr_docs
analyzer_source = '' if self.analyzer is None else self.analyzer.srcname
processed_doc = StringList(
list(
_process_docstrings(
_prepare_docstrings(props=self.props, attr_docs=attr_docs),
events=self._events,
props=self.props,
obj=self.props._obj,
options=self.options,
)
),
source=_docstring_source_name(props=self.props, source=analyzer_source),
)
_add_content(
processed_doc,
result=self.result,
indent=indent + ' ',
)
# add additional content (e.g. from document), if present
more_content = self._assemble_more_content(
more_content=StringList() if more_content is None else more_content,
typehints_format=self.config.autodoc_typehints_format,
python_display_short_literal_types=self.config.python_display_short_literal_types,
props=self.props,
)
_add_content(
more_content,
result=self.result,
indent=indent + ' ' * (self.props.obj_type != 'module'),
)
@staticmethod
def _assemble_more_content(
more_content: StringList,
*,
props: _ItemProperties,
typehints_format: Literal['fully-qualified', 'short'],
python_display_short_literal_types: bool,
) -> StringList:
"""Add content from docstrings, attribute documentation and user."""
obj = props._obj
if props.obj_type in {'data', 'attribute'}:
mode = _get_render_mode(typehints_format)
# Support for documenting GenericAliases
if inspect.isgenericalias(obj):
alias = restify(obj, mode=mode)
more_content.append(_('alias of %s') % alias, '')
more_content.append('', '')
return more_content
if props.obj_type in {'class', 'exception'}:
from sphinx.ext.autodoc._property_types import _ClassDefProperties
assert isinstance(props, _ClassDefProperties)
mode = _get_render_mode(typehints_format)
if isinstance(obj, NewType):
supertype = restify(obj.__supertype__, mode=mode)
return StringList([_('alias of %s') % supertype, ''], source='')
if isinstance(obj, TypeVar):
short_literals = python_display_short_literal_types
attrs = [
repr(obj.__name__),
*(
stringify_annotation(
constraint, mode, short_literals=short_literals
)
for constraint in obj.__constraints__
),
]
if obj.__bound__:
attrs.append(rf'bound=\ {restify(obj.__bound__, mode=mode)}')
if obj.__covariant__:
attrs.append('covariant=True')
if obj.__contravariant__:
attrs.append('contravariant=True')
alias = f'TypeVar({", ".join(attrs)})'
return StringList([_('alias of %s') % alias, ''], source='')
if props.doc_as_attr:
try:
analyzer = ModuleAnalyzer.for_module(props.module_name)
analyzer.analyze()
key = ('', props.dotted_parts)
no_classvar_doc_comment = key not in analyzer.attr_docs
except PycodeError:
no_classvar_doc_comment = True
if no_classvar_doc_comment:
alias = restify(obj, mode=mode)
return StringList([_('alias of %s') % alias], source='')
return more_content
return more_content
def _generate(
self,
more_content: StringList | None = None,
real_modname: str | None = None,
check_module: bool = False,
all_members: bool = False,
) -> None:
"""Generate reST for the object given by *self.props*, and possibly for
its members.
If *more_content* is given, include that content. If *real_modname* is
given, use that module name to find attribute docs. If *check_module* is
True, only generate if the object is defined in the module name it is
imported from. If *all_members* is True, document all members.
"""
if self.props.obj_type in {'class', 'exception'}:
# Do not pass real_modname and use the name from the __module__
# attribute of the class.
# If a class gets imported into the module real_modname
# the analyzer won't find the source of the class, if
# it looks in real_modname.
real_modname = None
# If there is no real module defined, figure out which to use.
# The real module is used in the module analyzer to look up the module
# where the attribute documentation would actually be found in.
# This is used for situations where you have a module that collects the
# functions and classes of internal submodules.
guess_modname = self.props._obj___module__ or self.props.module_name
self.real_modname: str = real_modname or guess_modname
# try to also get a source code analyzer for attribute docs
try:
self.analyzer = ModuleAnalyzer.for_module(self.real_modname)
# parse right now, to get PycodeErrors on parsing (results will
# be cached anyway)
self.analyzer.find_attr_docs()
except PycodeError as exc:
logger.debug('[autodoc] module analyzer failed: %s', exc)
# no source file -- e.g. for builtin and C modules
self.analyzer = None
# at least add the module source file as a dependency
if self.props.module_name:
try:
module_spec = sys.modules[self.props.module_name].__spec__
except (AttributeError, KeyError):
pass
else:
if (
module_spec is not None
and module_spec.has_location
and module_spec.origin
):
self.record_dependencies.add(module_spec.origin)
else:
self.record_dependencies.add(self.analyzer.srcname)
if self.real_modname != guess_modname:
# Add module to dependency list if target object is defined in other module.
try:
srcname = ModuleAnalyzer.for_module(guess_modname).srcname
self.record_dependencies.add(srcname)
except PycodeError:
pass
has_docstring = any(self.props._docstrings or ())
if ismock(self.props._obj) and not has_docstring:
logger.warning(
__('A mocked object is detected: %r'),
self.props.full_name,
type='autodoc',
subtype='mocked_object',
)
# check __module__ of object (for members not given explicitly)
if check_module and not self.options.imported_members:
subject = inspect.unpartial(self.props._obj)
modname = self.get_attr(subject, '__module__', None)
if modname and modname != self.props.module_name:
return
indent = self.indent
analyzer_source = '' if self.analyzer is None else self.analyzer.srcname
source_name = _docstring_source_name(props=self.props, source=analyzer_source)
# make sure that the result starts with an empty line. This is
# necessary for some situations where another directive preprocesses
# reST and no starting newline is present
self.result.append('', source_name)
# generate the directive header and options, if applicable
self.add_directive_header(indent=indent)
self.result.append('', source_name)
# add all content (from docstrings, attribute docs etc.)
self.add_content(more_content, indent=indent)
# document members, if possible
analyzer = self.analyzer
if analyzer is not None:
analyzer.analyze()
_document_members(
all_members=all_members,
analyzer_order=analyzer.tagorder if analyzer is not None else {},
attr_docs=analyzer.attr_docs if analyzer is not None else {},
config=self.config,
current_document=self._current_document,
env=self.env,
events=self._events,
get_attr=self.get_attr,
indent=indent,
options=self.options,
props=self.props,
real_modname=self.real_modname,
record_dependencies=self.record_dependencies,
registry=self.env._registry,
result=self.result,
)
class ModuleDocumenter(Documenter):
"""Specialized Documenter subclass for modules."""
@@ -500,49 +182,3 @@ class TypeAliasDocumenter(Documenter):
'annotation': annotation_option,
'no-value': bool_option,
}
class _AutodocAttrGetter:
"""getattr() override for types such as Zope interfaces."""
_attr_getters: Sequence[tuple[type, Callable[[Any, str, Any], Any]]]
__slots__ = ('_attr_getters',)
def __init__(
self, attr_getters: dict[type, Callable[[Any, str, Any], Any]], /
) -> None:
super().__setattr__('_attr_getters', tuple(attr_getters.items()))
def __call__(self, obj: Any, name: str, *defargs: Any) -> Any:
for typ, func in self._attr_getters:
if isinstance(obj, typ):
return func(obj, name, *defargs)
return safe_getattr(obj, name, *defargs)
def __repr__(self) -> str:
return f'_AutodocAttrGetter({dict(self._attr_getters)!r})'
def __setattr__(self, key: str, value: Any) -> NoReturn:
msg = f'{self.__class__.__name__} is immutable'
raise AttributeError(msg)
def __delattr__(self, key: str) -> NoReturn:
msg = f'{self.__class__.__name__} is immutable'
raise AttributeError(msg)
def _docstring_source_name(*, props: _ItemProperties, source: str) -> str:
obj_module = inspect.safe_getattr(props._obj, '__module__', None)
obj_qualname = inspect.safe_getattr(props._obj, '__qualname__', None)
if obj_module and obj_qualname:
# Get the correct location of docstring from props._obj
# to support inherited methods
fullname = f'{obj_module}.{obj_qualname}'
else:
fullname = props.full_name
if source:
return f'{source}:docstring of {fullname}'
return f'docstring of {fullname}'
+386
View File
@@ -0,0 +1,386 @@
from __future__ import annotations
import sys
from typing import TYPE_CHECKING
from docutils.statemachine import StringList
from sphinx.errors import PycodeError
from sphinx.ext.autodoc._docstrings import _prepare_docstrings, _process_docstrings
from sphinx.ext.autodoc._member_finder import _gather_members
from sphinx.ext.autodoc._renderer import _add_content, _directive_header_lines
from sphinx.ext.autodoc._sentinels import ALL
from sphinx.ext.autodoc.mock import ismock
from sphinx.locale import _, __
from sphinx.pycode import ModuleAnalyzer
from sphinx.util import inspect, logging
from sphinx.util.typing import restify, stringify_annotation
if TYPE_CHECKING:
from collections.abc import Iterator, Mapping
from typing import Literal
from sphinx.config import Config
from sphinx.environment import BuildEnvironment, _CurrentDocument
from sphinx.events import EventManager
from sphinx.ext.autodoc._directive_options import _AutoDocumenterOptions
from sphinx.ext.autodoc._property_types import _ItemProperties
from sphinx.ext.autodoc.importer import _AttrGetter
from sphinx.util.typing import _RestifyMode
logger = logging.getLogger('sphinx.ext.autodoc')
def _generate_directives(
more_content: StringList | None = None,
real_modname: str | None = None,
check_module: bool = False,
all_members: bool = False,
*,
config: Config,
current_document: _CurrentDocument,
env: BuildEnvironment,
events: EventManager,
get_attr: _AttrGetter,
indent: str,
options: _AutoDocumenterOptions,
props: _ItemProperties,
record_dependencies: set[str],
result: StringList,
) -> None:
"""Generate reST for the object given by *props*, and possibly for its members.
If *more_content* is given, include that content. If *real_modname* is
given, use that module name to find attribute docs. If *check_module* is
True, only generate if the object is defined in the module name it is
imported from. If *all_members* is True, document all members.
"""
# If there is no real module defined, figure out which to use.
# The real module is used in the module analyzer to look up the module
# where the attribute documentation would actually be found in.
# This is used for situations where you have a module that collects the
# functions and classes of internal submodules.
guess_modname = props._obj___module__ or props.module_name
if props.obj_type in {'class', 'exception'}:
# Do not pass real_modname and use the name from the __module__
# attribute of the class.
# If a class gets imported into the module real_modname
# the analyzer won't find the source of the class, if
# it looks in real_modname.
real_modname = guess_modname
else:
real_modname = real_modname or guess_modname
# try to also get a source code analyzer for attribute docs
try:
analyzer = ModuleAnalyzer.for_module(real_modname)
# parse right now, to get PycodeErrors on parsing (results will
# be cached anyway)
analyzer.analyze()
record_dependencies.add(analyzer.srcname)
except PycodeError as exc:
logger.debug('[autodoc] module analyzer failed: %s', exc)
# no source file -- e.g. for builtin and C modules
analyzer = None
# at least add the module source file as a dependency
if props.module_name:
try:
module_spec = sys.modules[props.module_name].__spec__
except (AttributeError, KeyError):
pass
else:
if (
module_spec is not None
and module_spec.has_location
and module_spec.origin
):
record_dependencies.add(module_spec.origin)
if real_modname != guess_modname:
# Add module to dependency list if target object is defined in other module.
try:
srcname, _ = ModuleAnalyzer.get_module_source(guess_modname)
record_dependencies.add(str(srcname))
except PycodeError:
pass
has_docstring = any(props._docstrings or ())
if ismock(props._obj) and not has_docstring:
logger.warning(
__('A mocked object is detected: %r'),
props.full_name,
type='autodoc',
subtype='mocked_object',
)
# check __module__ of object (for members not given explicitly)
if check_module and not options.imported_members:
subject = inspect.unpartial(props._obj)
modname = get_attr(subject, '__module__', None)
if modname and modname != props.module_name:
return
# add all content (from docstrings, attribute docs etc.)
analyzer_source = '' if analyzer is None else analyzer.srcname
_add_directive_lines(
more_content=more_content,
attr_docs={} if analyzer is None else analyzer.attr_docs,
is_final=analyzer is not None and props.dotted_parts in analyzer.finals,
config=config,
events=events,
indent=indent,
options=options,
props=props,
result=result,
source_name=_docstring_source_name(props=props, source=analyzer_source),
)
# document members, if possible
_document_members(
all_members=all_members,
analyzer_order=analyzer.tagorder if analyzer is not None else {},
attr_docs=analyzer.attr_docs if analyzer is not None else {},
config=config,
current_document=current_document,
env=env,
events=events,
get_attr=get_attr,
indent=indent,
options=options,
props=props,
real_modname=real_modname,
record_dependencies=record_dependencies,
result=result,
)
def _add_directive_lines(
*,
more_content: StringList | None,
attr_docs: Mapping[tuple[str, str], list[str]],
is_final: bool,
config: Config,
events: EventManager,
indent: str,
options: _AutoDocumenterOptions,
props: _ItemProperties,
result: StringList,
source_name: str,
) -> None:
# generate the directive header and options, if applicable
lines = _directive_header_lines(
autodoc_typehints=config.autodoc_typehints,
directive_name=(
'py:attribute'
if props.obj_type in {'class', 'exception'} and props.doc_as_attr # type: ignore[attr-defined]
else f'py:{props.obj_type}'
),
is_final=is_final,
options=options,
props=props,
)
header_lines = StringList(list(lines), source='')
# add content from docstrings or attribute documentation
docstrings = _prepare_docstrings(props=props, attr_docs=attr_docs)
lines = _process_docstrings(
docstrings,
events=events,
props=props,
options=options,
)
docstring_lines = StringList(list(lines), source=source_name)
# add alias information, if applicable
lines = _body_alias_lines(
typehints_format=config.autodoc_typehints_format,
python_display_short_literal_types=config.python_display_short_literal_types,
props=props,
)
alias_lines = StringList(list(lines), source='')
# make sure that the result starts with an empty line. This is
# necessary for some situations where another directive preprocesses
# reST and no starting newline is present
result.append('', '')
_add_content(header_lines, result=result, indent=indent)
result.append('', '')
_add_content(docstring_lines, result=result, indent=indent + ' ')
if more_content is not None:
# add additional content from the directive, if present
_add_content(more_content, result=result, indent=indent + ' ')
_add_content(alias_lines, result=result, indent=indent + ' ')
def _document_members(
*,
all_members: bool,
analyzer_order: dict[str, int],
attr_docs: dict[tuple[str, str], list[str]],
config: Config,
current_document: _CurrentDocument,
env: BuildEnvironment,
events: EventManager,
get_attr: _AttrGetter,
indent: str,
options: _AutoDocumenterOptions,
props: _ItemProperties,
real_modname: str,
record_dependencies: set[str],
result: StringList,
) -> None:
"""Generate reST for member documentation.
If *all_members* is True, document all members, else those given by
*self.options.members*.
"""
has_members = props.obj_type == 'module' or (
props.obj_type in {'class', 'exception'} and not props.doc_as_attr # type: ignore[attr-defined]
)
if not has_members:
return
want_all = bool(all_members or options.inherited_members or options.members is ALL)
member_documenters = _gather_members(
want_all=want_all,
indent=indent,
analyzer_order=analyzer_order,
attr_docs=attr_docs,
config=config,
current_document=current_document,
env=env,
events=events,
get_attr=get_attr,
options=options,
props=props,
)
# for implicit module members, check __module__ to avoid
# documenting imported objects
members_check_module = bool(
props.obj_type == 'module'
and want_all
and (options.ignore_module_all or props.all is None) # type: ignore[attr-defined]
)
for member_props, is_attr, member_indent in member_documenters:
assert member_props.module_name
# Note that those two methods above do not emit events, so
# whatever objects we deduced should not have changed.
_generate_directives(
more_content=None,
real_modname=real_modname,
check_module=members_check_module and not is_attr,
all_members=True,
config=config,
current_document=current_document,
env=env,
events=events,
get_attr=get_attr,
indent=member_indent,
options=options,
props=member_props,
record_dependencies=record_dependencies,
result=result,
)
def _body_alias_lines(
*,
props: _ItemProperties,
typehints_format: Literal['fully-qualified', 'short'],
python_display_short_literal_types: bool,
) -> Iterator[str]:
"""Add content from docstrings, attribute documentation and user."""
if props.obj_type in {'data', 'attribute'}:
from sphinx.ext.autodoc._property_types import _AssignStatementProperties
assert isinstance(props, _AssignStatementProperties)
mode = _get_render_mode(typehints_format)
# Support for documenting GenericAliases
if props._obj_is_generic_alias:
alias = restify(props._obj, mode=mode)
yield _('alias of %s') % alias
yield ''
return
return
if props.obj_type in {'class', 'exception'}:
from sphinx.ext.autodoc._property_types import _ClassDefProperties
assert isinstance(props, _ClassDefProperties)
obj = props._obj
mode = _get_render_mode(typehints_format)
if props._obj_is_new_type:
supertype = restify(obj.__supertype__, mode=mode)
yield _('alias of %s') % supertype
yield ''
return
if props._obj_is_typevar:
short_literals = python_display_short_literal_types
attrs = [
repr(obj.__name__),
*(
stringify_annotation(
constraint, mode, short_literals=short_literals
)
for constraint in obj.__constraints__
),
]
if obj.__bound__:
attrs.append(rf'bound=\ {restify(obj.__bound__, mode=mode)}')
if obj.__covariant__:
attrs.append('covariant=True')
if obj.__contravariant__:
attrs.append('contravariant=True')
alias = f'TypeVar({", ".join(attrs)})'
yield _('alias of %s') % alias
yield ''
return
if props.doc_as_attr:
try:
analyzer = ModuleAnalyzer.for_module(props.module_name)
analyzer.analyze()
key = ('', props.dotted_parts)
class_var_doc_comment = key in analyzer.attr_docs
except PycodeError:
class_var_doc_comment = False
if class_var_doc_comment:
return
alias = restify(obj, mode=mode)
yield _('alias of %s') % alias
return
return
return
def _docstring_source_name(*, props: _ItemProperties, source: str) -> str:
obj_module = inspect.safe_getattr(props._obj, '__module__', None)
obj_qualname = inspect.safe_getattr(props._obj, '__qualname__', None)
if obj_module and obj_qualname:
# Get the correct location of docstring from props._obj
# to support inherited methods
fullname = f'{obj_module}.{obj_qualname}'
else:
fullname = props.full_name
if source:
return f'{source}:docstring of {fullname}'
return f'docstring of {fullname}'
def _get_render_mode(
typehints_format: Literal['fully-qualified', 'short'],
) -> _RestifyMode:
if typehints_format == 'short':
return 'smart'
return 'fully-qualified-except-typing'
-78
View File
@@ -32,8 +32,6 @@ if TYPE_CHECKING:
from collections.abc import Iterable, Iterator, Mapping, Sequence, Set
from typing import Any, Literal
from docutils.statemachine import StringList
from sphinx.config import Config
from sphinx.environment import BuildEnvironment, _CurrentDocument
from sphinx.events import EventManager
@@ -46,7 +44,6 @@ if TYPE_CHECKING:
SLOTS_ATTR_T,
)
from sphinx.ext.autodoc.importer import _AttrGetter
from sphinx.registry import SphinxComponentRegistry
logger = logging.getLogger('sphinx.ext.autodoc')
special_member_re = re.compile(r'^__\S+__$')
@@ -91,81 +88,6 @@ class ObjectMember:
)
def _document_members(
*,
all_members: bool,
analyzer_order: dict[str, int],
attr_docs: dict[tuple[str, str], list[str]],
config: Config,
current_document: _CurrentDocument,
env: BuildEnvironment,
events: EventManager,
get_attr: _AttrGetter,
indent: str,
options: _AutoDocumenterOptions,
props: _ItemProperties,
real_modname: str,
record_dependencies: set[str],
registry: SphinxComponentRegistry,
result: StringList,
) -> None:
"""Generate reST for member documentation.
If *all_members* is True, document all members, else those given by
*self.options.members*.
"""
has_members = props.obj_type == 'module' or (
props.obj_type in {'class', 'exception'} and not props.doc_as_attr # type: ignore[attr-defined]
)
if not has_members:
return
want_all = bool(all_members or options.inherited_members or options.members is ALL)
member_documenters = _gather_members(
want_all=want_all,
indent=indent,
analyzer_order=analyzer_order,
attr_docs=attr_docs,
config=config,
current_document=current_document,
env=env,
events=events,
get_attr=get_attr,
options=options,
props=props,
)
# for implicit module members, check __module__ to avoid
# documenting imported objects
members_check_module = bool(
props.obj_type == 'module'
and want_all
and (options.ignore_module_all or props.all is None) # type: ignore[attr-defined]
)
for member_props, is_attr, member_indent in member_documenters:
assert member_props.module_name
# We can directly call ._generate() since the documenters
# already called ``_load_object_by_name()`` before.
#
# Note that those two methods above do not emit events, so
# whatever objects we deduced should not have changed.
doccls = registry.documenters[member_props.obj_type]
documenter = doccls(
env=env,
options=options,
get_attr=get_attr,
record_dependencies=record_dependencies,
result=result,
indent=member_indent,
)
documenter.props = member_props
documenter._generate(
all_members=True,
real_modname=real_modname,
check_module=members_check_module and not is_attr,
)
def _gather_members(
*,
want_all: bool,
+69 -37
View File
@@ -1,6 +1,5 @@
from __future__ import annotations
from collections.abc import Callable
from typing import TYPE_CHECKING
from docutils import nodes
@@ -10,24 +9,56 @@ from sphinx.ext.autodoc._directive_options import (
_AutoDocumenterOptions,
_process_documenter_options,
)
from sphinx.ext.autodoc._documenters import _AutodocAttrGetter
from sphinx.ext.autodoc._generate import _generate_directives
from sphinx.ext.autodoc.importer import _load_object_by_name
from sphinx.util import logging
from sphinx.util.docutils import SphinxDirective, switch_source_input
from sphinx.util.inspect import safe_getattr
from sphinx.util.parsing import nested_parse_to_nodes
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
from typing import Any, NoReturn
from docutils.nodes import Node
from docutils.parsers.rst.states import RSTState
from sphinx.config import Config
from sphinx.ext.autodoc import Documenter
from sphinx.ext.autodoc._directive_options import Options
logger = logging.getLogger(__name__)
class DummyOptionSpec(dict[str, Callable[[str], str]]): # NoQA: FURB189
class _AutodocAttrGetter:
"""getattr() override for types such as Zope interfaces."""
_attr_getters: Sequence[tuple[type, Callable[[Any, str, Any], Any]]]
__slots__ = ('_attr_getters',)
def __init__(
self, attr_getters: dict[type, Callable[[Any, str, Any], Any]], /
) -> None:
super().__setattr__('_attr_getters', tuple(attr_getters.items()))
def __call__(self, obj: Any, name: str, *defargs: Any) -> Any:
for typ, func in self._attr_getters:
if isinstance(obj, typ):
return func(obj, name, *defargs)
return safe_getattr(obj, name, *defargs)
def __repr__(self) -> str:
return f'_AutodocAttrGetter({dict(self._attr_getters)!r})'
def __setattr__(self, key: str, value: Any) -> NoReturn:
msg = f'{self.__class__.__name__} is immutable'
raise AttributeError(msg)
def __delattr__(self, key: str) -> NoReturn:
msg = f'{self.__class__.__name__} is immutable'
raise AttributeError(msg)
class DummyOptionSpec(dict[str, 'Callable[[str], str]']): # NoQA: FURB189
"""An option_spec allows any options."""
def __bool__(self) -> bool:
@@ -38,25 +69,12 @@ class DummyOptionSpec(dict[str, Callable[[str], str]]): # NoQA: FURB189
return lambda x: x
def process_documenter_options(
documenter: type[Documenter], config: Config, options: dict[str, str | None]
) -> Options:
from sphinx.ext.autodoc._directive_options import Options
opts = _process_documenter_options(
option_spec=documenter.option_spec,
default_options=config.autodoc_default_options,
options=options,
)
return Options(opts)
def parse_generated_content(
state: RSTState, content: StringList, documenter: Documenter
state: RSTState, content: StringList, titles_allowed: bool
) -> list[Node]:
"""Parse an item of content generated by Documenter."""
with switch_source_input(state, content):
if documenter.titles_allowed:
if titles_allowed:
return nested_parse_to_nodes(state, content)
node = nodes.paragraph()
@@ -90,14 +108,18 @@ class AutodocDirective(SphinxDirective):
source, lineno = (None, None)
logger.debug('[autodoc] %s:%s: input:\n%s', source, lineno, self.block_text)
registry = self.env._registry
# look up target Documenter
objtype = self.name[4:] # strip prefix (auto-).
doccls = self.env._registry.documenters[objtype]
doccls = registry.documenters[objtype]
option_spec = doccls.option_spec
titles_allowed = doccls.titles_allowed
# process the options with the selected documenter's option_spec
try:
opts = _process_documenter_options(
option_spec=doccls.option_spec,
option_spec=option_spec,
default_options=self.config.autodoc_default_options,
options=self.options,
)
@@ -114,16 +136,22 @@ class AutodocDirective(SphinxDirective):
documenter_options._tab_width = self.state.document.settings.tab_width
# generate the output
get_attr = _AutodocAttrGetter(self.env._registry.autodoc_attrgetters)
get_attr = _AutodocAttrGetter(registry.autodoc_attrgetters)
name = self.arguments[0]
env = self.env
config = env.config
current_document = env.current_document
events = env.events
props = _load_object_by_name(
name=self.arguments[0],
name=name,
objtype=objtype, # type: ignore[arg-type]
mock_imports=self.config.autodoc_mock_imports,
type_aliases=self.config.autodoc_type_aliases,
current_document=self.env.current_document,
config=self.config,
env=self.env,
events=self.env.events,
mock_imports=config.autodoc_mock_imports,
type_aliases=config.autodoc_type_aliases,
current_document=current_document,
config=config,
env=env,
events=events,
get_attr=get_attr,
options=documenter_options,
)
@@ -132,15 +160,19 @@ class AutodocDirective(SphinxDirective):
record_dependencies: set[str] = set()
result = StringList()
documenter = doccls(
env=self.env,
options=documenter_options,
_generate_directives(
more_content=self.content,
config=config,
current_document=current_document,
env=env,
events=events,
get_attr=get_attr,
indent='',
options=documenter_options,
props=props,
record_dependencies=record_dependencies,
result=result,
)
documenter.props = props
documenter._generate(more_content=self.content)
if not result:
return []
@@ -151,4 +183,4 @@ class AutodocDirective(SphinxDirective):
for fn in record_dependencies:
self.state.document.settings.record_dependencies.add(fn)
return parse_generated_content(self.state, result, documenter)
return parse_generated_content(self.state, result, titles_allowed)
+22 -34
View File
@@ -67,9 +67,10 @@ import sphinx
from sphinx import addnodes
from sphinx.errors import PycodeError
from sphinx.ext.autodoc._directive_options import _AutoDocumenterOptions
from sphinx.ext.autodoc._documenters import _AutodocAttrGetter
from sphinx.ext.autodoc._docstrings import _prepare_docstrings, _process_docstrings
from sphinx.ext.autodoc._member_finder import _best_object_type_for_member
from sphinx.ext.autodoc._sentinels import INSTANCE_ATTR
from sphinx.ext.autodoc.directive import _AutodocAttrGetter
from sphinx.ext.autodoc.importer import _load_object_by_name, import_module
from sphinx.ext.autodoc.mock import mock
from sphinx.locale import __
@@ -94,7 +95,6 @@ if TYPE_CHECKING:
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment
from sphinx.ext.autodoc import Documenter
from sphinx.ext.autodoc._property_types import _AutodocObjType
from sphinx.util.typing import ExtensionMetadata, OptionSpec
from sphinx.writers.html5 import HTML5Translator
@@ -154,33 +154,26 @@ def autosummary_table_visit_html(
# -- autodoc integration -------------------------------------------------------
def get_documenter(app: Sphinx, obj: Any, parent: Any) -> type[Documenter]:
"""Get an autodoc.Documenter class suitable for documenting the given
object.
*obj* is the Python object to be documented, and *parent* is an
another Python object (e.g. a module or a class) to which *obj*
belongs to.
"""
obj_type = _get_documenter(obj, parent)
return app.registry.documenters[obj_type]
def _get_documenter(obj: Any, parent: Any) -> _AutodocObjType:
"""Get an autodoc.Documenter class suitable for documenting the given
object.
"""Get the best object type suitable for documenting the given object.
*obj* is the Python object to be documented, and *parent* is an
another Python object (e.g. a module or a class) to which *obj*
belongs to.
*obj* is the Python object to be documented, and *parent* is another
Python object (e.g. a module or a class) to which *obj* belongs.
"""
if inspect.ismodule(obj):
return 'module'
if parent is None:
if parent is None or inspect.ismodule(parent):
parent_obj_type = 'module'
else:
parent_obj_type = _get_documenter(parent, None)
parent_opt = _best_object_type_for_member(
member=parent,
member_name='',
is_attr=False,
parent_obj_type='module',
parent_props=None,
)
parent_obj_type = parent_opt if parent_opt is not None else 'data'
# Get the correct documenter class for *obj*
if obj_type := _best_object_type_for_member(
@@ -394,21 +387,16 @@ class Autosummary(SphinxDirective):
# -- Grab the summary
result = StringList() # Initialise for each documenter
doccls = env._registry.documenters[obj_type]
documenter = doccls(
env=env,
# get content from docstrings or attribute documentation
attr_docs = {} if analyzer is None else analyzer.attr_docs
docstrings = _prepare_docstrings(props=props, attr_docs=attr_docs)
docstring_lines = _process_docstrings(
docstrings,
events=events,
props=props,
options=opts,
get_attr=get_attr,
result=result,
)
documenter.props = props
documenter.analyzer = analyzer
documenter.add_content(None, indent='')
lines = result.data[:]
if props.obj_type != 'module':
lines[:] = [line.removeprefix(' ') for line in lines]
summary = extract_summary(lines, self.state.document)
summary = extract_summary(list(docstring_lines), self.state.document)
items.append((display_name, sig, summary, real_name))
+23 -13
View File
@@ -8,6 +8,7 @@ from sphinx.ext.autodoc._directive_options import (
_AutoDocumenterOptions,
_process_documenter_options,
)
from sphinx.ext.autodoc._generate import _generate_directives
from sphinx.ext.autodoc.importer import _load_object_by_name
from sphinx.util.inspect import safe_getattr
@@ -27,33 +28,42 @@ def do_autodoc(
options = {} if options is None else options.copy()
if not app.env.current_document.docname:
app.env.current_document.docname = 'index' # set dummy docname
doccls = app.registry.documenters[obj_type]
option_spec = app.registry.documenters[obj_type].option_spec
opts = _process_documenter_options(
option_spec=doccls.option_spec,
option_spec=option_spec,
default_options=app.config.autodoc_default_options,
options=options,
)
doc_options = _AutoDocumenterOptions.from_directive_options(opts)
config = app.config
current_document = app.env.current_document
env = app.env
events = app.events
props = _load_object_by_name(
name=name,
objtype=obj_type,
mock_imports=app.config.autodoc_mock_imports,
type_aliases=app.config.autodoc_type_aliases,
current_document=app.env.current_document,
config=app.config,
env=app.env,
events=app.events,
mock_imports=config.autodoc_mock_imports,
type_aliases=config.autodoc_type_aliases,
current_document=current_document,
config=config,
env=env,
events=events,
get_attr=safe_getattr,
options=doc_options,
)
result = StringList()
if props is not None:
documenter = doccls(
env=app.env,
options=doc_options,
_generate_directives(
config=config,
current_document=current_document,
env=env,
events=events,
get_attr=safe_getattr,
indent='',
options=doc_options,
props=props,
record_dependencies=set(),
result=result,
)
documenter.props = props
documenter._generate()
return result
+24 -13
View File
@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING
from warnings import catch_warnings
import pytest
from docutils.statemachine import StringList
from sphinx import addnodes
from sphinx.ext.autodoc._directive_options import (
@@ -19,13 +20,15 @@ from sphinx.ext.autodoc._directive_options import (
inherited_members_option,
)
from sphinx.ext.autodoc._docstrings import _get_docstring_lines
from sphinx.ext.autodoc._documenters import Documenter, _AutodocAttrGetter
from sphinx.ext.autodoc._documenters import Documenter
from sphinx.ext.autodoc._generate import _generate_directives
from sphinx.ext.autodoc._property_types import (
_ClassDefProperties,
_FunctionDefProperties,
_ItemProperties,
)
from sphinx.ext.autodoc._sentinels import ALL
from sphinx.ext.autodoc.directive import _AutodocAttrGetter
from sphinx.ext.autodoc.importer import (
_format_signatures,
_load_object_by_name,
@@ -547,29 +550,37 @@ def test_attrgetter_using(app):
def _assert_getter_works(app, get_attr, options, objtype, name, *attrs):
env = app.env
config = app.config
current_document = env.current_document
events = env.events
getattr_spy.clear()
props = _load_object_by_name(
name=name,
objtype=objtype,
mock_imports=app.config.autodoc_mock_imports,
type_aliases=app.config.autodoc_type_aliases,
current_document=app.env.current_document,
config=app.config,
env=app.env,
events=app.events,
mock_imports=config.autodoc_mock_imports,
type_aliases=config.autodoc_type_aliases,
current_document=current_document,
config=config,
env=env,
events=events,
get_attr=get_attr,
options=options,
)
if props is not None:
doccls = app.registry.documenters[objtype]
documenter = doccls(
env=app.env,
options=options,
_generate_directives(
config=config,
current_document=current_document,
env=env,
events=events,
get_attr=get_attr,
indent='',
options=options,
props=props,
record_dependencies=set(),
result=StringList(),
)
documenter.props = props
documenter._generate()
hooked_members = {s[1] for s in getattr_spy}
documented_members = {s[1] for s in processed_signatures}