Skip to content

Commit 99e5ccb

Browse files
authored
Consolidate add_content() handling (#13936)
1 parent f3e8999 commit 99e5ccb

File tree

2 files changed

+172
-128
lines changed

2 files changed

+172
-128
lines changed

sphinx/ext/autodoc/_documenters.py

Lines changed: 162 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
)
2424
from sphinx.ext.autodoc._member_finder import _filter_members, _get_members_to_document
2525
from sphinx.ext.autodoc._renderer import (
26+
_add_content,
2627
_directive_header_lines,
2728
_hide_value_re,
2829
)
@@ -545,42 +546,170 @@ def get_sourcename(self) -> str:
545546

546547
def add_content(self, more_content: StringList | None) -> None:
547548
"""Add content from docstrings, attribute documentation and user."""
548-
docstring = True
549-
550-
# set sourcename and add content from attribute documentation
551-
sourcename = self.get_sourcename()
552-
if self.analyzer:
553-
attr_docs = self.analyzer.find_attr_docs()
554-
if self.props.parts:
555-
key = ('.'.join(self.props.parts[:-1]), self.props.parts[-1])
556-
if key in attr_docs:
557-
docstring = False
558-
# make a copy of docstring for attributes to avoid cache
559-
# the change of autodoc-process-docstring event.
560-
attribute_docstrings = [list(attr_docs[key])]
561-
562-
for i, line in enumerate(self.process_doc(attribute_docstrings)):
563-
self.add_line(line, sourcename, i)
564-
565549
# add content from docstrings
566-
if docstring:
567-
docstrings = self.get_doc()
568-
if docstrings is None:
569-
# Do not call autodoc-process-docstring on get_doc() returns None.
570-
pass
571-
else:
572-
if not docstrings:
573-
# append at least a dummy docstring, so that the event
574-
# autodoc-process-docstring is fired and can add some
575-
# content if desired
576-
docstrings.append([])
577-
for i, line in enumerate(self.process_doc(docstrings)):
578-
self.add_line(line, sourcename, i)
550+
processed_doc = StringList(
551+
list(
552+
self._process_docstrings(
553+
self._get_docstrings() or [],
554+
events=self._events,
555+
props=self.props,
556+
obj=self.props._obj,
557+
options=self.options,
558+
)
559+
),
560+
source=self.get_sourcename(),
561+
)
562+
_add_content(
563+
processed_doc,
564+
result=self.directive.result,
565+
indent=self.indent + ' ' * (self.props.obj_type == 'module'),
566+
)
579567

