diff --git a/CHANGES b/CHANGES index 44be3fbf9..3942c4240 100644 --- a/CHANGES +++ b/CHANGES @@ -74,6 +74,7 @@ Bugs fixed * #6588: autodoc: Decorated inherited method has no documentation * #7469: autodoc: The change of autodoc-process-docstring for variables is cached unexpectedly +* #7559: autodoc: misdetects a sync function is async * #7535: sphinx-autogen: crashes when custom template uses inheritance * #7536: sphinx-autogen: crashes when template uses i18n feature * #2785: html: Bad alignment of equation links diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 38777728a..9eb05b29b 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -125,13 +125,15 @@ def unwrap(obj: Any) -> Any: return obj -def unwrap_all(obj: Any) -> Any: +def unwrap_all(obj: Any, *, stop: Callable = None) -> Any: """ Get an original object from wrapped object (unwrapping partials, wrapped functions, and other decorators). """ while True: - if ispartial(obj): + if stop and stop(obj): + return obj + elif ispartial(obj): obj = obj.func elif inspect.isroutine(obj) and hasattr(obj, '__wrapped__'): obj = obj.__wrapped__ @@ -287,7 +289,8 @@ def isroutine(obj: Any) -> bool: def iscoroutinefunction(obj: Any) -> bool: """Check if the object is coroutine-function.""" - obj = unwrap_all(obj) + # unwrap staticmethod, classmethod and partial (except wrappers) + obj = unwrap_all(obj, stop=lambda o: hasattr(o, '__wrapped__')) 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 69602325d..692dd4883 100644 --- a/tests/roots/test-ext-autodoc/target/coroutine.py +++ b/tests/roots/test-ext-autodoc/target/coroutine.py @@ -1,3 +1,7 @@ +import asyncio +from functools import wraps + + class AsyncClass: async def do_coroutine(self): """A documented coroutine function""" @@ -16,3 +20,14 @@ class AsyncClass: async def _other_coro_func(): return "run" + + +def myawait(f): + @wraps(f) + def wrapper(*args, **kwargs): + awaitable = f(*args, **kwargs) + return asyncio.run(awaitable) + return wrapper + + +sync_func = myawait(_other_coro_func) diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index cbbdbb787..6d57d795d 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -1379,6 +1379,15 @@ def test_coroutine(): '', ] + # force-synchronized wrapper + actual = do_autodoc(app, 'function', 'target.coroutine.sync_func') + assert list(actual) == [ + '', + '.. py:function:: sync_func()', + ' :module: target.coroutine', + '', + ] + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_partialmethod(app):