diff --git a/CHANGES b/CHANGES index d6b4135b9f0..d15819da529 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,9 @@ Deprecated Features added -------------- +* #8107: autodoc: Add ``class-doc-from`` option to :rst:dir:`autoclass` + directive to control the content of the specific class like + :confval:`autoclass_content` * #9129: html search: Show search summaries when html_copy_source = False * #9120: html theme: Eliminate prompt characters of code-block from copyable text @@ -24,6 +27,7 @@ Features added Bugs fixed ---------- +* #8872: autodoc: stacked singledispatches are wrongly rendered * #8597: autodoc: a docsting having metadata only should be treated as undocumented diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index da0ff7c994c..13a2b30107f 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -343,6 +343,10 @@ inserting them into the page source under a suitable :rst:dir:`py:module`, .. autoclass:: module.name::Noodle + * :rst:dir:`autoclass` also recognizes the ``class-doc-from`` option that + can be used to override the global value of :confval:`autoclass_content`. + + .. versionadded:: 4.1 .. rst:directive:: autofunction autodecorator @@ -507,7 +511,7 @@ There are also config values that you can set: The supported options are ``'members'``, ``'member-order'``, ``'undoc-members'``, ``'private-members'``, ``'special-members'``, ``'inherited-members'``, ``'show-inheritance'``, ``'ignore-module-all'``, - ``'imported-members'`` and ``'exclude-members'``. + ``'imported-members'``, ``'exclude-members'`` and ``'class-doc-from'``. .. versionadded:: 1.8 @@ -517,6 +521,9 @@ There are also config values that you can set: .. versionchanged:: 2.1 Added ``'imported-members'``. + .. versionchanged:: 4.1 + Added ``'class-doc-from'``. + .. confval:: autodoc_docstring_signature Functions imported from C modules cannot be introspected, and therefore the diff --git a/setup.py b/setup.py index b669afc00e3..7ce37f9eaf8 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,8 @@ 'sphinxcontrib-htmlhelp', 'sphinxcontrib-serializinghtml', 'sphinxcontrib-qthelp', - 'Jinja2>=2.3', + 'Jinja2>=2.3,<3.0', + 'MarkupSafe<2.0', 'Pygments>=2.0', 'docutils>=0.14,<0.18', 'snowballstemmer>=1.1', diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 962ba2ad0bf..ff6475c9442 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -129,6 +129,14 @@ def member_order_option(arg: Any) -> Optional[str]: raise ValueError(__('invalid value for member-order option: %s') % arg) +def class_doc_from_option(arg: Any) -> Optional[str]: + """Used to convert the :class-doc-from: option to autoclass directives.""" + if arg in ('both', 'class', 'init'): + return arg + else: + raise ValueError(__('invalid value for class-doc-from option: %s') % arg) + + SUPPRESS = object() @@ -1320,12 +1328,12 @@ def format_signature(self, **kwargs: Any) -> str: if typ is object: pass # default implementation. skipped. else: - self.annotate_to_first_argument(func, typ) - - documenter = FunctionDocumenter(self.directive, '') - documenter.object = func - documenter.objpath = [None] - sigs.append(documenter.format_signature()) + dispatchfunc = self.annotate_to_first_argument(func, typ) + if dispatchfunc: + documenter = FunctionDocumenter(self.directive, '') + documenter.object = dispatchfunc + documenter.objpath = [None] + sigs.append(documenter.format_signature()) if overloaded: actual = inspect.signature(self.object, type_aliases=self.config.autodoc_type_aliases) @@ -1350,28 +1358,34 @@ def merge_default_value(self, actual: Signature, overload: Signature) -> Signatu return overload.replace(parameters=parameters) - def annotate_to_first_argument(self, func: Callable, typ: Type) -> None: + def annotate_to_first_argument(self, func: Callable, typ: Type) -> Optional[Callable]: """Annotate type hint to the first argument of function if needed.""" try: sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases) except TypeError as exc: logger.warning(__("Failed to get a function signature for %s: %s"), self.fullname, exc) - return + return None except ValueError: - return + return None if len(sig.parameters) == 0: - return + return None + + def dummy(): + pass params = list(sig.parameters.values()) if params[0].annotation is Parameter.empty: params[0] = params[0].replace(annotation=typ) try: - func.__signature__ = sig.replace(parameters=params) # type: ignore + dummy.__signature__ = sig.replace(parameters=params) # type: ignore + return dummy except (AttributeError, TypeError): # failed to update signature (ex. built-in or extension types) - return + return None + else: + return None class DecoratorDocumenter(FunctionDocumenter): @@ -1417,6 +1431,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: 'show-inheritance': bool_option, 'member-order': member_order_option, 'exclude-members': exclude_members_option, 'private-members': members_option, 'special-members': members_option, + 'class-doc-from': class_doc_from_option, } _signature_class: Any = None @@ -1651,7 +1666,7 @@ def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]: if lines is not None: return lines - content = self.config.autoclass_content + classdoc_from = self.options.get('class-doc-from', self.config.autoclass_content) docstrings = [] attrdocstring = self.get_attr(self.object, '__doc__', None) @@ -1660,7 +1675,7 @@ def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]: # for classes, what the "docstring" is can be controlled via a # config value; the default is only the class docstring - if content in ('both', 'init'): + if classdoc_from in ('both', 'init'): __init__ = self.get_attr(self.object, '__init__', None) initdocstring = getdoc(__init__, self.get_attr, self.config.autodoc_inherit_docstrings, @@ -1682,7 +1697,7 @@ def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]: initdocstring.strip() == object.__new__.__doc__)): # for !pypy initdocstring = None if initdocstring: - if content == 'init': + if classdoc_from == 'init': docstrings = [initdocstring] else: docstrings.append(initdocstring) @@ -2109,13 +2124,13 @@ def format_signature(self, **kwargs: Any) -> str: if typ is object: pass # default implementation. skipped. else: - self.annotate_to_first_argument(func, typ) - - documenter = MethodDocumenter(self.directive, '') - documenter.parent = self.parent - documenter.object = func - documenter.objpath = [None] - sigs.append(documenter.format_signature()) + dispatchmeth = self.annotate_to_first_argument(func, typ) + if dispatchmeth: + documenter = MethodDocumenter(self.directive, '') + documenter.parent = self.parent + documenter.object = dispatchmeth + documenter.objpath = [None] + sigs.append(documenter.format_signature()) if overloaded: if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name): actual = inspect.signature(self.object, bound_method=False, @@ -2149,27 +2164,34 @@ def merge_default_value(self, actual: Signature, overload: Signature) -> Signatu return overload.replace(parameters=parameters) - def annotate_to_first_argument(self, func: Callable, typ: Type) -> None: + def annotate_to_first_argument(self, func: Callable, typ: Type) -> Optional[Callable]: """Annotate type hint to the first argument of function if needed.""" try: sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases) except TypeError as exc: logger.warning(__("Failed to get a method signature for %s: %s"), self.fullname, exc) - return + return None except ValueError: - return + return None + if len(sig.parameters) == 1: - return + return None + + def dummy(): + pass params = list(sig.parameters.values()) if params[1].annotation is Parameter.empty: params[1] = params[1].replace(annotation=typ) try: - func.__signature__ = sig.replace(parameters=params) # type: ignore + dummy.__signature__ = sig.replace(parameters=params) # type: ignore + return dummy except (AttributeError, TypeError): # failed to update signature (ex. built-in or extension types) - return + return None + else: + return None class NonDataDescriptorMixin(DataDocumenterMixinBase): diff --git a/sphinx/ext/autodoc/directive.py b/sphinx/ext/autodoc/directive.py index c58d0c41181..a554adf688b 100644 --- a/sphinx/ext/autodoc/directive.py +++ b/sphinx/ext/autodoc/directive.py @@ -30,7 +30,7 @@ AUTODOC_DEFAULT_OPTIONS = ['members', 'undoc-members', 'inherited-members', 'show-inheritance', 'private-members', 'special-members', 'ignore-module-all', 'exclude-members', 'member-order', - 'imported-members'] + 'imported-members', 'class-doc-from'] AUTODOC_EXTENDABLE_OPTIONS = ['members', 'private-members', 'special-members', 'exclude-members'] diff --git a/tests/roots/test-ext-autodoc/target/singledispatch.py b/tests/roots/test-ext-autodoc/target/singledispatch.py index 3fa81dcae4c..fca2b668381 100644 --- a/tests/roots/test-ext-autodoc/target/singledispatch.py +++ b/tests/roots/test-ext-autodoc/target/singledispatch.py @@ -15,6 +15,7 @@ def func(arg, kwarg=None): @func.register(int) +@func.register(float) def _func_int(arg, kwarg=None): """A function for int.""" pass diff --git a/tests/roots/test-ext-autodoc/target/singledispatchmethod.py b/tests/roots/test-ext-autodoc/target/singledispatchmethod.py index b5ccbb2f09d..086c7fe668d 100644 --- a/tests/roots/test-ext-autodoc/target/singledispatchmethod.py +++ b/tests/roots/test-ext-autodoc/target/singledispatchmethod.py @@ -10,6 +10,7 @@ def meth(self, arg, kwarg=None): pass @meth.register(int) + @meth.register(float) def _meth_int(self, arg, kwarg=None): """A method for int.""" pass diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index fcaafa2af4b..4c16886b3fc 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -2108,6 +2108,7 @@ def test_singledispatch(app): '', '', '.. py:function:: func(arg, kwarg=None)', + ' func(arg: float, kwarg=None)', ' func(arg: int, kwarg=None)', ' func(arg: str, kwarg=None)', ' :module: target.singledispatch', @@ -2135,6 +2136,7 @@ def test_singledispatchmethod(app): '', '', ' .. py:method:: Foo.meth(arg, kwarg=None)', + ' Foo.meth(arg: float, kwarg=None)', ' Foo.meth(arg: int, kwarg=None)', ' Foo.meth(arg: str, kwarg=None)', ' :module: target.singledispatchmethod', @@ -2153,6 +2155,7 @@ def test_singledispatchmethod_automethod(app): assert list(actual) == [ '', '.. py:method:: Foo.meth(arg, kwarg=None)', + ' Foo.meth(arg: float, kwarg=None)', ' Foo.meth(arg: int, kwarg=None)', ' Foo.meth(arg: str, kwarg=None)', ' :module: target.singledispatchmethod', diff --git a/tests/test_ext_autodoc_autoclass.py b/tests/test_ext_autodoc_autoclass.py index d879f8e1491..096dc939738 100644 --- a/tests/test_ext_autodoc_autoclass.py +++ b/tests/test_ext_autodoc_autoclass.py @@ -264,6 +264,53 @@ def test_show_inheritance_for_subclass_of_generic_type(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_class_doc_from_class(app): + options = {"members": None, + "class-doc-from": "class"} + actual = do_autodoc(app, 'class', 'target.autoclass_content.C', options) + assert list(actual) == [ + '', + '.. py:class:: C()', + ' :module: target.autoclass_content', + '', + ' A class having __init__, no __new__', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_class_doc_from_init(app): + options = {"members": None, + "class-doc-from": "init"} + actual = do_autodoc(app, 'class', 'target.autoclass_content.C', options) + assert list(actual) == [ + '', + '.. py:class:: C()', + ' :module: target.autoclass_content', + '', + ' __init__ docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_class_doc_from_both(app): + options = {"members": None, + "class-doc-from": "both"} + actual = do_autodoc(app, 'class', 'target.autoclass_content.C', options) + assert list(actual) == [ + '', + '.. py:class:: C()', + ' :module: target.autoclass_content', + '', + ' A class having __init__, no __new__', + '', + ' __init__ docstring', + '', + ] + + def test_class_alias(app): def autodoc_process_docstring(*args): """A handler always raises an error. diff --git a/tests/test_ext_autodoc_autofunction.py b/tests/test_ext_autodoc_autofunction.py index 6150918894c..ca2429b5e88 100644 --- a/tests/test_ext_autodoc_autofunction.py +++ b/tests/test_ext_autodoc_autofunction.py @@ -119,6 +119,7 @@ def test_singledispatch(app): assert list(actual) == [ '', '.. py:function:: func(arg, kwarg=None)', + ' func(arg: float, kwarg=None)', ' func(arg: int, kwarg=None)', ' func(arg: str, kwarg=None)', ' :module: target.singledispatch',