580568
# add additional content (e.g. from document), if present
581-
if more_content:
582-
for line, src in zip(more_content.data, more_content.items, strict=True):
583-
self.add_line(line, src[0], src[1])
569+
more_content = self._assemble_more_content(
570+
more_content=StringList() if more_content is None else more_content,
571+
typehints_format=self.config.autodoc_typehints_format,
572+
python_display_short_literal_types=self.config.python_display_short_literal_types,
573+
props=self.props,
574+
)
575+
_add_content(
576+
more_content,
577+
result=self.directive.result,
578+
indent=self.indent,
579+
)
580+
581+
def _get_docstrings(self) -> list[list[str]] | None:
582+
"""Add content from docstrings, attribute documentation and user."""
583+
docstrings = self.get_doc()
584+
attr_docs = None if self.analyzer is None else self.analyzer.find_attr_docs()
585+
props = self.props
586+
587+
if docstrings is not None and len(docstrings) == 0:
588+
# append at least a dummy docstring, so that the event
589+
# autodoc-process-docstring is fired and can add some
590+
# content if desired
591+
docstrings.append([])
592+
593+
if props.obj_type in {'data', 'attribute'}:
594+
return docstrings
595+
596+
if props.obj_type in {'class', 'exception'}:
597+
real_module = props._obj___module__ or props.module_name
598+
if props.module_name != real_module:
599+
try:
600+
# override analyzer to obtain doc-comment around its definition.
601+
ma = ModuleAnalyzer.for_module(props.module_name)
602+
ma.analyze()
603+
attr_docs = ma.attr_docs
604+
except PycodeError:
605+
pass
606+
607+
# add content from attribute documentation
608+
if attr_docs is not None and props.parts:
609+
key = ('.'.join(props.parent_names), props.name)
610+
if key in attr_docs:
611+
# make a copy of docstring for attributes to avoid cache
612+
# the change of autodoc-process-docstring event.
613+
return [list(attr_docs[key])]
614+
615+
return docstrings
616+
617+
@staticmethod
618+
def _process_docstrings(
619+
docstrings: list[list[str]],
620+
*,
621+
events: EventManager,
622+
props: _ItemProperties,
623+
obj: Any,
624+
options: _AutoDocumenterOptions,
625+
) -> Iterator[str]:
626+
"""Let the user process the docstrings before adding them."""
627+
for docstring_lines in docstrings:
628+
# let extensions preprocess docstrings
629+
events.emit(
630+
'autodoc-process-docstring',
631+
props.obj_type,
632+
props.full_name,
633+
obj,
634+
options,
635+
docstring_lines,
636+
)
637+
638+
if docstring_lines and docstring_lines[-1]:
639+
# append a blank line to the end of the docstring
640+
docstring_lines.append('')
641+
642+
yield from docstring_lines
643+
644+
@staticmethod
645+
def _assemble_more_content(
646+
more_content: StringList,
647+
*,
648+
props: _ItemProperties,
649+
typehints_format: Literal['fully-qualified', 'short'],
650+
python_display_short_literal_types: bool,
651+
) -> StringList:
652+
"""Add content from docstrings, attribute documentation and user."""
653+
obj = props._obj
654+
655+
if props.obj_type in {'data', 'attribute'}:
656+
mode = _get_render_mode(typehints_format)
657+
658+
# Support for documenting GenericAliases
659+
if inspect.isgenericalias(obj):
660+
alias = restify(obj, mode=mode)
661+
more_content.append(_('alias of %s') % alias, '')
662+
more_content.append('', '')
663+
return more_content
664+
665+
if props.obj_type in {'class', 'exception'}:
666+
from sphinx.ext.autodoc._property_types import _ClassDefProperties
667+
668+
assert isinstance(props, _ClassDefProperties)
669+
670+
mode = _get_render_mode(typehints_format)
671+
672+
if isinstance(obj, NewType):
673+
supertype = restify(obj.__supertype__, mode=mode)
674+
return StringList([_('alias of %s') % supertype, ''], source='')
675+
676+
if isinstance(obj, TypeVar):
677+
short_literals = python_display_short_literal_types
678+
attrs = [
679+
repr(obj.__name__),
680+
*(
681+
stringify_annotation(
682+
constraint, mode, short_literals=short_literals
683+
)
684+
for constraint in obj.__constraints__
685+
),
686+
]
687+
if obj.__bound__:
688+
attrs.append(rf'bound=\ {restify(obj.__bound__, mode=mode)}')
689+
if obj.__covariant__:
690+
attrs.append('covariant=True')
691+
if obj.__contravariant__:
692+
attrs.append('contravariant=True')
693+
694+
alias = f'TypeVar({", ".join(attrs)})'
695+
return StringList([_('alias of %s') % alias, ''], source='')
696+
697+
if props.doc_as_attr:
698+
try:
699+
analyzer = ModuleAnalyzer.for_module(props.module_name)
700+
analyzer.analyze()
701+
key = ('', props.dotted_parts)
702+
no_classvar_doc_comment = key not in analyzer.attr_docs
703+
except PycodeError:
704+
no_classvar_doc_comment = True
705+
706+
if no_classvar_doc_comment:
707+
alias = restify(obj, mode=mode)
708+
return StringList([_('alias of %s') % alias], source='')
709+
710+
return more_content
711+
712+
return more_content
584713

