From 4cf8e98175e501c671c3c43016d3df443def9ffc Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 8 Nov 2022 14:56:51 +0100 Subject: [PATCH 01/15] Added initial test and docs drafts. --- Doc/library/inspect.rst | 14 ++++++++++++++ Lib/test/test_inspect.py | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 44f1ae04c9e39e..48e3d060d00006 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -358,6 +358,20 @@ attributes: wrapped function is a :term:`coroutine function`. +.. function:: markcoroutinefunction(object) + + 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 an awaitable or objects + implementing an :keyword:`async def` ``__call__``. + + Prefer :keyword:`async def` functions or calling the function and testing + the return with :func:`isawaitable` where feasible. + + .. versionadded:: 3.12 + + .. function:: iscoroutine(object) Return ``True`` if the object is a :term:`coroutine` created by an diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 3f5c299ce681c5..a2ce6d6fcbecc4 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -202,6 +202,25 @@ def test_iscoroutine(self): gen_coroutine_function_example)))) self.assertTrue(inspect.isgenerator(gen_coro)) + # Use subtest initially to see both failures. + with self.subTest("Wrapper not recognised."): + # First case: sync function returning an awaitable. + async def _fn3(): + pass + + def fn3(): + return _fn3() + self.assertTrue(inspect.iscoroutinefunction(fn3)) + + with self.subTest("Awaitable instance not recongnised."): + # Second case: a class with an async def __call__. + # - instance is awaitable. + class Cl: + async def __call__(self): + pass + cl = Cl() + self.assertTrue(inspect.iscoroutinefunction(cl)) + self.assertFalse( inspect.iscoroutinefunction(unittest.mock.Mock())) self.assertTrue( From 6b8fa872c5378cc608b0a0ca01ed350c4193e07d Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 15 Nov 2022 09:06:56 +0100 Subject: [PATCH 02/15] Make first case pass setting code flags. --- Lib/test/test_inspect.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index a2ce6d6fcbecc4..a70d68d293a362 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -210,6 +210,12 @@ async def _fn3(): def fn3(): return _fn3() + + # TODO: Move this to decorator function. + fn3.__code__ = fn3.__code__.replace( + co_flags=fn3.__code__.co_flags | inspect.CO_COROUTINE + ) + self.assertTrue(inspect.iscoroutinefunction(fn3)) with self.subTest("Awaitable instance not recongnised."): From fa22a1631ebaf49f54d45eb36057ac3a49ca26f6 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 15 Nov 2022 09:07:26 +0100 Subject: [PATCH 03/15] Adjust iscoroutinefunction() to look for async __call__. --- Lib/inspect.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index d0015aa202044e..b0312f640785ca 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -396,7 +396,9 @@ def iscoroutinefunction(obj): Coroutine functions are defined with "async def" syntax. """ - return _has_code_flag(obj, CO_COROUTINE) + return _has_code_flag(obj, CO_COROUTINE) or ( + callable(obj) and _has_code_flag(obj.__call__, CO_COROUTINE) + ) def isasyncgenfunction(obj): """Return true if the object is an asynchronous generator function. From 513d3582081efe0d83bd5d4da1f4d9a8c99e3535 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Thu, 17 Nov 2022 10:02:19 +0000 Subject: [PATCH 04/15] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2022-11-17-10-02-18.gh-issue-94912.G2aa-E.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2022-11-17-10-02-18.gh-issue-94912.G2aa-E.rst 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 00000000000000..0eae0a278539c6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-11-17-10-02-18.gh-issue-94912.G2aa-E.rst @@ -0,0 +1 @@ +Added ``@markcoroutinefunction`` decorator to ``inspect`` module. From 397a975bc331733aafdc843611180759e135f012 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 17 Nov 2022 11:22:30 +0100 Subject: [PATCH 05/15] Moved to decorator; extra tests. --- Lib/inspect.py | 12 +++++++++++- Lib/test/test_inspect.py | 39 +++++++++++++++++++-------------------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index b0312f640785ca..79ba24bada639f 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -125,6 +125,7 @@ "ismodule", "isroutine", "istraceback", + "markcoroutinefunction", "signature", "stack", "trace", @@ -391,13 +392,22 @@ def isgeneratorfunction(obj): See help(isfunction) for a list of attributes.""" return _has_code_flag(obj, CO_GENERATOR) +def markcoroutinefunction(func): + """ + Decorator to ensure callable is recognised as a coroutine function. + """ + func.__code__ = func.__code__.replace( + co_flags=func.__code__.co_flags | CO_COROUTINE + ) + return func + def iscoroutinefunction(obj): """Return true if the object is a coroutine function. Coroutine functions are defined with "async def" syntax. """ return _has_code_flag(obj, CO_COROUTINE) or ( - callable(obj) and _has_code_flag(obj.__call__, CO_COROUTINE) + not isclass(obj) and callable(obj) and _has_code_flag(obj.__call__, CO_COROUTINE) ) def isasyncgenfunction(obj): diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index a70d68d293a362..9f243238a41d69 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -202,30 +202,29 @@ def test_iscoroutine(self): gen_coroutine_function_example)))) self.assertTrue(inspect.isgenerator(gen_coro)) - # Use subtest initially to see both failures. - with self.subTest("Wrapper not recognised."): - # First case: sync function returning an awaitable. - async def _fn3(): - pass + async def _fn3(): + pass - def fn3(): - return _fn3() + @inspect.markcoroutinefunction + def fn3(): + return _fn3() - # TODO: Move this to decorator function. - fn3.__code__ = fn3.__code__.replace( - co_flags=fn3.__code__.co_flags | inspect.CO_COROUTINE - ) + self.assertTrue(inspect.iscoroutinefunction(fn3)) - self.assertTrue(inspect.iscoroutinefunction(fn3)) + class Cl: + async def __call__(self): + pass + + self.assertFalse(inspect.iscoroutinefunction(Cl)) + self.assertTrue(inspect.iscoroutinefunction(Cl())) + + class Cl2: + @inspect.markcoroutinefunction + def __call__(self): + pass - with self.subTest("Awaitable instance not recongnised."): - # Second case: a class with an async def __call__. - # - instance is awaitable. - class Cl: - async def __call__(self): - pass - cl = Cl() - self.assertTrue(inspect.iscoroutinefunction(cl)) + self.assertFalse(inspect.iscoroutinefunction(Cl2)) + self.assertTrue(inspect.iscoroutinefunction(Cl2())) self.assertFalse( inspect.iscoroutinefunction(unittest.mock.Mock())) From d156eab1859aaade6b3e096a25caefdc93731ede Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 23 Nov 2022 15:39:18 +0100 Subject: [PATCH 06/15] Added tests for lambda, @classmethod, and @staticmethod cases. --- Lib/inspect.py | 2 ++ Lib/test/test_inspect.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/Lib/inspect.py b/Lib/inspect.py index 79ba24bada639f..76ecdb2d56a7e3 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -396,6 +396,8 @@ def markcoroutinefunction(func): """ Decorator to ensure callable is recognised as a coroutine function. """ + if hasattr(func, '__func__'): + func = func.__func__ func.__code__ = func.__code__.replace( co_flags=func.__code__.co_flags | CO_COROUTINE ) diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 9f243238a41d69..490f34705c6570 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -210,6 +210,11 @@ def fn3(): return _fn3() self.assertTrue(inspect.iscoroutinefunction(fn3)) + self.assertTrue( + inspect.iscoroutinefunction( + inspect.markcoroutinefunction(lambda: _fn3()) + ) + ) class Cl: async def __call__(self): @@ -226,6 +231,20 @@ def __call__(self): self.assertFalse(inspect.iscoroutinefunction(Cl2)) self.assertTrue(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( From 34859de64ada345fca4a5741825180297bc35438 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 23 Nov 2022 15:41:34 +0100 Subject: [PATCH 07/15] Adjusted docs signature for review comment. --- Doc/library/inspect.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 48e3d060d00006..f963467a5dca9f 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -358,7 +358,7 @@ attributes: wrapped function is a :term:`coroutine function`. -.. function:: markcoroutinefunction(object) +.. function:: markcoroutinefunction(func) Decorator to mark a callable as a :term:`coroutine function` if it would not otherwise be detected by :func:`iscoroutinefunction`. From 47a9fea9653ca539c02e62649bebe8300ad260e9 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Wed, 23 Nov 2022 15:53:45 -0800 Subject: [PATCH 08/15] Improve grammar in NEWS file --- .../next/Library/2022-11-17-10-02-18.gh-issue-94912.G2aa-E.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index 0eae0a278539c6..ee00f9d8d03f2c 100644 --- 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 @@ -1 +1,2 @@ -Added ``@markcoroutinefunction`` decorator to ``inspect`` module. +Add :func:`inspect.markcoroutinefunction` decorator which manually marks +a function as a coroutine for the benefit of :func:`iscoroutinefunction`. From cd6c491359b2424d3ff6b2bac6b8efc12c62ffc0 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 29 Nov 2022 10:05:34 +0100 Subject: [PATCH 09/15] Adjusted docs. --- Doc/library/inspect.rst | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index f963467a5dca9f..0aa31df1c7dcef 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -348,8 +348,10 @@ 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 an instance of a class defining + an :keyword:`async def` ``__call__``. .. versionadded:: 3.5 @@ -357,17 +359,19 @@ attributes: Functions wrapped in :func:`functools.partial` now return ``True`` if the wrapped function is a :term:`coroutine function`. + .. versionchanged:: 3.12 + Instances of classes defining an :keyword:`async def` ``__call__`` 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 an awaitable or objects - implementing an :keyword:`async def` ``__call__``. - - Prefer :keyword:`async def` functions or calling the function and testing - the return with :func:`isawaitable` where feasible. + This may be of use for sync functions that return a :term:`coroutine`, but + prefer :keyword:`async def` functions, or if necessary calling the function + and testing the return with :func:`iscoroutine` where feasible. .. versionadded:: 3.12 From 629dd8192dc2f5559286028e98f29d58f8cbbf1f Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 30 Nov 2022 10:20:40 +0100 Subject: [PATCH 10/15] Updated for review comments. --- Doc/library/inspect.rst | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 0aa31df1c7dcef..3a527ee76db9bf 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -350,8 +350,9 @@ attributes: 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 an instance of a class defining - an :keyword:`async def` ``__call__``. + wrapping a :term:`coroutine function`, an instance of a class defining an + :keyword:`async def` ``__call__``, or a sync function marked with + :func:`markcoroutinefunction`. .. versionadded:: 3.5 @@ -360,8 +361,9 @@ attributes: wrapped function is a :term:`coroutine function`. .. versionchanged:: 3.12 - Instances of classes defining an :keyword:`async def` ``__call__`` now - return ``True``. + Instances of classes defining an :keyword:`async def` ``__call__``, or + sync functions marked with :func:`markcoroutinefunction` now return + ``True``. .. function:: markcoroutinefunction(func) @@ -369,9 +371,12 @@ attributes: 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`, but - prefer :keyword:`async def` functions, or if necessary calling the function - and testing the return with :func:`iscoroutine` where feasible. + 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 From c6d2b88a1884039c8ea98124401acbf10accf660 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 6 Dec 2022 12:27:37 +0100 Subject: [PATCH 11/15] Use marker for implementation. --- Lib/inspect.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index cd369de02c1bd2..883d0b02bf4423 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -392,22 +392,31 @@ 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 = object() + def markcoroutinefunction(func): """ Decorator to ensure callable is recognised as a coroutine function. """ if hasattr(func, '__func__'): func = func.__func__ - func.__code__ = func.__code__.replace( - co_flags=func.__code__.co_flags | CO_COROUTINE - ) + func._is_coroutine = _is_coroutine 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. """ + func = getattr(obj, "__func__", obj) + if getattr(func, "_is_coroutine", None) is _is_coroutine: + return True + + if not isclass(obj) and callable(obj) and getattr(obj.__call__, "_is_coroutine", None) is _is_coroutine: + return True + return _has_code_flag(obj, CO_COROUTINE) or ( not isclass(obj) and callable(obj) and _has_code_flag(obj.__call__, CO_COROUTINE) ) From b7249a89b25ab6f5643208f6b8dcd00dd3507593 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 6 Dec 2022 12:36:47 +0100 Subject: [PATCH 12/15] Added "What's New..." entry. --- Doc/whatsnew/3.12.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index c0f98b59ccaf0f..bdb1eb4e2c5e4d 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -221,6 +221,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 ------- From 3bc72e2422ba4f44725525c5eb87989c677c6b34 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 8 Dec 2022 09:49:40 +0100 Subject: [PATCH 13/15] Adjusted implementation. --- Lib/inspect.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 883d0b02bf4423..3b9bd14a383205 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -410,12 +410,13 @@ def iscoroutinefunction(obj): Coroutine functions are normally defined with "async def" syntax, but may be marked via markcoroutinefunction. """ - func = getattr(obj, "__func__", obj) - if getattr(func, "_is_coroutine", None) is _is_coroutine: - return True - - if not isclass(obj) and callable(obj) and getattr(obj.__call__, "_is_coroutine", None) is _is_coroutine: - return True + if not isclass(obj) and callable(obj): + # Test both the function and the __call__ implementation for the + # _is_coroutine marker. + f = getattr(getattr(obj, "__func__", obj), "_is_coroutine", None) + c = getattr(obj.__call__, "_is_coroutine", None) + if f is _is_coroutine or c is _is_coroutine: + return True return _has_code_flag(obj, CO_COROUTINE) or ( not isclass(obj) and callable(obj) and _has_code_flag(obj.__call__, CO_COROUTINE) From c073cf79867a702dc4de6a0659dd9d16bfe4b44e Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 13 Dec 2022 09:16:44 +0100 Subject: [PATCH 14/15] Renamed to _is_coroutine_marker. --- Lib/inspect.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 3b9bd14a383205..fec2e319f5a6fa 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -393,7 +393,7 @@ def isgeneratorfunction(obj): return _has_code_flag(obj, CO_GENERATOR) # A marker for markcoroutinefunction and iscoroutinefunction. -_is_coroutine = object() +_is_coroutine_marker = object() def markcoroutinefunction(func): """ @@ -401,7 +401,7 @@ def markcoroutinefunction(func): """ if hasattr(func, '__func__'): func = func.__func__ - func._is_coroutine = _is_coroutine + func._is_coroutine_marker = _is_coroutine_marker return func def iscoroutinefunction(obj): @@ -412,10 +412,10 @@ def iscoroutinefunction(obj): """ if not isclass(obj) and callable(obj): # Test both the function and the __call__ implementation for the - # _is_coroutine marker. - f = getattr(getattr(obj, "__func__", obj), "_is_coroutine", None) - c = getattr(obj.__call__, "_is_coroutine", None) - if f is _is_coroutine or c is _is_coroutine: + # _is_coroutine_marker. + f = getattr(getattr(obj, "__func__", obj), "_is_coroutine_marker", None) + c = getattr(obj.__call__, "_is_coroutine_marker", None) + if f is _is_coroutine_marker or c is _is_coroutine_marker: return True return _has_code_flag(obj, CO_COROUTINE) or ( From 5ffba320051f474cad2796621bc648e0e66890dc Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 13 Dec 2022 09:35:28 +0100 Subject: [PATCH 15/15] Adjusted implementation and docs. --- Doc/library/inspect.rst | 6 ++---- Lib/inspect.py | 20 +++++++++----------- Lib/test/test_inspect.py | 6 ++++-- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 13917c5f7b8518..d07da184280643 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -345,8 +345,7 @@ attributes (see :ref:`import-mod-attrs` for module attributes): 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`, an instance of a class defining an - :keyword:`async def` ``__call__``, or a sync function marked with + wrapping a :term:`coroutine function`, or a sync function marked with :func:`markcoroutinefunction`. .. versionadded:: 3.5 @@ -356,8 +355,7 @@ attributes (see :ref:`import-mod-attrs` for module attributes): wrapped function is a :term:`coroutine function`. .. versionchanged:: 3.12 - Instances of classes defining an :keyword:`async def` ``__call__``, or - sync functions marked with :func:`markcoroutinefunction` now return + Sync functions marked with :func:`markcoroutinefunction` now return ``True``. diff --git a/Lib/inspect.py b/Lib/inspect.py index fec2e319f5a6fa..9b531d174102cb 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -395,6 +395,14 @@ def isgeneratorfunction(obj): # 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. @@ -410,17 +418,7 @@ def iscoroutinefunction(obj): Coroutine functions are normally defined with "async def" syntax, but may be marked via markcoroutinefunction. """ - if not isclass(obj) and callable(obj): - # Test both the function and the __call__ implementation for the - # _is_coroutine_marker. - f = getattr(getattr(obj, "__func__", obj), "_is_coroutine_marker", None) - c = getattr(obj.__call__, "_is_coroutine_marker", None) - if f is _is_coroutine_marker or c is _is_coroutine_marker: - return True - - return _has_code_flag(obj, CO_COROUTINE) or ( - not isclass(obj) and callable(obj) and _has_code_flag(obj.__call__, 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 490f34705c6570..f6dfc555e7983a 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -221,7 +221,8 @@ async def __call__(self): pass self.assertFalse(inspect.iscoroutinefunction(Cl)) - self.assertTrue(inspect.iscoroutinefunction(Cl())) + # instances with async def __call__ are NOT recognised. + self.assertFalse(inspect.iscoroutinefunction(Cl())) class Cl2: @inspect.markcoroutinefunction @@ -229,7 +230,8 @@ def __call__(self): pass self.assertFalse(inspect.iscoroutinefunction(Cl2)) - self.assertTrue(inspect.iscoroutinefunction(Cl2())) + # instances with marked __call__ are NOT recognised. + self.assertFalse(inspect.iscoroutinefunction(Cl2())) class Cl3: @inspect.markcoroutinefunction