diff --git a/pyproject.toml b/pyproject.toml index 0ff9868c2c..c64d5ab2e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,6 +85,7 @@ dependencies = [ "roman-numerals-py>=1.0.0", "packaging>=23.0", "colorama>=0.4.6; sys_platform == 'win32'", + "ipython>=9.6.0", ] dynamic = ["version"] diff --git a/sphinx/ext/autodoc/_directive_options.py b/sphinx/ext/autodoc/_directive_options.py index bfbf498bf7..b3f42d96fa 100644 --- a/sphinx/ext/autodoc/_directive_options.py +++ b/sphinx/ext/autodoc/_directive_options.py @@ -46,6 +46,7 @@ class _AutoDocumenterOptions: 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 diff --git a/sphinx/ext/autodoc/_docstrings.py b/sphinx/ext/autodoc/_docstrings.py new file mode 100644 index 0000000000..c191b91b44 --- /dev/null +++ b/sphinx/ext/autodoc/_docstrings.py @@ -0,0 +1,251 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar + +from sphinx.errors import PycodeError +from sphinx.ext.autodoc._property_types import _ClassDefProperties +from sphinx.ext.autodoc._sentinels import ( + RUNTIME_INSTANCE_ATTRIBUTE, + SLOTS_ATTR, + UNINITIALIZED_ATTR, +) +from sphinx.locale import __ +from sphinx.pycode import ModuleAnalyzer +from sphinx.util import inspect, logging +from sphinx.util.docstrings import prepare_docstring +from sphinx.util.inspect import getdoc + +if TYPE_CHECKING: + from collections.abc import Sequence + from typing import Any, Literal + + from sphinx.ext.autodoc._property_types import _ItemProperties + from sphinx.ext.autodoc.importer import _AttrGetter + +logger = logging.getLogger('sphinx.ext.autodoc') + +_OBJECT_INIT_DOCSTRING = (tuple(prepare_docstring(object.__init__.__doc__ or '')),) +_OBJECT_NEW_DOCSTRING = (tuple(prepare_docstring(object.__new__.__doc__ or '')),) + + +def _get_docstring_lines( + props: _ItemProperties, + *, + class_doc_from: Literal['both', 'class', 'init'], + get_attr: _AttrGetter, + inherit_docstrings: bool, + parent: Any, + tab_width: int, + _new_docstrings: Sequence[Sequence[str]] | None = None, +) -> Sequence[Sequence[str]] | None: + obj = props._obj + + if props.obj_type in {'class', 'exception'}: + assert isinstance(props, _ClassDefProperties) + + if isinstance(obj, TypeVar): + if obj.__doc__ == TypeVar.__doc__: + return () + if props.doc_as_attr: + # Don't show the docstring of the class when it is an alias. + if _class_variable_comment(props): + return () + return None + + if _new_docstrings is not None: + return tuple(tuple(doc) for doc in _new_docstrings) + + docstrings = [] + attrdocstring = getdoc(obj) + if attrdocstring: + docstrings.append(attrdocstring) + + # for classes, what the "docstring" is can be controlled via a + # config value; the default is only the class docstring + if class_doc_from in {'both', 'init'}: + __init__ = get_attr(obj, '__init__', None) + init_docstring = getdoc( + __init__, + allow_inherited=inherit_docstrings, + cls=obj, # TODO: object or obj? + name='__init__', + ) + # no __init__ means default __init__ + if init_docstring == object.__init__.__doc__: + init_docstring = None + if not init_docstring: + # try __new__ + __new__ = get_attr(obj, '__new__', None) + init_docstring = getdoc( + __new__, + allow_inherited=inherit_docstrings, + cls=object, # TODO: object or obj? + name='__new__', + ) + # no __new__ means default __new__ + if init_docstring == object.__new__.__doc__: + init_docstring = None + if init_docstring: + if class_doc_from == 'init': + docstrings = [init_docstring] + else: + docstrings.append(init_docstring) + + return tuple( + tuple(prepare_docstring(docstring, tab_width)) for docstring in docstrings + ) + + if props.obj_type == 'method': + docstring = _get_doc( + obj, + props=props, + inherit_docstrings=inherit_docstrings, + _new_docstrings=_new_docstrings, + parent=parent, + tab_width=tab_width, + ) + if props.name == '__init__': + if docstring == _OBJECT_INIT_DOCSTRING: + return () + if props.name == '__new__': + if docstring == _OBJECT_NEW_DOCSTRING: + return () + return docstring + + if props.obj_type == 'data': + # Check the variable has a docstring-comment + + # get_module_comment() + comment = None + try: + analyzer = ModuleAnalyzer.for_module(props.module_name) + analyzer.analyze() + key = ('', props.name) + if key in analyzer.attr_docs: + comment = tuple(analyzer.attr_docs[key]) + except PycodeError: + pass + + if comment: + return (comment,) + return _get_doc( + obj, + props=props, + inherit_docstrings=inherit_docstrings, + _new_docstrings=_new_docstrings, + parent=parent, + tab_width=tab_width, + ) + + if props.obj_type == 'attribute': + from sphinx.ext.autodoc.importer import ( + _get_attribute_comment, + _is_runtime_instance_attribute_not_commented, + ) + + # Check the attribute has a docstring-comment + comment = _get_attribute_comment( + parent=parent, obj_path=props.parts, attrname=props.parts[-1] + ) + if comment: + return (comment,) + + # Disable `autodoc_inherit_docstring` to avoid to obtain + # a docstring from the value which descriptor returns unexpectedly. + # See: https://github.com/sphinx-doc/sphinx/issues/7805 + inherit_docstrings = False + + if obj is SLOTS_ATTR: + # support for __slots__ + try: + parent___slots__ = inspect.getslots(parent) + if parent___slots__ and (docstring := parent___slots__.get(props.name)): + docstring = tuple(prepare_docstring(docstring)) + return (docstring,) + return () + except ValueError as exc: + logger.warning( + __('Invalid __slots__ found on %s. Ignored.'), + (parent.__qualname__, exc), + type='autodoc', + ) + return () + + if ( + obj is RUNTIME_INSTANCE_ATTRIBUTE + and _is_runtime_instance_attribute_not_commented( + parent=parent, obj_path=props.parts + ) + ): + return None + + if obj is UNINITIALIZED_ATTR: + return None + + if not inspect.isattributedescriptor(obj): + # the docstring of non-data descriptor is very probably + # the wrong thing to display + return None + + return _get_doc( + obj, + props=props, + inherit_docstrings=inherit_docstrings, + _new_docstrings=_new_docstrings, + parent=parent, + tab_width=tab_width, + ) + + docstring = _get_doc( + obj, + props=props, + inherit_docstrings=inherit_docstrings, + _new_docstrings=_new_docstrings, + parent=parent, + tab_width=tab_width, + ) + return docstring + + +def _get_doc( + obj: Any, + *, + props: _ItemProperties, + inherit_docstrings: bool, + _new_docstrings: Sequence[Sequence[str]] | None, + parent: Any, + tab_width: int, +) -> Sequence[Sequence[str]] | None: + """Decode and return lines of the docstring(s) for the object. + + When it returns None, autodoc-process-docstring will not be called for this + object. + """ + if obj is UNINITIALIZED_ATTR: + return () + + if props.obj_type not in {'module', 'data'} and _new_docstrings is not None: + # docstring already returned previously, then modified due to + # ``Documenter._find_signature()``. Just return the + # previously-computed result, so that we don't lose the processing. + return _new_docstrings + + docstring = getdoc( + obj, + allow_inherited=inherit_docstrings, + cls=parent, + name=props.object_name, + ) + if docstring: + return (tuple(prepare_docstring(docstring, tab_width)),) + return () + + +def _class_variable_comment(props: _ItemProperties) -> bool: + try: + analyzer = ModuleAnalyzer.for_module(props.module_name) + analyzer.analyze() + key = ('', props.dotted_parts) + return bool(analyzer.attr_docs.get(key, False)) + except PycodeError: + return False diff --git a/sphinx/ext/autodoc/_documenters.py b/sphinx/ext/autodoc/_documenters.py index c7dc91ff4a..d07286803a 100644 --- a/sphinx/ext/autodoc/_documenters.py +++ b/sphinx/ext/autodoc/_documenters.py @@ -1,8 +1,5 @@ from __future__ import annotations -import functools -import itertools -import operator import re import sys from inspect import Parameter, Signature @@ -21,21 +18,16 @@ from sphinx.ext.autodoc._directive_options import ( member_order_option, members_option, ) +from sphinx.ext.autodoc._docstrings import _get_docstring_lines from sphinx.ext.autodoc._member_finder import _filter_members, _get_members_to_document from sphinx.ext.autodoc._renderer import ( _add_content, _directive_header_lines, - _hide_value_re, ) from sphinx.ext.autodoc._sentinels import ( ALL, - RUNTIME_INSTANCE_ATTRIBUTE, - SLOTS_ATTR, - UNINITIALIZED_ATTR, ) from sphinx.ext.autodoc.importer import ( - _get_attribute_comment, - _is_runtime_instance_attribute_not_commented, _load_object_by_name, _resolve_name, ) @@ -43,10 +35,9 @@ 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.docstrings import prepare_docstring, separate_metadata +from sphinx.util.docstrings import prepare_docstring from sphinx.util.inspect import ( evaluate_signature, - getdoc, safe_getattr, stringify_signature, ) @@ -134,7 +125,6 @@ class Documenter: __uninitialized_global_variable__: ClassVar[bool] = False """If True, support uninitialized (type annotation only) global variables""" - _new_docstrings: list[list[str]] | None = None _signatures: list[str] = [] option_spec: ClassVar[OptionSpec] = { @@ -212,6 +202,7 @@ class Documenter: env=self.env, events=self._events, get_attr=self.get_attr, + options=self.options, ) if ret is None: return None @@ -337,10 +328,11 @@ class Documenter: if hasattr(self.props._obj, '__mro__'): valid_names.extend(cls.__name__ for cls in self.props._obj.__mro__) - docstrings = self.get_doc() + docstrings = self.props._docstrings if docstrings is None: return None, None - self._new_docstrings = docstrings[:] + _new_docstrings = [list(l) for l in docstrings] + tab_width = self.options._tab_width self._signatures = [] result = None for i, doclines in enumerate(docstrings): @@ -363,9 +355,7 @@ class Documenter: break # re-prepare docstring to ignore more leading indentation - directive = self.directive - tab_width = directive.state.document.settings.tab_width - self._new_docstrings[i] = prepare_docstring( + _new_docstrings[i] = prepare_docstring( '\n'.join(doclines[j + 1 :]), tab_width ) @@ -380,6 +370,17 @@ class Documenter: # finish the loop when signature found break + self.props._docstrings = _get_docstring_lines( + self.props, + class_doc_from=self.options.class_doc_from + if self.options.class_doc_from is not None + else self.config.autoclass_content, + get_attr=self.get_attr, + inherit_docstrings=self.config.autodoc_inherit_docstrings, + parent=self.parent, + tab_width=self.options._tab_width, + _new_docstrings=tuple(tuple(doc) for doc in _new_docstrings), + ) return result def format_signature(self, **kwargs: Any) -> str: @@ -455,14 +456,6 @@ class Documenter: directive_name = getattr(self, 'directivetype', self.objtype) directive_name = f'{domain_name}:{directive_name}' - docstrings = self.get_doc() - docstrings_has_hide_value = False - if docstrings: - for line in itertools.chain.from_iterable(docstrings): - if _hide_value_re.match(line): - docstrings_has_hide_value = True - break - if self.analyzer: is_final = self.props.dotted_parts in self.analyzer.finals else: @@ -475,7 +468,6 @@ class Documenter: sig, autodoc_typehints=self.config.autodoc_typehints, directive_name=directive_name, - docstrings_has_hide_value=docstrings_has_hide_value, is_final=is_final, options=self.options, props=self.props, @@ -485,50 +477,6 @@ class Documenter: else: result.append('', sourcename) - def get_doc(self) -> list[list[str]] | None: - """Decode and return lines of the docstring(s) for the object. - - When it returns None, autodoc-process-docstring will not be called for this - object. - """ - if self.props._obj is UNINITIALIZED_ATTR: - return [] - - if self.__docstring_signature__ and self._new_docstrings is not None: - return self._new_docstrings - - docstring = getdoc( - self.props._obj, - self.get_attr, - self.config.autodoc_inherit_docstrings, - self.parent, - self.props.object_name, - ) - if docstring: - tab_width = self.directive.state.document.settings.tab_width - return [prepare_docstring(docstring, tab_width)] - return [] - - def process_doc(self, docstrings: list[list[str]]) -> Iterator[str]: - """Let the user process the docstrings before adding them.""" - for docstringlines in docstrings: - if self._events is not None: - # let extensions preprocess docstrings - self._events.emit( - 'autodoc-process-docstring', - self.objtype, - self.props.full_name, - self.props._obj, - self.options, - docstringlines, - ) - - if docstringlines and docstringlines[-1]: - # append a blank line to the end of the docstring - docstringlines.append('') - - yield from docstringlines - def get_sourcename(self) -> str: obj_module = inspect.safe_getattr(self.props._obj, '__module__', None) obj_qualname = inspect.safe_getattr(self.props._obj, '__qualname__', None) @@ -550,7 +498,7 @@ class Documenter: processed_doc = StringList( list( self._process_docstrings( - self._get_docstrings() or [], + self._get_docstrings(), events=self._events, props=self.props, obj=self.props._obj, @@ -580,8 +528,10 @@ class Documenter: def _get_docstrings(self) -> list[list[str]] | None: """Add content from docstrings, attribute documentation and user.""" - docstrings = self.get_doc() - attr_docs = None if self.analyzer is None else self.analyzer.find_attr_docs() + if self.props._docstrings is not None: + docstrings = [list(doc) for doc in self.props._docstrings] + else: + docstrings = None props = self.props if docstrings is not None and len(docstrings) == 0: @@ -593,6 +543,7 @@ class Documenter: if props.obj_type in {'data', 'attribute'}: return docstrings + attr_docs = None if self.analyzer is None else self.analyzer.find_attr_docs() if props.obj_type in {'class', 'exception'}: real_module = props._obj___module__ or props.module_name if props.module_name != real_module: @@ -616,7 +567,7 @@ class Documenter: @staticmethod def _process_docstrings( - docstrings: list[list[str]], + docstrings: list[list[str]] | None, *, events: EventManager, props: _ItemProperties, @@ -624,6 +575,8 @@ class Documenter: options: _AutoDocumenterOptions, ) -> Iterator[str]: """Let the user process the docstrings before adding them.""" + if docstrings is None: + return for docstring_lines in docstrings: # let extensions preprocess docstrings events.emit( @@ -806,10 +759,8 @@ class Documenter: except PycodeError: pass - docstrings: list[str] = functools.reduce( - operator.iadd, self.get_doc() or [], [] - ) - if ismock(self.props._obj) and not docstrings: + 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.name, @@ -1474,87 +1425,6 @@ class ClassDocumenter(Documenter): else: return None - def get_doc(self) -> list[list[str]] | None: - if isinstance(self.props._obj, TypeVar): - if self.props._obj.__doc__ == TypeVar.__doc__: - return [] - if self.props.doc_as_attr: - # Don't show the docstring of the class when it is an alias. - if self.get_variable_comment(): - return [] - else: - return None - - lines = getattr(self, '_new_docstrings', None) - if lines is not None: - return lines - - if self.options.class_doc_from is not None: - classdoc_from = self.options.class_doc_from - else: - classdoc_from = self.config.autoclass_content - - docstrings = [] - attrdocstring = getdoc(self.props._obj, self.get_attr) - if attrdocstring: - docstrings.append(attrdocstring) - - # for classes, what the "docstring" is can be controlled via a - # config value; the default is only the class docstring - if classdoc_from in {'both', 'init'}: - __init__ = self.get_attr(self.props._obj, '__init__', None) - initdocstring = getdoc( - __init__, - self.get_attr, - self.config.autodoc_inherit_docstrings, - self.props._obj, - '__init__', - ) - # for new-style classes, no __init__ means default __init__ - if initdocstring is not None and ( - initdocstring == object.__init__.__doc__ # for pypy - or initdocstring.strip() == object.__init__.__doc__ # for !pypy - ): - initdocstring = None - if not initdocstring: - # try __new__ - __new__ = self.get_attr(self.props._obj, '__new__', None) - initdocstring = getdoc( - __new__, - self.get_attr, - self.config.autodoc_inherit_docstrings, - self.props._obj, - '__new__', - ) - # for new-style classes, no __new__ means default __new__ - if initdocstring is not None and ( - initdocstring == object.__new__.__doc__ # for pypy - or initdocstring.strip() == object.__new__.__doc__ # for !pypy - ): - initdocstring = None - if initdocstring: - if classdoc_from == 'init': - docstrings = [initdocstring] - else: - docstrings.append(initdocstring) - - tab_width = self.directive.state.document.settings.tab_width - return [prepare_docstring(docstring, tab_width) for docstring in docstrings] - - def get_variable_comment(self) -> list[str] | None: - try: - key = ('', self.props.dotted_parts) - if self.props.doc_as_attr: - analyzer = ModuleAnalyzer.for_module(self.props.module_name) - else: - analyzer = ModuleAnalyzer.for_module( - self.props._obj___module__ or self.props.module_name - ) - analyzer.analyze() - return list(analyzer.attr_docs.get(key, [])) - except PycodeError: - return None - def generate( self, more_content: StringList | None = None, @@ -1622,39 +1492,6 @@ class DataDocumenter(Documenter): ) -> bool: return isinstance(parent, ModuleDocumenter) and isattr - def should_suppress_value_header(self) -> bool: - if self.props._obj is UNINITIALIZED_ATTR: - return True - else: - doc = self.get_doc() or [] - _docstring, metadata = separate_metadata( - '\n'.join(functools.reduce(operator.iadd, doc, [])) - ) - if 'hide-value' in metadata: - return True - - return False - - def get_module_comment(self, attrname: str) -> list[str] | None: - try: - analyzer = ModuleAnalyzer.for_module(self.props.module_name) - analyzer.analyze() - key = ('', attrname) - if key in analyzer.attr_docs: - return list(analyzer.attr_docs[key]) - except PycodeError: - pass - - return None - - def get_doc(self) -> list[list[str]] | None: - # Check the variable has a docstring-comment - comment = self.get_module_comment(self.props.name) - if comment: - return [comment] - else: - return super().get_doc() - class MethodDocumenter(Documenter): """Specialized Documenter subclass for methods (normal, static and class).""" @@ -1845,51 +1682,6 @@ class MethodDocumenter(Documenter): return func - def get_doc(self) -> list[list[str]] | None: - if self._new_docstrings is not None: - # docstring already returned previously, then modified due to - # ``__docstring_signature__ = True``. Just return the - # previously-computed result, so that we don't loose the processing. - return self._new_docstrings - if self.props.name == '__init__': - docstring = getdoc( - self.props._obj, - self.get_attr, - self.config.autodoc_inherit_docstrings, - self.parent, - self.props.object_name, - ) - if docstring is not None and ( - docstring == object.__init__.__doc__ # for pypy - or docstring.strip() == object.__init__.__doc__ # for !pypy - ): - docstring = None - if docstring: - tab_width = self.directive.state.document.settings.tab_width - return [prepare_docstring(docstring, tabsize=tab_width)] - else: - return [] - elif self.props.name == '__new__': - docstring = getdoc( - self.props._obj, - self.get_attr, - self.config.autodoc_inherit_docstrings, - self.parent, - self.props.object_name, - ) - if docstring is not None and ( - docstring == object.__new__.__doc__ # for pypy - or docstring.strip() == object.__new__.__doc__ # for !pypy - ): - docstring = None - if docstring: - tab_width = self.directive.state.document.settings.tab_width - return [prepare_docstring(docstring, tabsize=tab_width)] - else: - return [] - else: - return super().get_doc() - class AttributeDocumenter(Documenter): """Specialized Documenter subclass for attributes.""" @@ -1925,89 +1717,6 @@ class AttributeDocumenter(Documenter): return True return not inspect.isroutine(member) and not isinstance(member, type) - @property - def _is_non_data_descriptor(self) -> bool: - return not inspect.isattributedescriptor(self.props._obj) - - def should_suppress_value_header(self) -> bool: - if self.props._obj is SLOTS_ATTR: - return True - if self.props._obj is RUNTIME_INSTANCE_ATTRIBUTE: - return True - if self.props._obj is UNINITIALIZED_ATTR: - return True - if not self._is_non_data_descriptor or inspect.isgenericalias(self.props._obj): - return True - else: - doc = self.get_doc() - if doc: - _docstring, metadata = separate_metadata( - '\n'.join(functools.reduce(operator.iadd, doc, [])) - ) - if 'hide-value' in metadata: - return True - - return False - - def get_attribute_comment(self, parent: Any, attrname: str) -> list[str] | None: - return _get_attribute_comment( - parent=parent, obj_path=self.props.parts, attrname=attrname - ) - - def get_doc(self) -> list[list[str]] | None: - # Check the attribute has a docstring-comment - comment = _get_attribute_comment( - parent=self.parent, obj_path=self.props.parts, attrname=self.props.parts[-1] - ) - if comment: - return [comment] - - try: - # Disable `autodoc_inherit_docstring` temporarily to avoid to obtain - # a docstring from the value which descriptor returns unexpectedly. - # See: https://github.com/sphinx-doc/sphinx/issues/7805 - orig = self.config.autodoc_inherit_docstrings - self.config.autodoc_inherit_docstrings = False - - if self.props._obj is SLOTS_ATTR: - # support for __slots__ - try: - parent___slots__ = inspect.getslots(self.parent) - if parent___slots__ and ( - docstring := parent___slots__.get(self.props.name) - ): - docstring = prepare_docstring(docstring) - return [docstring] - else: - return [] - except ValueError as exc: - logger.warning( - __('Invalid __slots__ found on %s. Ignored.'), - (self.parent.__qualname__, exc), - type='autodoc', - ) - return [] - - if ( - self.props._obj is RUNTIME_INSTANCE_ATTRIBUTE - and _is_runtime_instance_attribute_not_commented( - parent=self.parent, obj_path=self.props.parts - ) - ): - return None - - if self.props._obj is UNINITIALIZED_ATTR: - return None - - if self._is_non_data_descriptor: - # the docstring of non-data descriptor is very probably - # the wrong thing to display - return None - - return super().get_doc() - finally: - self.config.autodoc_inherit_docstrings = orig - class PropertyDocumenter(Documenter): """Specialized Documenter subclass for properties.""" diff --git a/sphinx/ext/autodoc/_property_types.py b/sphinx/ext/autodoc/_property_types.py index ca136523e1..74e4aca162 100644 --- a/sphinx/ext/autodoc/_property_types.py +++ b/sphinx/ext/autodoc/_property_types.py @@ -48,6 +48,8 @@ class _ItemProperties: #: This item's docstring, as a sequence of lines docstring_lines: tuple[str, ...] + _docstrings: Sequence[Sequence[str]] | None = None + _docstrings_has_hide_value: bool = False _obj: Any _obj___module__: str | None diff --git a/sphinx/ext/autodoc/_renderer.py b/sphinx/ext/autodoc/_renderer.py index 53228c59e6..20782fe4cc 100644 --- a/sphinx/ext/autodoc/_renderer.py +++ b/sphinx/ext/autodoc/_renderer.py @@ -1,6 +1,5 @@ from __future__ import annotations -import re from typing import TYPE_CHECKING from sphinx.ext.autodoc._property_types import ( @@ -22,7 +21,6 @@ if TYPE_CHECKING: from sphinx.ext.autodoc._property_types import _ItemProperties logger = logging.getLogger('sphinx.ext.autodoc') -_hide_value_re = re.compile(r'^:meta \s*hide-value:( +|$)') def _directive_header_lines( @@ -30,7 +28,6 @@ def _directive_header_lines( *, autodoc_typehints: Literal['signature', 'description', 'none', 'both'], directive_name: str, - docstrings_has_hide_value: bool, is_final: bool, options: _AutoDocumenterOptions, props: _ItemProperties, @@ -140,7 +137,7 @@ def _directive_header_lines( if ( not options.no_value and props._obj_is_sentinel is None # not any sentinel - and not docstrings_has_hide_value + and not props._docstrings_has_hide_value and not props._obj_is_mock ): yield f' :value: {props._obj_repr_rst}' @@ -164,7 +161,7 @@ def _directive_header_lines( and props._obj_is_sentinel is None # not any sentinel and not props._obj_is_attribute_descriptor and not props._obj_is_generic_alias - and not docstrings_has_hide_value + and not props._docstrings_has_hide_value and not props._obj_is_mock ): yield f' :value: {props._obj_repr_rst}' diff --git a/sphinx/ext/autodoc/directive.py b/sphinx/ext/autodoc/directive.py index 0f27fd7df3..36e903f07a 100644 --- a/sphinx/ext/autodoc/directive.py +++ b/sphinx/ext/autodoc/directive.py @@ -133,6 +133,7 @@ class AutodocDirective(SphinxDirective): location=(self.env.current_document.docname, lineno), ) return [] + documenter_options._tab_width = self.state.document.settings.tab_width # generate the output params = DocumenterBridge( diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index 8724f23e70..6a396047cd 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -4,7 +4,9 @@ from __future__ import annotations import contextlib import importlib +import itertools import os +import re import sys import traceback import typing @@ -17,6 +19,7 @@ from typing import TYPE_CHECKING, NewType, TypeVar from weakref import WeakSet from sphinx.errors import PycodeError +from sphinx.ext.autodoc._docstrings import _get_docstring_lines from sphinx.ext.autodoc._property_types import ( _AssignStatementProperties, _ClassDefProperties, @@ -47,6 +50,7 @@ if TYPE_CHECKING: 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 _AutodocFuncProperty, _AutodocObjType class _AttrGetter(Protocol): @@ -56,6 +60,8 @@ if TYPE_CHECKING: _NATIVE_SUFFIXES: frozenset[str] = frozenset({'.pyx', *EXTENSION_SUFFIXES}) logger = logging.getLogger(__name__) +_hide_value_re = re.compile(r'^:meta \s*hide-value:( +|$)') + class _ImportedObject: #: module containing the object to document @@ -419,7 +425,7 @@ def _is_runtime_instance_attribute_not_commented( def _get_attribute_comment( parent: Any, obj_path: Sequence[str], attrname: str -) -> list[str] | None: +) -> tuple[str, ...] | None: for cls in inspect.getmro(parent): try: module = safe_getattr(cls, '__module__') @@ -430,7 +436,7 @@ def _get_attribute_comment( if qualname and obj_path: key = (qualname, attrname) if key in analyzer.attr_docs: - return list(analyzer.attr_docs[key]) + return tuple(analyzer.attr_docs[key]) except (AttributeError, PycodeError): pass @@ -467,6 +473,7 @@ def _load_object_by_name( env: BuildEnvironment, events: EventManager, get_attr: _AttrGetter, + options: _AutoDocumenterOptions, ) -> tuple[_ItemProperties, str | None, str | None, ModuleType | None, Any] | None: """Import and load the object given by *name*.""" parsed = _parse_name( @@ -777,6 +784,23 @@ def _load_object_by_name( _obj___module__=get_attr(obj, '__module__', None), ) + if options.class_doc_from is not None: + class_doc_from = options.class_doc_from + else: + class_doc_from = config.autoclass_content + props._docstrings = _get_docstring_lines( + props, + class_doc_from=class_doc_from, + get_attr=get_attr, + inherit_docstrings=config.autodoc_inherit_docstrings, + parent=parent, + tab_width=options._tab_width, + ) + for line in itertools.chain.from_iterable(props._docstrings or ()): + if _hide_value_re.match(line): + props._docstrings_has_hide_value = True + break + return props, args, retann, module, parent diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index d827a0e2ee..65088c38d0 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -1051,10 +1051,8 @@ def getdoc( return doc -def _getdoc_internal( - obj: Any, attrgetter: Callable[[Any, str, Any], Any] = safe_getattr -) -> str | None: - doc = attrgetter(obj, '__doc__', None) +def _getdoc_internal(obj: Any, /) -> str | None: + doc = safe_getattr(obj, '__doc__', None) if isinstance(doc, str): return doc return None diff --git a/tests/test_ext_autodoc/autodoc_util.py b/tests/test_ext_autodoc/autodoc_util.py index 0831a709e0..574bcb30e9 100644 --- a/tests/test_ext_autodoc/autodoc_util.py +++ b/tests/test_ext_autodoc/autodoc_util.py @@ -37,7 +37,6 @@ def do_autodoc( ) docoptions = _AutoDocumenterOptions.from_directive_options(opts) state = Mock() - state.document.settings.tab_width = 8 bridge = DocumenterBridge(app.env, LoggingReporter(''), docoptions, 1, state) documenter = doccls(bridge, name) documenter.generate() diff --git a/tests/test_ext_autodoc/test_ext_autodoc.py b/tests/test_ext_autodoc/test_ext_autodoc.py index b0a1eacc9a..b744998af9 100644 --- a/tests/test_ext_autodoc/test_ext_autodoc.py +++ b/tests/test_ext_autodoc/test_ext_autodoc.py @@ -6,9 +6,7 @@ source file translated by test_build. from __future__ import annotations -import functools import itertools -import operator import sys from typing import TYPE_CHECKING from unittest.mock import Mock @@ -21,6 +19,7 @@ from sphinx.ext.autodoc._directive_options import ( _AutoDocumenterOptions, inherited_members_option, ) +from sphinx.ext.autodoc._docstrings import _get_docstring_lines from sphinx.ext.autodoc._documenters import ModuleLevelDocumenter from sphinx.ext.autodoc._property_types import ( _ClassDefProperties, @@ -75,7 +74,6 @@ def make_directive_bridge(env: BuildEnvironment) -> DocumenterBridge: lineno=0, state=Mock(), ) - directive.state.document.settings.tab_width = 8 return directive @@ -177,6 +175,14 @@ def test_format_signature(app): _obj_is_new_type=False, _obj_is_typevar=False, ) + inst.props._docstrings = _get_docstring_lines( + inst.props, + class_doc_from=inst.config.autoclass_content, + get_attr=inst.get_attr, + inherit_docstrings=inst.config.autodoc_inherit_docstrings, + parent=inst.parent, + tab_width=8, + ) res = inst.format_signature() print(res) return res @@ -283,10 +289,11 @@ def test_format_signature(app): """some docstring for F2.""" def __init__(self, *args, **kw): - """__init__(a1, a2, kw1=True, kw2=False) + """ + __init__(a1, a2, kw1=True, kw2=False) some docstring for __init__. - """ + """ # NoQA: D212 class G2(F2): pass @@ -398,10 +405,19 @@ def test_get_doc(app): ) inst.parent = object # dummy inst.doc_as_attr = False + inst.props._docstrings = _get_docstring_lines( + inst.props, + class_doc_from=inst.config.autoclass_content, + get_attr=inst.get_attr, + inherit_docstrings=inst.config.autodoc_inherit_docstrings, + parent=inst.parent, + tab_width=8, + ) + inst.format_signature() # handle docstring signatures! - ds = inst.get_doc() + ds = inst.props._docstrings # for testing purposes, concat them and strip the empty line at the end - res = functools.reduce(operator.iadd, ds, [])[:-1] + res = list(itertools.chain.from_iterable(ds))[:-1] print(res) return res @@ -416,7 +432,9 @@ def test_get_doc(app): """Docstring""" def g(): - """Docstring""" + """ + Docstring + """ # NoQA: D212 for func in (f, g): assert getdocl('function', func) == ['Docstring']