diff --git a/sphinx/ext/autodoc/_directive_options.py b/sphinx/ext/autodoc/_directive_options.py index b3f42d96fa4..8021402fe57 100644 --- a/sphinx/ext/autodoc/_directive_options.py +++ b/sphinx/ext/autodoc/_directive_options.py @@ -89,20 +89,6 @@ def copy(self) -> Self: def from_directive_options(cls, opts: Mapping[str, Any], /) -> Self: return cls(**{k.replace('-', '_'): v for k, v in opts.items() if v is not None}) - def merge_member_options(self) -> Self: - """Merge :private-members: and :special-members: into :members:""" - if self.members is ALL: - # merging is not needed when members: ALL - return self - - members = self.members or [] - for others in self.private_members, self.special_members: - if others is not None and others is not ALL: - members.extend(others) - new = self.copy() - new.members = list(dict.fromkeys(members)) # deduplicate; preserve order - return new - def identity(x: Any) -> Any: return x diff --git a/sphinx/ext/autodoc/_documenters.py b/sphinx/ext/autodoc/_documenters.py index 24c7980b045..c33cf5a4c27 100644 --- a/sphinx/ext/autodoc/_documenters.py +++ b/sphinx/ext/autodoc/_documenters.py @@ -100,16 +100,6 @@ def __init__( # the module analyzer to get at attribute docs, or None self.analyzer: ModuleAnalyzer | None = None - if isinstance(self, ModuleDocumenter): - self.options = self.options.merge_member_options() - elif isinstance(self, ClassDocumenter): - if self.config.autodoc_class_signature == 'separated': - # show __init__() method - if self.options.special_members is None: - self.options.special_members = [] - self.options.special_members += ['__new__', '__init__'] - self.options = self.options.merge_member_options() - 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 diff --git a/sphinx/ext/autodoc/_member_finder.py b/sphinx/ext/autodoc/_member_finder.py index de27b88dcd3..4ecc3c826eb 100644 --- a/sphinx/ext/autodoc/_member_finder.py +++ b/sphinx/ext/autodoc/_member_finder.py @@ -193,10 +193,13 @@ def _gather_members( found_members = _get_members_to_document( want_all=want_all, get_attr=get_attr, + class_signature=config.autodoc_class_signature, inherit_docstrings=config.autodoc_inherit_docstrings, props=props, opt_members=options.members or (), inherited_members=inherited_members, + opt_private_members=options.private_members, + opt_special_members=options.special_members, ignore_module_all=bool(options.ignore_module_all), attr_docs=attr_docs, ) @@ -205,6 +208,7 @@ def _gather_members( want_all=want_all, events=events, get_attr=get_attr, + class_signature=config.autodoc_class_signature, inherit_docstrings=config.autodoc_inherit_docstrings, options=options, props=props, @@ -229,12 +233,10 @@ def _gather_members( if not obj_type: # don't know how to document this member continue - doccls = registry.documenters[obj_type] # give explicitly separated module name, so that members # of inner classes can be documented dotted_parts = '.'.join((*props.parts, member_name)) full_name = f'{props.module_name}::{dotted_parts}' - documenter = doccls(directive, full_name, indent) # We now try to import all objects before ordering them. This is to # avoid possible circular imports if we were to import objects after @@ -249,10 +251,13 @@ def _gather_members( env=env, events=events, get_attr=get_attr, - options=documenter.options, + options=directive.genopt, ) if member_props is None: continue + + doccls = registry.documenters[obj_type] + documenter = doccls(directive, full_name, indent) documenter.props = member_props member_documenters.append((documenter, is_attr)) @@ -277,10 +282,13 @@ def _get_members_to_document( *, want_all: bool, get_attr: _AttrGetter, + class_signature: Literal['mixed', 'separated'], inherit_docstrings: bool, props: _ModuleProperties | _ClassDefProperties, opt_members: ALL_T | Sequence[str], inherited_members: Set[str], + opt_private_members: ALL_T | Sequence[str] | None, + opt_special_members: ALL_T | Sequence[str] | None, ignore_module_all: bool, attr_docs: dict[tuple[str, str], list[str]], ) -> list[ObjectMember]: @@ -315,7 +323,16 @@ def _get_members_to_document( else: # specific members given assert opt_members is not ALL - wanted_members = frozenset(opt_members) + + # Merge :private-members: and :special-members: into :members: + combined_members = set(opt_members) + if opt_private_members is not None and opt_private_members is not ALL: + combined_members.update(opt_private_members) + if opt_special_members is not None and opt_special_members is not ALL: + combined_members.update(opt_special_members) + if class_signature == 'separated' and props.obj_type in {'class', 'exception'}: + combined_members |= {'__new__', '__init__'} # show __init__() method + wanted_members = frozenset(combined_members) object_members_map: dict[str, ObjectMember] = {} if props.obj_type == 'module': @@ -489,6 +506,7 @@ def _filter_members( get_attr: _AttrGetter, options: _AutoDocumenterOptions, props: _ModuleProperties | _ClassDefProperties, + class_signature: Literal['mixed', 'separated'], inherit_docstrings: bool, inherited_members: Set[str], exclude_members: EMPTY_T | Set[str] | None, @@ -513,6 +531,7 @@ def _filter_members( member_cls=obj.class_, get_attr=get_attr, has_attr_doc=has_attr_doc, + class_signature=class_signature, inherit_docstrings=inherit_docstrings, inherited_members=inherited_members, parent=props._obj, @@ -793,6 +812,7 @@ def _should_keep_member( member_cls: Any, get_attr: _AttrGetter, has_attr_doc: bool, + class_signature: Literal['mixed', 'separated'], inherit_docstrings: bool, inherited_members: Set[str], parent: Any, @@ -862,12 +882,16 @@ def _should_keep_member( if special_member_re.match(member_name): # special __methods__ + if member_name == '__doc__' or is_filtered_inherited_member: + return False if special_members and member_name in special_members: - if member_name == '__doc__': # NoQA: SIM114 - return False - elif is_filtered_inherited_member: - return False return has_doc + if ( + class_signature == 'separated' + and member_name in {'__new__', '__init__'} + and inspect.isclass(parent) + ): + return has_doc # show __init__() method return False if is_private: diff --git a/sphinx/ext/autodoc/directive.py b/sphinx/ext/autodoc/directive.py index 33099dc186f..3394689b551 100644 --- a/sphinx/ext/autodoc/directive.py +++ b/sphinx/ext/autodoc/directive.py @@ -142,10 +142,6 @@ def run(self) -> list[Node]: # generate the output get_attr = _AutodocAttrGetter(self.env._registry.autodoc_attrgetters) - params = DocumenterBridge( - self.env, reporter, documenter_options, lineno, self.state, get_attr - ) - documenter = doccls(params, self.arguments[0]) props = _load_object_by_name( name=self.arguments[0], objtype=objtype, # type: ignore[arg-type] @@ -156,11 +152,17 @@ def run(self) -> list[Node]: env=self.env, events=self.env.events, get_attr=get_attr, - options=documenter.options, + options=documenter_options, + ) + if props is None: + return [] + + params = DocumenterBridge( + self.env, reporter, documenter_options, lineno, self.state, get_attr ) - if props is not None: - documenter.props = props - documenter._generate(more_content=self.content) + documenter = doccls(params, self.arguments[0]) + documenter.props = props + documenter._generate(more_content=self.content) if not params.result: return [] diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index 474e3826962..a510fc17a2c 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -369,7 +369,6 @@ def get_items(self, names: list[str]) -> list[tuple[str, str | None, str, str]]: result = StringList() # initialize for each documenter obj_type = _get_documenter(obj, parent) - doccls = env._registry.documenters[obj_type] if isinstance(obj, ModuleType): full_name = real_name else: @@ -379,7 +378,6 @@ def get_items(self, names: list[str]) -> list[tuple[str, str | None, str, str]]: # NB. using full_name here is important, since Documenters # handle module prefixes slightly differently self.bridge.result = result - documenter = doccls(self.bridge, full_name) props = _load_object_by_name( name=full_name, objtype=obj_type, @@ -390,7 +388,7 @@ def get_items(self, names: list[str]) -> list[tuple[str, str | None, str, str]]: env=env, events=events, get_attr=get_attr, - options=documenter.options, + options=self.bridge.genopt, ) if props is None: logger.warning( @@ -400,7 +398,6 @@ def get_items(self, names: list[str]) -> list[tuple[str, str | None, str, str]]: ) items.append((display_name, '', '', real_name)) continue - documenter.props = props # try to also get a source code analyzer for attribute docs real_module = props._obj___module__ or props.module_name @@ -413,7 +410,6 @@ def get_items(self, names: list[str]) -> list[tuple[str, str | None, str, str]]: logger.debug('[autodoc] module analyzer failed: %s', err) # no source file -- e.g. for builtin and C modules analyzer = None - documenter.analyzer = analyzer # -- Grab the signature @@ -429,6 +425,10 @@ def get_items(self, names: list[str]) -> list[tuple[str, str | None, str, str]]: # -- Grab the summary + doccls = env._registry.documenters[obj_type] + documenter = doccls(self.bridge, full_name) + documenter.props = props + documenter.analyzer = analyzer documenter.add_content(None, indent=documenter.indent) lines = result.data[:] if props.obj_type != 'module': diff --git a/tests/test_ext_autodoc/autodoc_util.py b/tests/test_ext_autodoc/autodoc_util.py index 6e9a086341f..f559e0a95d3 100644 --- a/tests/test_ext_autodoc/autodoc_util.py +++ b/tests/test_ext_autodoc/autodoc_util.py @@ -3,6 +3,8 @@ from typing import TYPE_CHECKING from unittest.mock import Mock +from docutils.statemachine import StringList + from sphinx.ext.autodoc._directive_options import ( _AutoDocumenterOptions, _process_documenter_options, @@ -17,8 +19,6 @@ if TYPE_CHECKING: from typing import Any - from docutils.statemachine import StringList - from sphinx.application import Sphinx from sphinx.ext.autodoc._property_types import _AutodocObjType @@ -38,12 +38,7 @@ def do_autodoc( default_options=app.config.autodoc_default_options, options=options, ) - docoptions = _AutoDocumenterOptions.from_directive_options(opts) - state = Mock() - bridge = DocumenterBridge( - app.env, LoggingReporter(''), docoptions, 1, state, safe_getattr - ) - documenter = doccls(bridge, name) + doc_options = _AutoDocumenterOptions.from_directive_options(opts) props = _load_object_by_name( name=name, objtype=obj_type, @@ -54,9 +49,16 @@ def do_autodoc( env=app.env, events=app.events, get_attr=safe_getattr, - options=documenter.options, + options=doc_options, ) + result = StringList() if props is not None: + state = Mock() + bridge = DocumenterBridge( + app.env, LoggingReporter(''), doc_options, 1, state, safe_getattr + ) + bridge.result = result + documenter = doccls(bridge, name) documenter.props = props documenter._generate() - return bridge.result + return result diff --git a/tests/test_ext_autodoc/test_ext_autodoc.py b/tests/test_ext_autodoc/test_ext_autodoc.py index 5238d5be583..34aea68ca0b 100644 --- a/tests/test_ext_autodoc/test_ext_autodoc.py +++ b/tests/test_ext_autodoc/test_ext_autodoc.py @@ -598,8 +598,6 @@ def _special_getattr(obj, attr_name, *defargs): def _assert_getter_works(app, directive, objtype, name, *attrs): getattr_spy.clear() - doccls = app.registry.documenters[objtype] - documenter = doccls(directive, name) props = _load_object_by_name( name=name, objtype=objtype, @@ -610,9 +608,11 @@ def _assert_getter_works(app, directive, objtype, name, *attrs): env=app.env, events=app.events, get_attr=directive.get_attr, - options=documenter.options, + options=directive.genopt, ) if props is not None: + doccls = app.registry.documenters[objtype] + documenter = doccls(directive, name) documenter.props = props documenter._generate()