From c86e90842fd9dc0c08d34a8d556c936f392633b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Thu, 14 Mar 2024 12:26:35 +0100 Subject: [PATCH] [autodoc] add support for singledispatchmethod class methods (#11284) --- CHANGES.rst | 3 ++ sphinx/ext/autodoc/__init__.py | 5 +- .../singledispatchmethod_classmethod.py | 31 ++++++++++++ tests/test_extensions/test_ext_autodoc.py | 49 +++++++++++++++++++ 4 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 tests/roots/test-ext-autodoc/target/singledispatchmethod_classmethod.py diff --git a/CHANGES.rst b/CHANGES.rst index 7a0ebf759b8..cc0b0d26c0d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -93,6 +93,9 @@ Bugs fixed * #11474: Fix doctrees caching causing files not be rebuilt in some cases, e.g., when :confval:`numfig` is ``True``. Patch by Bénédikt Tran. +* #11278: autodoc: Fix rendering of :class:`functools.singledispatchmethod` + combined with :func:`@classmethod `. + Patch by Bénédikt Tran. Testing ------- diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 27058af5c09..c57ecd8d5fa 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -2229,7 +2229,8 @@ def add_directive_header(self, sig: str) -> None: self.add_line(' :abstractmethod:', sourcename) if inspect.iscoroutinefunction(obj) or inspect.isasyncgenfunction(obj): self.add_line(' :async:', sourcename) - if inspect.isclassmethod(obj): + if (inspect.isclassmethod(obj) or + inspect.is_singledispatch_method(obj) and inspect.isclassmethod(obj.func)): self.add_line(' :classmethod:', sourcename) if inspect.isstaticmethod(obj, cls=self.parent, name=self.object_name): self.add_line(' :staticmethod:', sourcename) @@ -2261,6 +2262,8 @@ def format_signature(self, **kwargs: Any) -> str: if typ is object: pass # default implementation. skipped. else: + if inspect.isclassmethod(func): + func = func.__func__ dispatchmeth = self.annotate_to_first_argument(func, typ) if dispatchmeth: documenter = MethodDocumenter(self.directive, '') diff --git a/tests/roots/test-ext-autodoc/target/singledispatchmethod_classmethod.py b/tests/roots/test-ext-autodoc/target/singledispatchmethod_classmethod.py new file mode 100644 index 00000000000..039fada8977 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/singledispatchmethod_classmethod.py @@ -0,0 +1,31 @@ +from functools import singledispatchmethod + + +class Foo: + """docstring""" + + @singledispatchmethod + @classmethod + def class_meth(cls, arg, kwarg=None): + """A class method for general use.""" + pass + + @class_meth.register(int) + @class_meth.register(float) + @classmethod + def _class_meth_int(cls, arg, kwarg=None): + """A class method for numbers.""" + pass + + @class_meth.register(str) + @classmethod + def _class_meth_str(cls, arg, kwarg=None): + """A class method for str.""" + pass + + @class_meth.register + @classmethod + def _class_meth_dict(cls, arg: dict, kwarg=None): + """A class method for dict.""" + # This function tests for specifying type through annotations + pass diff --git a/tests/test_extensions/test_ext_autodoc.py b/tests/test_extensions/test_ext_autodoc.py index 627f2c9aa83..203e5b439ca 100644 --- a/tests/test_extensions/test_ext_autodoc.py +++ b/tests/test_extensions/test_ext_autodoc.py @@ -2111,6 +2111,55 @@ def test_singledispatchmethod_automethod(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_singledispatchmethod_classmethod(app): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.singledispatchmethod_classmethod', options) + + assert list(actual) == [ + '', + '.. py:module:: target.singledispatchmethod_classmethod', + '', + '', + '.. py:class:: Foo()', + ' :module: target.singledispatchmethod_classmethod', + '', + ' docstring', + '', + '', + ' .. py:method:: Foo.class_meth(arg, kwarg=None)', + ' Foo.class_meth(arg: float, kwarg=None)', + ' Foo.class_meth(arg: int, kwarg=None)', + ' Foo.class_meth(arg: str, kwarg=None)', + ' Foo.class_meth(arg: dict, kwarg=None)', + ' :module: target.singledispatchmethod_classmethod', + ' :classmethod:', + '', + ' A class method for general use.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_singledispatchmethod_classmethod_automethod(app): + options = {} + actual = do_autodoc(app, 'method', 'target.singledispatchmethod_classmethod.Foo.class_meth', options) + + assert list(actual) == [ + '', + '.. py:method:: Foo.class_meth(arg, kwarg=None)', + ' Foo.class_meth(arg: float, kwarg=None)', + ' Foo.class_meth(arg: int, kwarg=None)', + ' Foo.class_meth(arg: str, kwarg=None)', + ' Foo.class_meth(arg: dict, kwarg=None)', + ' :module: target.singledispatchmethod_classmethod', + ' :classmethod:', + '', + ' A class method for general use.', + '', + ] + + @pytest.mark.skipif(sys.version_info[:2] >= (3, 13), reason='Cython does not support Python 3.13 yet.') @pytest.mark.skipif(pyximport is None, reason='cython is not installed')