summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/inspect.rst25
-rw-r--r--Doc/whatsnew/3.12.rst6
-rw-r--r--Lib/inspect.py26
-rw-r--r--Lib/test/test_inspect.py45
-rw-r--r--Misc/NEWS.d/next/Library/2022-11-17-10-02-18.gh-issue-94912.G2aa-E.rst2
5 files changed, 100 insertions, 4 deletions
diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst
index 6705577..58b84a3 100644
--- a/Doc/library/inspect.rst
+++ b/Doc/library/inspect.rst
@@ -343,8 +343,10 @@ attributes (see :ref:`import-mod-attrs` for module attributes):
.. function:: iscoroutinefunction(object)
- Return ``True`` if the object is a :term:`coroutine function`
- (a function defined with an :keyword:`async def` syntax).
+ Return ``True`` if the object is a :term:`coroutine function` (a function
+ defined with an :keyword:`async def` syntax), a :func:`functools.partial`
+ wrapping a :term:`coroutine function`, or a sync function marked with
+ :func:`markcoroutinefunction`.
.. versionadded:: 3.5
@@ -352,6 +354,25 @@ attributes (see :ref:`import-mod-attrs` for module attributes):
Functions wrapped in :func:`functools.partial` now return ``True`` if the
wrapped function is a :term:`coroutine function`.
+ .. versionchanged:: 3.12
+ Sync functions marked with :func:`markcoroutinefunction` now return
+ ``True``.
+
+
+.. function:: markcoroutinefunction(func)
+
+ Decorator to mark a callable as a :term:`coroutine function` if it would not
+ otherwise be detected by :func:`iscoroutinefunction`.
+
+ This may be of use for sync functions that return a :term:`coroutine`, if
+ the function is passed to an API that requires :func:`iscoroutinefunction`.
+
+ When possible, using an :keyword:`async def` function is preferred. Also
+ acceptable is calling the function and testing the return with
+ :func:`iscoroutine`.
+
+ .. versionadded:: 3.12
+
.. function:: iscoroutine(object)
diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst
index 73dc462..0cc4471 100644
--- a/Doc/whatsnew/3.12.rst
+++ b/Doc/whatsnew/3.12.rst
@@ -225,6 +225,12 @@ asyncio
a custom event loop factory.
(Contributed by Kumar Aditya in :gh:`99388`.)
+inspect
+-------
+
+* Add :func:`inspect.markcoroutinefunction` to mark sync functions that return
+ a :term:`coroutine` for use with :func:`iscoroutinefunction`.
+ (Contributed Carlton Gibson in :gh:`99247`.)
pathlib
-------
diff --git a/Lib/inspect.py b/Lib/inspect.py
index e92c355..052f0bf 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -125,6 +125,7 @@ __all__ = [
"ismodule",
"isroutine",
"istraceback",
+ "markcoroutinefunction",
"signature",
"stack",
"trace",
@@ -391,12 +392,33 @@ def isgeneratorfunction(obj):
See help(isfunction) for a list of attributes."""
return _has_code_flag(obj, CO_GENERATOR)
+# A marker for markcoroutinefunction and iscoroutinefunction.
+_is_coroutine_marker = object()
+
+def _has_coroutine_mark(f):
+ while ismethod(f):
+ f = f.__func__
+ f = functools._unwrap_partial(f)
+ if not (isfunction(f) or _signature_is_functionlike(f)):
+ return False
+ return getattr(f, "_is_coroutine_marker", None) is _is_coroutine_marker
+
+def markcoroutinefunction(func):
+ """
+ Decorator to ensure callable is recognised as a coroutine function.
+ """
+ if hasattr(func, '__func__'):
+ func = func.__func__
+ func._is_coroutine_marker = _is_coroutine_marker
+ return func
+
def iscoroutinefunction(obj):
"""Return true if the object is a coroutine function.
- Coroutine functions are defined with "async def" syntax.
+ Coroutine functions are normally defined with "async def" syntax, but may
+ be marked via markcoroutinefunction.
"""
- return _has_code_flag(obj, CO_COROUTINE)
+ return _has_code_flag(obj, CO_COROUTINE) or _has_coroutine_mark(obj)
def isasyncgenfunction(obj):
"""Return true if the object is an asynchronous generator function.
diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py
index 0f8217b..78e6e9e 100644
--- a/Lib/test/test_inspect.py
+++ b/Lib/test/test_inspect.py
@@ -202,6 +202,51 @@ class TestPredicates(IsTestBase):
gen_coroutine_function_example))))
self.assertTrue(inspect.isgenerator(gen_coro))
+ async def _fn3():
+ pass
+
+ @inspect.markcoroutinefunction
+ def fn3():
+ return _fn3()
+
+ self.assertTrue(inspect.iscoroutinefunction(fn3))
+ self.assertTrue(
+ inspect.iscoroutinefunction(
+ inspect.markcoroutinefunction(lambda: _fn3())
+ )
+ )
+
+ class Cl:
+ async def __call__(self):
+ pass
+
+ self.assertFalse(inspect.iscoroutinefunction(Cl))
+ # instances with async def __call__ are NOT recognised.
+ self.assertFalse(inspect.iscoroutinefunction(Cl()))
+
+ class Cl2:
+ @inspect.markcoroutinefunction
+ def __call__(self):
+ pass
+
+ self.assertFalse(inspect.iscoroutinefunction(Cl2))
+ # instances with marked __call__ are NOT recognised.
+ self.assertFalse(inspect.iscoroutinefunction(Cl2()))
+
+ class Cl3:
+ @inspect.markcoroutinefunction
+ @classmethod
+ def do_something_classy(cls):
+ pass
+
+ @inspect.markcoroutinefunction
+ @staticmethod
+ def do_something_static():
+ pass
+
+ self.assertTrue(inspect.iscoroutinefunction(Cl3.do_something_classy))
+ self.assertTrue(inspect.iscoroutinefunction(Cl3.do_something_static))
+
self.assertFalse(
inspect.iscoroutinefunction(unittest.mock.Mock()))
self.assertTrue(
diff --git a/Misc/NEWS.d/next/Library/2022-11-17-10-02-18.gh-issue-94912.G2aa-E.rst b/Misc/NEWS.d/next/Library/2022-11-17-10-02-18.gh-issue-94912.G2aa-E.rst
new file mode 100644
index 0000000..ee00f9d
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-11-17-10-02-18.gh-issue-94912.G2aa-E.rst
@@ -0,0 +1,2 @@
+Add :func:`inspect.markcoroutinefunction` decorator which manually marks
+a function as a coroutine for the benefit of :func:`iscoroutinefunction`.