diff --git a/CHANGES b/CHANGES index 3ef0bb53eaf..da8580fe67a 100644 --- a/CHANGES +++ b/CHANGES @@ -28,6 +28,8 @@ Features added * #9195: autodoc: The arguments of ``typing.Literal`` are wrongly rendered * #9185: autodoc: :confval:`autodoc_typehints` allows ``'both'`` setting to allow typehints to be included both in the signature and description +* #4257: autodoc: Add :confval:`autodoc_class_signature` to separate the class + entry and the definition of ``__init__()`` method * #3257: autosummary: Support instance attributes for classes * #9129: html search: Show search summaries when html_copy_source = False * #9120: html theme: Eliminate prompt characters of code-block from copyable diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index e547e698b11..2c9e19a8e50 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 how the signautre will be displayed for the class defined + by :rst:dir:`autoclass` directive. The possible values are: + + ``"mixed"`` + Display the signature with the class name. + ``"separated"`` + Display the signature as a 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 c6449613992..1f4c98284bc 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.""" @@ -1440,6 +1443,15 @@ 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'] = {'__new__', '__init__'} + else: + self.options.special_members.append('__new__') + self.options.special_members.append('__init__') + merge_members_option(self.options) @classmethod @@ -1556,6 +1568,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 +2208,38 @@ 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) + 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 [] + elif self.objpath[-1] == '__new__': + __new__ = self.get_attr(self.object, '__new__', None) + if __new__: + docstring = getdoc(__new__, self.get_attr, + self.config.autodoc_inherit_docstrings, + self.parent, self.object_name) + if (docstring is not None and + (docstring == object.__new__.__doc__ or # for pypy + 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 NonDataDescriptorMixin(DataDocumenterMixinBase): """ @@ -2662,6 +2709,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/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index be9f0cc650c..0bf921f0bad 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -174,6 +174,7 @@ def __init__(self) -> None: document = Struct(settings=settings) env = BuildEnvironment() env.config = Config() + env.config.add('autodoc_class_signature', 'mixed', True, None) state = Struct(document=document) super().__init__(env, None, Options(), 0, state) diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index b9bed16352e..e0f08ea7701 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -140,6 +140,57 @@ 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.Bar', options) + assert list(actual) == [ + '', + '.. py:class:: Bar(x, y)', + ' :module: target.classes', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_class_signature_separated_init(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_autodoc_class_signature_separated_new(app): + app.config.autodoc_class_signature = 'separated' + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'class', 'target.classes.Baz', options) + assert list(actual) == [ + '', + '.. py:class:: Baz', + ' :module: target.classes', + '', + '', + ' .. py:method:: Baz.__new__(cls, x, y)', + ' :module: target.classes', + ' :staticmethod:', + '', + ] + + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autoclass_content_both(app): app.config.autoclass_content = 'both'