From 2fec37219fc8d99f96026308116ef859c67d7588 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 22 Feb 2020 00:28:35 +0900 Subject: [PATCH] Fix #7189: autodoc: classmethod coroutines are not detected --- CHANGES | 1 + sphinx/util/inspect.py | 21 +++++++++++++++---- .../test-ext-autodoc/target/coroutine.py | 10 +++++++++ tests/test_autodoc.py | 18 +++++++++++++++- tests/test_util_inspect.py | 10 +++++++++ 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index a34909ae9..10762b617 100644 --- a/CHANGES +++ b/CHANGES @@ -18,6 +18,7 @@ Bugs fixed * #7184: autodoc: ``*args`` and ``**kwarg`` in type comments are not handled properly +* #7189: autodoc: classmethod coroutines are not detected Testing -------- diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 619512fd7..2bcccdd60 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -111,6 +111,19 @@ def getargspec(func): kwonlyargs, kwdefaults, annotations) +def unwrap(obj: Any) -> Any: + """Get an original object from wrapped object.""" + while True: + if ispartial(obj): + obj = unpartial(obj) + elif isclassmethod(obj): + obj = obj.__func__ + elif isstaticmethod(obj): + obj = obj.__func__ + else: + return obj + + def isenumclass(x: Any) -> bool: """Check if the object is subclass of enum.""" return inspect.isclass(x) and issubclass(x, enum.Enum) @@ -141,7 +154,7 @@ def isclassmethod(obj: Any) -> bool: """Check if the object is classmethod.""" if isinstance(obj, classmethod): return True - elif inspect.ismethod(obj) and obj.__self__ is not None: + elif inspect.ismethod(obj) and obj.__self__ is not None and isclass(obj.__self__): return True return False @@ -208,17 +221,17 @@ def isattributedescriptor(obj: Any) -> bool: def isfunction(obj: Any) -> bool: """Check if the object is function.""" - return inspect.isfunction(unpartial(obj)) + return inspect.isfunction(unwrap(obj)) def isbuiltin(obj: Any) -> bool: """Check if the object is builtin.""" - return inspect.isbuiltin(unpartial(obj)) + return inspect.isbuiltin(unwrap(obj)) def iscoroutinefunction(obj: Any) -> bool: """Check if the object is coroutine-function.""" - obj = unpartial(obj) + obj = unwrap(obj) if hasattr(obj, '__code__') and inspect.iscoroutinefunction(obj): # check obj.__code__ because iscoroutinefunction() crashes for custom method-like # objects (see https://github.com/sphinx-doc/sphinx/issues/6605) diff --git a/tests/roots/test-ext-autodoc/target/coroutine.py b/tests/roots/test-ext-autodoc/target/coroutine.py index b3223a820..69602325d 100644 --- a/tests/roots/test-ext-autodoc/target/coroutine.py +++ b/tests/roots/test-ext-autodoc/target/coroutine.py @@ -3,6 +3,16 @@ class AsyncClass: """A documented coroutine function""" attr_coro_result = await _other_coro_func() # NOQA + @classmethod + async def do_coroutine2(cls): + """A documented coroutine classmethod""" + pass + + @staticmethod + async def do_coroutine3(): + """A documented coroutine staticmethod""" + pass + async def _other_coro_func(): return "run" diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index 2e8ff0414..a86211f18 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -1319,7 +1319,23 @@ def test_coroutine(): ' :async:', ' ', ' A documented coroutine function', - ' ' + ' ', + ' ', + ' .. py:method:: AsyncClass.do_coroutine2()', + ' :module: target.coroutine', + ' :async:', + ' :classmethod:', + ' ', + ' A documented coroutine classmethod', + ' ', + ' ', + ' .. py:method:: AsyncClass.do_coroutine3()', + ' :module: target.coroutine', + ' :async:', + ' :staticmethod:', + ' ', + ' A documented coroutine staticmethod', + ' ', ] diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index c6b2c9149..a8019a9c3 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -419,6 +419,16 @@ def test_dict_customtype(): assert ": 2" in description +@pytest.mark.sphinx(testroot='ext-autodoc') +def test_isclassmethod(app): + from target.methods import Base, Inherited + + assert inspect.isclassmethod(Base.classmeth) is True + assert inspect.isclassmethod(Base.meth) is False + assert inspect.isclassmethod(Inherited.classmeth) is True + assert inspect.isclassmethod(Inherited.meth) is False + + @pytest.mark.sphinx(testroot='ext-autodoc') def test_isstaticmethod(app): from target.methods import Base, Inherited