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()`.
This commit is contained in:
Carlton Gibson 2022-12-18 20:13:24 +01:00 committed by GitHub
parent 1cf3d78c92
commit 532aa4e4e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 100 additions and 4 deletions

View file

@ -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)

View file

@ -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
-------

View file

@ -125,6 +125,7 @@
"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.

View file

@ -202,6 +202,51 @@ def test_iscoroutine(self):
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(

View file

@ -0,0 +1,2 @@
Add :func:`inspect.markcoroutinefunction` decorator which manually marks
a function as a coroutine for the benefit of :func:`iscoroutinefunction`.