585714
def sort_members(
586715
self, documenters: list[tuple[Documenter, bool]], order: str
@@ -863,15 +992,6 @@ def __init__(self, *args: Any) -> None:
863992
self.options = self.options.merge_member_options()
864993
self.__all__: Sequence[str] | None = None
865994

866-
def add_content(self, more_content: StringList | None) -> None:
867-
old_indent = self.indent
868-
self.indent += self._extra_indent
869-
super().add_content(None)
870-
self.indent = old_indent
871-
if more_content:
872-
for line, src in zip(more_content.data, more_content.items, strict=True):
873-
self.add_line(line, src[0], src[1])
874-
875995
@classmethod
876996
def can_document_member(
877997
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
@@ -1435,50 +1555,6 @@ def get_variable_comment(self) -> list[str] | None:
14351555
except PycodeError:
14361556
return None
14371557

1438-
def add_content(self, more_content: StringList | None) -> None:
1439-
mode = _get_render_mode(self.config.autodoc_typehints_format)
1440-
short_literals = self.config.python_display_short_literal_types
1441-
1442-
if isinstance(self.props._obj, NewType):
1443-
supertype = restify(self.props._obj.__supertype__, mode=mode)
1444-
1445-
more_content = StringList([_('alias of %s') % supertype, ''], source='')
1446-
if isinstance(self.props._obj, TypeVar):
1447-
attrs = [repr(self.props._obj.__name__)]
1448-
attrs.extend(
1449-
stringify_annotation(constraint, mode, short_literals=short_literals)
1450-
for constraint in self.props._obj.__constraints__
1451-
)
1452-
if self.props._obj.__bound__:
1453-
bound = restify(self.props._obj.__bound__, mode=mode)
1454-
attrs.append(r'bound=\ ' + bound)
1455-
if self.props._obj.__covariant__:
1456-
attrs.append('covariant=True')
1457-
if self.props._obj.__contravariant__:
1458-
attrs.append('contravariant=True')
1459-
1460-
more_content = StringList(
1461-
[_('alias of TypeVar(%s)') % ', '.join(attrs), ''], source=''
1462-
)
1463-
if self.props.doc_as_attr and self.props.module_name != (
1464-
self.props._obj___module__ or self.props.module_name
1465-
):
1466-
try:
1467-
# override analyzer to obtain doccomment around its definition.
1468-
self.analyzer = ModuleAnalyzer.for_module(self.props.module_name)
1469-
self.analyzer.analyze()
1470-
except PycodeError:
1471-
pass
1472-
1473-
if self.props.doc_as_attr and not self.get_variable_comment():
1474-
try:
1475-
alias = restify(self.props._obj, mode=mode)
1476-
more_content = StringList([_('alias of %s') % alias], source='')
1477-
except AttributeError:
1478-
pass # Invalid class object is passed.
1479-
1480-
super().add_content(more_content)
1481-
14821558
def generate(
14831559
self,
14841560
more_content: StringList | None = None,
@@ -1579,21 +1655,6 @@ def get_doc(self) -> list[list[str]] | None:
15791655
else:
15801656
return super().get_doc()
15811657

1582-
def add_content(self, more_content: StringList | None) -> None:
1583-
# Disable analyzing variable comment on Documenter.add_content() to control it on
1584-
# DataDocumenter.add_content()
1585-
self.analyzer = None
1586-
1587-
if not more_content:
1588-
more_content = StringList()
1589-
1590-
_add_content_generic_alias_(
1591-
more_content,
1592-
self.props._obj,
1593-
autodoc_typehints_format=self.config.autodoc_typehints_format,
1594-
)
1595-
super().add_content(more_content)
1596-
15971658

15981659
class MethodDocumenter(Documenter):
15991660
"""Specialized Documenter subclass for methods (normal, static and class)."""
@@ -1947,20 +2008,6 @@ def get_doc(self) -> list[list[str]] | None:
19472008
finally:
19482009
self.config.autodoc_inherit_docstrings = orig
19492010

1950-
def add_content(self, more_content: StringList | None) -> None:
1951-
# Disable analyzing attribute comment on Documenter.add_content() to control it on
1952-
# AttributeDocumenter.add_content()
1953-
self.analyzer = None
1954-
1955-
if more_content is None:
1956-
more_content = StringList()
1957-
_add_content_generic_alias_(
1958-
more_content,
1959-
self.props._obj,
1960-
autodoc_typehints_format=self.config.autodoc_typehints_format,
1961-
)
1962-
super().add_content(more_content)
1963-
19642011

19652012
class PropertyDocumenter(Documenter):
19662013
"""Specialized Documenter subclass for properties."""
@@ -2069,19 +2116,6 @@ def autodoc_attrgetter(
20692116
return safe_getattr(obj, name, *defargs)
20702117

20712118

2072-
def _add_content_generic_alias_(
2073-
more_content: StringList,
2074-
/,
2075-
obj: object,
2076-
autodoc_typehints_format: Literal['fully-qualified', 'short'],
2077-
) -> None:
2078-
"""Support for documenting GenericAliases."""
2079-
if inspect.isgenericalias(obj):
2080-
alias = restify(obj, mode=_get_render_mode(autodoc_typehints_format))
2081-
more_content.append(_('alias of %s') % alias, '')
2082-
more_content.append('', '')
2083-
2084-
20852119
def _document_members(
20862120
*,
20872121
member_documenters: list[tuple[Documenter, bool]],

sphinx/ext/autodoc/_renderer.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from collections.abc import Iterator
1717
from typing import Literal
1818

19+
from docutils.statemachine import StringList
20+
1921
from sphinx.ext.autodoc._directive_options import _AutoDocumenterOptions
2022
from sphinx.ext.autodoc._property_types import _ItemProperties
2123

@@ -166,3 +168,11 @@ def _directive_header_lines(
166168
and not props._obj_is_mock
167169
):
168170
yield f' :value: {props._obj_repr_rst}'
171+
172+
173+
def _add_content(content: StringList, *, result: StringList, indent: str) -> None:
174+
for line, src in zip(content.data, content.items, strict=True):
175+
if line.strip(): # not a blank line
176+
result.append(indent + line, src[0], src[1])
177+
else:
178+
result.append('', src[0], src[1])

0 commit comments

Comments
 (0)