diff --git a/CHANGES b/CHANGES index ce70aeb83c4..8711186af8d 100644 --- a/CHANGES +++ b/CHANGES @@ -21,6 +21,8 @@ Features added * #8588: autodoc: :confval:`autodoc_type_aliases` now supports dotted name. It allows you to define an alias for a class with module name like ``foo.bar.BazClass`` +* #4257: autodoc: Add :confval:`autodoc_class_signature` to separate the class + entry and the definition of ``__init__()`` method * #9129: html search: Show search summaries when html_copy_source = False * #9120: html theme: Eliminate prompt characters of code-block from copyable text diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index 13a2b30107f..2eb16a7c072 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -463,6 +463,20 @@ There are also config values that you can set: .. versionadded:: 1.4 +.. confval:: autodoc_class_signature + + This value selects what content will be shown as the class signature of an + :rst:dir:`autoclass` directive. The possible values are: + + ``"mixed"`` + Display the signature with its class name. + ``"separated"`` + Separate the class entry and the definition of ``__init__()`` method + + The default is ``"mixed"``. + + .. versionadded:: 4.1 + .. confval:: autodoc_member_order This value selects if automatically documented members are sorted diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index ff6475c9442..a2e34563145 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -70,6 +70,9 @@ class _All: def __contains__(self, item: Any) -> bool: return True + def append(self, item: Any) -> None: + pass # nothing + class _Empty: """A special value for :exclude-members: that never matches to any member.""" @@ -1439,6 +1442,14 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: def __init__(self, *args: Any) -> None: super().__init__(*args) + + if self.config.autodoc_class_signature == 'separated': + # show __init__() method + if self.options.special_members is None: + self.options.special_members = {'__init__'} + else: + self.options.special_members.append('__init__') + merge_members_option(self.options) @classmethod @@ -1555,6 +1566,9 @@ def format_args(self, **kwargs: Any) -> str: def format_signature(self, **kwargs: Any) -> str: if self.doc_as_attr: return '' + if self.config.autodoc_class_signature == 'separated': + # do not show signatures + return '' sig = super().format_signature() sigs = [] @@ -2193,6 +2207,24 @@ def dummy(): else: return None + def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]: + if self.objpath[-1] == '__init__': + docstring = getdoc(self.object, self.get_attr, + self.config.autodoc_inherit_docstrings, + self.parent, self.object_name) + # for new-style classes, no __init__ means default __init__ + if (docstring is not None and + (docstring == object.__init__.__doc__ or # for pypy + 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 [] + else: + return super().get_doc() # type: ignore + class NonDataDescriptorMixin(DataDocumenterMixinBase): """ @@ -2662,6 +2694,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('autoclass_content', 'class', True, ENUM('both', 'class', 'init')) app.add_config_value('autodoc_member_order', 'alphabetical', True, ENUM('alphabetic', 'alphabetical', 'bysource', 'groupwise')) + app.add_config_value('autodoc_class_signature', 'mixed', True, ENUM('mixed', 'separated')) app.add_config_value('autodoc_default_options', {}, True) app.add_config_value('autodoc_docstring_signature', True, True) app.add_config_value('autodoc_mock_imports', [], True) diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index 04d35e3359a..4bddbf76e36 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -140,6 +140,38 @@ def test_autoclass_content_init(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_class_signature_mixed(app): + app.config.autodoc_class_signature = 'mixed' + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'class', 'target.classes.Baz', options) + assert list(actual) == [ + '', + '.. py:class:: Baz(x, y)', + ' :module: target.classes', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_class_signature_separated(app): + app.config.autodoc_class_signature = 'separated' + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'class', 'target.classes.Bar', options) + assert list(actual) == [ + '', + '.. py:class:: Bar', + ' :module: target.classes', + '', + '', + ' .. py:method:: Bar.__init__(x, y)', + ' :module: target.classes', + '', + ] + + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autoclass_content_both(app): app.config.autoclass_content = 'both'