diff --git a/doc/development/tutorials/examples/autodoc_intenum.py b/doc/development/tutorials/examples/autodoc_intenum.py index d8ba39b14e..bb36ea0e6b 100644 --- a/doc/development/tutorials/examples/autodoc_intenum.py +++ b/doc/development/tutorials/examples/autodoc_intenum.py @@ -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 diff --git a/pyproject.toml b/pyproject.toml index c64d5ab2e6..e3418ab98a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/sphinx/ext/autodoc/_docstrings.py b/sphinx/ext/autodoc/_docstrings.py index 96a195931b..11fab9fe11 100644 --- a/sphinx/ext/autodoc/_docstrings.py +++ b/sphinx/ext/autodoc/_docstrings.py @@ -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, ) diff --git a/sphinx/ext/autodoc/_documenters.py b/sphinx/ext/autodoc/_documenters.py index f5e3624284..4cfe8db897 100644 --- a/sphinx/ext/autodoc/_documenters.py +++ b/sphinx/ext/autodoc/_documenters.py @@ -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}' diff --git a/sphinx/ext/autodoc/_generate.py b/sphinx/ext/autodoc/_generate.py new file mode 100644 index 0000000000..84caf47232 --- /dev/null +++ b/sphinx/ext/autodoc/_generate.py @@ -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' diff --git a/sphinx/ext/autodoc/_member_finder.py b/sphinx/ext/autodoc/_member_finder.py index 5703c2b2c0..57419109c6 100644 --- a/sphinx/ext/autodoc/_member_finder.py +++ b/sphinx/ext/autodoc/_member_finder.py @@ -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, diff --git a/sphinx/ext/autodoc/directive.py b/sphinx/ext/autodoc/directive.py index d7c50d40eb..01c79ff70c 100644 --- a/sphinx/ext/autodoc/directive.py +++ b/sphinx/ext/autodoc/directive.py @@ -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) diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index 2c1bcb2d78..efc47a8052 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -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)) diff --git a/tests/test_ext_autodoc/autodoc_util.py b/tests/test_ext_autodoc/autodoc_util.py index 5c07a79d4b..5bf75b7db8 100644 --- a/tests/test_ext_autodoc/autodoc_util.py +++ b/tests/test_ext_autodoc/autodoc_util.py @@ -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 diff --git a/tests/test_ext_autodoc/test_ext_autodoc.py b/tests/test_ext_autodoc/test_ext_autodoc.py index cc48d8efec..fa9042c30a 100644 --- a/tests/test_ext_autodoc/test_ext_autodoc.py +++ b/tests/test_ext_autodoc/test_ext_autodoc.py @@ -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}