Skip to content

Commit 532aa4e

Browse files
gh-94912: Added marker for non-standard coroutine function detection (#99247)
This introduces a new decorator `@inspect.markcoroutinefunction`, which, applied to a sync function, makes it appear async to `inspect.iscoroutinefunction()`.
1 parent 1cf3d78 commit 532aa4e

File tree

5 files changed

+100
-4
lines changed

5 files changed

+100
-4
lines changed

Doc/library/inspect.rst

+23-2
Original file line numberDiff line numberDiff line change
@@ -343,15 +343,36 @@ attributes (see :ref:`import-mod-attrs` for module attributes):
343343

344344
.. function:: iscoroutinefunction(object)
345345

346-
Return ``True`` if the object is a :term:`coroutine function`
347-
(a function defined with an :keyword:`async def` syntax).
346+
Return ``True`` if the object is a :term:`coroutine function` (a function
347+
defined with an :keyword:`async def` syntax), a :func:`functools.partial`
348+
wrapping a :term:`coroutine function`, or a sync function marked with
349+
:func:`markcoroutinefunction`.
348350

349351
.. versionadded:: 3.5
350352

351353
.. versionchanged:: 3.8
352354
Functions wrapped in :func:`functools.partial` now return ``True`` if the
353355
wrapped function is a :term:`coroutine function`.
354356

357+
.. versionchanged:: 3.12
358+
Sync functions marked with :func:`markcoroutinefunction` now return
359+
``True``.
360+
361+
362+
.. function:: markcoroutinefunction(func)
363+
364+
Decorator to mark a callable as a :term:`coroutine function` if it would not
365+
otherwise be detected by :func:`iscoroutinefunction`.
366+
367+
This may be of use for sync functions that return a :term:`coroutine`, if
368+
the function is passed to an API that requires :func:`iscoroutinefunction`.
369+
370+
When possible, using an :keyword:`async def` function is preferred. Also
371+
acceptable is calling the function and testing the return with
372+
:func:`iscoroutine`.
373+
374+
.. versionadded:: 3.12
375+
355376

356377
.. function:: iscoroutine(object)
357378

Doc/whatsnew/3.12.rst

+6
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,12 @@ asyncio
225225
a custom event loop factory.
226226
(Contributed by Kumar Aditya in :gh:`99388`.)
227227

228+
inspect
229+
-------
230+
231+
* Add :func:`inspect.markcoroutinefunction` to mark sync functions that return
232+
a :term:`coroutine` for use with :func:`iscoroutinefunction`.
233+
(Contributed Carlton Gibson in :gh:`99247`.)
228234

229235
pathlib
230236
-------

Lib/inspect.py

+24-2
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@
125125
"ismodule",
126126
"isroutine",
127127
"istraceback",
128+
"markcoroutinefunction",
128129
"signature",
129130
"stack",
130131
"trace",
@@ -391,12 +392,33 @@ def isgeneratorfunction(obj):
391392
See help(isfunction) for a list of attributes."""
392393
return _has_code_flag(obj, CO_GENERATOR)
393394

395+
# A marker for markcoroutinefunction and iscoroutinefunction.
396+
_is_coroutine_marker = object()
397+
398+
def _has_coroutine_mark(f):
399+
while ismethod(f):
400+
f = f.__func__
401+
f = functools._unwrap_partial(f)
402+
if not (isfunction(f) or _signature_is_functionlike(f)):
403+
return False
404+
return getattr(f, "_is_coroutine_marker", None) is _is_coroutine_marker
405+
406+
def markcoroutinefunction(func):
407+
"""
408+
Decorator to ensure callable is recognised as a coroutine function.
409+
"""
410+
if hasattr(func, '__func__'):
411+
func = func.__func__
412+
func._is_coroutine_marker = _is_coroutine_marker
413+
return func
414+
394415
def iscoroutinefunction(obj):
395416
"""Return true if the object is a coroutine function.
396417
397-
Coroutine functions are defined with "async def" syntax.
418+
Coroutine functions are normally defined with "async def" syntax, but may
419+
be marked via markcoroutinefunction.
398420
"""
399-
return _has_code_flag(obj, CO_COROUTINE)
421+
return _has_code_flag(obj, CO_COROUTINE) or _has_coroutine_mark(obj)
400422

401423
def isasyncgenfunction(obj):
402424
"""Return true if the object is an asynchronous generator function.

Lib/test/test_inspect.py

+45
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,51 @@ def test_iscoroutine(self):
202202
gen_coroutine_function_example))))
203203
self.assertTrue(inspect.isgenerator(gen_coro))
204204

205+
async def _fn3():
206+
pass
207+
208+
@inspect.markcoroutinefunction
209+
def fn3():
210+
return _fn3()
211+
212+
self.assertTrue(inspect.iscoroutinefunction(fn3))
213+
self.assertTrue(
214+
inspect.iscoroutinefunction(
215+
inspect.markcoroutinefunction(lambda: _fn3())
216+
)
217+
)
218+
219+
class Cl:
220+
async def __call__(self):
221+
pass
222+
223+
self.assertFalse(inspect.iscoroutinefunction(Cl))
224+
# instances with async def __call__ are NOT recognised.
225+
self.assertFalse(inspect.iscoroutinefunction(Cl()))
226+
227+
class Cl2:
228+
@inspect.markcoroutinefunction
229+
def __call__(self):
230+
pass
231+
232+
self.assertFalse(inspect.iscoroutinefunction(Cl2))
233+
# instances with marked __call__ are NOT recognised.
234+
self.assertFalse(inspect.iscoroutinefunction(Cl2()))
235+
236+
class Cl3:
237+
@inspect.markcoroutinefunction
238+
@classmethod
239+
def do_something_classy(cls):
240+
pass
241+
242+
@inspect.markcoroutinefunction
243+
@staticmethod
244+
def do_something_static():
245+
pass
246+
247+
self.assertTrue(inspect.iscoroutinefunction(Cl3.do_something_classy))
248+
self.assertTrue(inspect.iscoroutinefunction(Cl3.do_something_static))
249+
205250
self.assertFalse(
206251
inspect.iscoroutinefunction(unittest.mock.Mock()))
207252
self.assertTrue(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :func:`inspect.markcoroutinefunction` decorator which manually marks
2+
a function as a coroutine for the benefit of :func:`iscoroutinefunction`.

0 commit comments

Comments
 (0)