From ff314dd85072c06c8fda3e8314cab3fc6903b41a Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 29 Jan 2022 14:21:50 +0100 Subject: [PATCH 1/8] Improve detection of coroutine functions --- starlette/_utils.py | 12 ++++++ starlette/authentication.py | 4 +- starlette/background.py | 4 +- starlette/endpoints.py | 4 +- starlette/exceptions.py | 4 +- starlette/middleware/errors.py | 4 +- starlette/routing.py | 6 +-- starlette/testclient.py | 7 +--- tests/test__utils.py | 70 ++++++++++++++++++++++++++++++++++ tests/test_background.py | 20 ++++++++++ 10 files changed, 117 insertions(+), 18 deletions(-) create mode 100644 starlette/_utils.py create mode 100644 tests/test__utils.py diff --git a/starlette/_utils.py b/starlette/_utils.py new file mode 100644 index 000000000..7049c69c4 --- /dev/null +++ b/starlette/_utils.py @@ -0,0 +1,12 @@ +import asyncio +import functools +import typing + + +def iscoroutinefunction(obj: typing.Any) -> bool: + while isinstance(obj, functools.partial): + obj = obj.func + + return asyncio.iscoroutinefunction(obj) or ( + callable(obj) and asyncio.iscoroutinefunction(obj.__call__) + ) diff --git a/starlette/authentication.py b/starlette/authentication.py index b4882070d..ca8622196 100644 --- a/starlette/authentication.py +++ b/starlette/authentication.py @@ -1,8 +1,8 @@ -import asyncio import functools import inspect import typing +from starlette._utils import iscoroutinefunction from starlette.exceptions import HTTPException from starlette.requests import HTTPConnection, Request from starlette.responses import RedirectResponse, Response @@ -52,7 +52,7 @@ async def websocket_wrapper( return websocket_wrapper - elif asyncio.iscoroutinefunction(func): + elif iscoroutinefunction(func): # Handle async request/response functions. @functools.wraps(func) async def async_wrapper( diff --git a/starlette/background.py b/starlette/background.py index 14a4e9e1a..720109c9d 100644 --- a/starlette/background.py +++ b/starlette/background.py @@ -1,4 +1,3 @@ -import asyncio import sys import typing @@ -7,6 +6,7 @@ else: # pragma: no cover from typing_extensions import ParamSpec +from starlette._utils import iscoroutinefunction from starlette.concurrency import run_in_threadpool P = ParamSpec("P") @@ -19,7 +19,7 @@ def __init__( self.func = func self.args = args self.kwargs = kwargs - self.is_async = asyncio.iscoroutinefunction(func) + self.is_async = iscoroutinefunction(func) async def __call__(self) -> None: if self.is_async: diff --git a/starlette/endpoints.py b/starlette/endpoints.py index 73367c257..3d5df0e0b 100644 --- a/starlette/endpoints.py +++ b/starlette/endpoints.py @@ -1,8 +1,8 @@ -import asyncio import json import typing from starlette import status +from starlette._utils import iscoroutinefunction from starlette.concurrency import run_in_threadpool from starlette.exceptions import HTTPException from starlette.requests import Request @@ -37,7 +37,7 @@ async def dispatch(self) -> None: handler: typing.Callable[[Request], typing.Any] = getattr( self, handler_name, self.method_not_allowed ) - is_async = asyncio.iscoroutinefunction(handler) + is_async = iscoroutinefunction(handler) if is_async: response = await handler(request) else: diff --git a/starlette/exceptions.py b/starlette/exceptions.py index 8f28b6e2d..ace9a6ac5 100644 --- a/starlette/exceptions.py +++ b/starlette/exceptions.py @@ -1,7 +1,7 @@ -import asyncio import http import typing +from starlette._utils import iscoroutinefunction from starlette.concurrency import run_in_threadpool from starlette.requests import Request from starlette.responses import PlainTextResponse, Response @@ -94,7 +94,7 @@ async def sender(message: Message) -> None: raise RuntimeError(msg) from exc request = Request(scope, receive=receive) - if asyncio.iscoroutinefunction(handler): + if iscoroutinefunction(handler): response = await handler(request, exc) else: response = await run_in_threadpool(handler, request, exc) diff --git a/starlette/middleware/errors.py b/starlette/middleware/errors.py index 30f5570ca..e16111719 100644 --- a/starlette/middleware/errors.py +++ b/starlette/middleware/errors.py @@ -1,9 +1,9 @@ -import asyncio import html import inspect import traceback import typing +from starlette._utils import iscoroutinefunction from starlette.concurrency import run_in_threadpool from starlette.requests import Request from starlette.responses import HTMLResponse, PlainTextResponse, Response @@ -168,7 +168,7 @@ async def _send(message: Message) -> None: response = self.error_response(request, exc) else: # Use an installed 500 error handler. - if asyncio.iscoroutinefunction(self.handler): + if iscoroutinefunction(self.handler): response = await self.handler(request, exc) else: response = await run_in_threadpool(self.handler, request, exc) diff --git a/starlette/routing.py b/starlette/routing.py index 84ffcb3fb..20b6f6cd0 100644 --- a/starlette/routing.py +++ b/starlette/routing.py @@ -1,4 +1,3 @@ -import asyncio import contextlib import functools import inspect @@ -10,6 +9,7 @@ import warnings from enum import Enum +from starlette._utils import iscoroutinefunction from starlette.concurrency import run_in_threadpool from starlette.convertors import CONVERTOR_TYPES, Convertor from starlette.datastructures import URL, Headers, URLPath @@ -600,7 +600,7 @@ async def startup(self) -> None: Run any `.on_startup` event handlers. """ for handler in self.on_startup: - if asyncio.iscoroutinefunction(handler): + if iscoroutinefunction(handler): await handler() else: handler() @@ -610,7 +610,7 @@ async def shutdown(self) -> None: Run any `.on_shutdown` event handlers. """ for handler in self.on_shutdown: - if asyncio.iscoroutinefunction(handler): + if iscoroutinefunction(handler): await handler() else: handler() diff --git a/starlette/testclient.py b/starlette/testclient.py index c951767b4..3b2f6679f 100644 --- a/starlette/testclient.py +++ b/starlette/testclient.py @@ -1,4 +1,3 @@ -import asyncio import contextlib import http import inspect @@ -16,6 +15,7 @@ import requests from anyio.streams.stapled import StapledObjectStream +from starlette._utils import iscoroutinefunction from starlette.types import Message, Receive, Scope, Send from starlette.websockets import WebSocketDisconnect @@ -84,10 +84,7 @@ def _get_reason_phrase(status_code: int) -> str: def _is_asgi3(app: typing.Union[ASGI2App, ASGI3App]) -> bool: if inspect.isclass(app): return hasattr(app, "__await__") - elif inspect.isfunction(app): - return asyncio.iscoroutinefunction(app) - call = getattr(app, "__call__", None) - return asyncio.iscoroutinefunction(call) + return iscoroutinefunction(app) class _WrapASGI2: diff --git a/tests/test__utils.py b/tests/test__utils.py new file mode 100644 index 000000000..2ee241f9c --- /dev/null +++ b/tests/test__utils.py @@ -0,0 +1,70 @@ +import functools + +from starlette._utils import iscoroutinefunction + + +def test_async_func(): + async def async_func(): + ... + + def func(): + ... + + assert iscoroutinefunction(async_func) + assert not iscoroutinefunction(func) + + +def test_async_partial(): + async def async_func(a, b): + ... + + def func(a, b): + ... + + partial = functools.partial(async_func, 1) + assert iscoroutinefunction(partial) + + partial = functools.partial(func, 1) + assert not iscoroutinefunction(partial) + + +def test_async_method(): + class Async: + async def method(self): + ... + + class Sync: + def method(self): + ... + + assert iscoroutinefunction(Async().method) + assert not iscoroutinefunction(Sync().method) + + +def test_async_object_call(): + class Async: + async def __call__(self): + ... + + class Sync: + def __call__(self): + ... + + assert iscoroutinefunction(Async()) + assert not iscoroutinefunction(Sync()) + + +def test_async_partial_object_call(): + class Async: + async def __call__(self, a, b): + ... + + class Sync: + def __call__(self, a, b): + ... + + partial = functools.partial(Async(), 1) + assert iscoroutinefunction(partial) + + partial = functools.partial(Sync(), 1) + assert not iscoroutinefunction(partial) diff --git a/tests/test_background.py b/tests/test_background.py index e299ec362..f59ad966c 100644 --- a/tests/test_background.py +++ b/tests/test_background.py @@ -21,6 +21,26 @@ async def app(scope, receive, send): assert TASK_COMPLETE +def test_async_callable_task(test_client_factory): + TASK_COMPLETE = False + + class async_callable_task: + async def __call__(self): + nonlocal TASK_COMPLETE + TASK_COMPLETE = True + + task = BackgroundTask(async_callable_task()) + + async def app(scope, receive, send): + response = Response("task initiated", media_type="text/plain", background=task) + await response(scope, receive, send) + + client = test_client_factory(app) + response = client.get("/") + assert response.text == "task initiated" + assert TASK_COMPLETE + + def test_sync_task(test_client_factory): TASK_COMPLETE = False From a1c0764c43a1bf3d0eba0d5ba0d614a4cb191e8f Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 29 Jan 2022 14:29:37 +0100 Subject: [PATCH 2/8] Remove test from background tasks --- tests/test_background.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/tests/test_background.py b/tests/test_background.py index f59ad966c..e299ec362 100644 --- a/tests/test_background.py +++ b/tests/test_background.py @@ -21,26 +21,6 @@ async def app(scope, receive, send): assert TASK_COMPLETE -def test_async_callable_task(test_client_factory): - TASK_COMPLETE = False - - class async_callable_task: - async def __call__(self): - nonlocal TASK_COMPLETE - TASK_COMPLETE = True - - task = BackgroundTask(async_callable_task()) - - async def app(scope, receive, send): - response = Response("task initiated", media_type="text/plain", background=task) - await response(scope, receive, send) - - client = test_client_factory(app) - response = client.get("/") - assert response.text == "task initiated" - assert TASK_COMPLETE - - def test_sync_task(test_client_factory): TASK_COMPLETE = False From 636a26f6afe679b1dd1326eff46ed8af83a0e13f Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 29 Jan 2022 14:40:09 +0100 Subject: [PATCH 3/8] Fix coverage --- tests/test__utils.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test__utils.py b/tests/test__utils.py index 2ee241f9c..5c23d2790 100644 --- a/tests/test__utils.py +++ b/tests/test__utils.py @@ -5,10 +5,10 @@ def test_async_func(): async def async_func(): - ... + ... # pragma: no cover def func(): - ... + ... # pragma: no cover assert iscoroutinefunction(async_func) assert not iscoroutinefunction(func) @@ -16,10 +16,10 @@ def func(): def test_async_partial(): async def async_func(a, b): - ... + ... # pragma: no cover def func(a, b): - ... + ... # pragma: no cover partial = functools.partial(async_func, 1) assert iscoroutinefunction(partial) @@ -31,11 +31,11 @@ def func(a, b): def test_async_method(): class Async: async def method(self): - ... + ... # pragma: no cover class Sync: def method(self): - ... + ... # pragma: no cover assert iscoroutinefunction(Async().method) assert not iscoroutinefunction(Sync().method) @@ -44,11 +44,11 @@ def method(self): def test_async_object_call(): class Async: async def __call__(self): - ... + ... # pragma: no cover class Sync: def __call__(self): - ... + ... # pragma: no cover assert iscoroutinefunction(Async()) assert not iscoroutinefunction(Sync()) @@ -57,11 +57,11 @@ def __call__(self): def test_async_partial_object_call(): class Async: async def __call__(self, a, b): - ... + ... # pragma: no cover class Sync: def __call__(self, a, b): - ... + ... # pragma: no cover partial = functools.partial(Async(), 1) assert iscoroutinefunction(partial) From f6935c3d9fca1c288f9e7f1fd511a5928a9a65c5 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 29 Jan 2022 16:36:01 +0100 Subject: [PATCH 4/8] Add test for nested functools --- tests/test__utils.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test__utils.py b/tests/test__utils.py index 5c23d2790..db530f660 100644 --- a/tests/test__utils.py +++ b/tests/test__utils.py @@ -68,3 +68,12 @@ def __call__(self, a, b): partial = functools.partial(Sync(), 1) assert not iscoroutinefunction(partial) + + +def test_async_nested_partial(): + async def async_func(a, b): + return a + b + + partial = functools.partial(async_func, b=2) + nested_partial = functools.partial(partial, a=1) + assert iscoroutinefunction(nested_partial) From 73124568821b0379188a794ef975dae6ca53e8a8 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 29 Jan 2022 16:37:57 +0100 Subject: [PATCH 5/8] Ignore coverage --- tests/test__utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test__utils.py b/tests/test__utils.py index db530f660..303fbfc27 100644 --- a/tests/test__utils.py +++ b/tests/test__utils.py @@ -72,7 +72,7 @@ def __call__(self, a, b): def test_async_nested_partial(): async def async_func(a, b): - return a + b + ... # pragma: no cover partial = functools.partial(async_func, b=2) nested_partial = functools.partial(partial, a=1) From 4151df6599ca6f813e4014c482f3b4a4cc48f91b Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 29 Jan 2022 17:04:33 +0100 Subject: [PATCH 6/8] Deprecate iscoroutinefunction_or_partial --- starlette/routing.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/starlette/routing.py b/starlette/routing.py index 20b6f6cd0..ad68a427b 100644 --- a/starlette/routing.py +++ b/starlette/routing.py @@ -43,6 +43,11 @@ def iscoroutinefunction_or_partial(obj: typing.Any) -> bool: Correctly determines if an object is a coroutine function, including those wrapped in functools.partial objects. """ + warnings.warn( + "iscoroutinefunction_or_partial is deprecated, " + "and will be removed in a future release.", + DeprecationWarning, + ) while isinstance(obj, functools.partial): obj = obj.func return inspect.iscoroutinefunction(obj) @@ -53,7 +58,7 @@ def request_response(func: typing.Callable) -> ASGIApp: Takes a function or coroutine `func(request) -> response`, and returns an ASGI application. """ - is_coroutine = iscoroutinefunction_or_partial(func) + is_coroutine = iscoroutinefunction(func) async def app(scope: Scope, receive: Receive, send: Send) -> None: request = Request(scope, receive=receive, send=send) From 8438a9f26d42d66356d24c8be4073dc72a4feb57 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 29 Jan 2022 17:13:00 +0100 Subject: [PATCH 7/8] Ignore coverage for iscoroutinefunction_or_partial --- starlette/routing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starlette/routing.py b/starlette/routing.py index ad68a427b..1e013add9 100644 --- a/starlette/routing.py +++ b/starlette/routing.py @@ -38,7 +38,7 @@ class Match(Enum): FULL = 2 -def iscoroutinefunction_or_partial(obj: typing.Any) -> bool: +def iscoroutinefunction_or_partial(obj: typing.Any) -> bool: # pragma: no cover """ Correctly determines if an object is a coroutine function, including those wrapped in functools.partial objects. From 7c74668b073cc1d68b5ca2cae7ae4351df04e652 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Fri, 27 May 2022 07:09:57 +0200 Subject: [PATCH 8/8] Rename `iscoroutinefunction` to `is_async_callable` --- starlette/_utils.py | 2 +- starlette/authentication.py | 4 ++-- starlette/background.py | 4 ++-- starlette/endpoints.py | 4 ++-- starlette/middleware/errors.py | 4 ++-- starlette/middleware/exceptions.py | 4 ++-- starlette/routing.py | 8 ++++---- starlette/testclient.py | 4 ++-- tests/test__utils.py | 24 ++++++++++++------------ 9 files changed, 29 insertions(+), 29 deletions(-) diff --git a/starlette/_utils.py b/starlette/_utils.py index 7049c69c4..0710aebdc 100644 --- a/starlette/_utils.py +++ b/starlette/_utils.py @@ -3,7 +3,7 @@ import typing -def iscoroutinefunction(obj: typing.Any) -> bool: +def is_async_callable(obj: typing.Any) -> bool: while isinstance(obj, functools.partial): obj = obj.func diff --git a/starlette/authentication.py b/starlette/authentication.py index 4a76cd4a0..4affb4383 100644 --- a/starlette/authentication.py +++ b/starlette/authentication.py @@ -3,7 +3,7 @@ import typing from urllib.parse import urlencode -from starlette._utils import iscoroutinefunction +from starlette._utils import is_async_callable from starlette.exceptions import HTTPException from starlette.requests import HTTPConnection, Request from starlette.responses import RedirectResponse, Response @@ -53,7 +53,7 @@ async def websocket_wrapper( return websocket_wrapper - elif iscoroutinefunction(func): + elif is_async_callable(func): # Handle async request/response functions. @functools.wraps(func) async def async_wrapper( diff --git a/starlette/background.py b/starlette/background.py index 96ceb6db9..4aaf7ae3c 100644 --- a/starlette/background.py +++ b/starlette/background.py @@ -6,7 +6,7 @@ else: # pragma: no cover from typing_extensions import ParamSpec -from starlette._utils import iscoroutinefunction +from starlette._utils import is_async_callable from starlette.concurrency import run_in_threadpool P = ParamSpec("P") @@ -19,7 +19,7 @@ def __init__( self.func = func self.args = args self.kwargs = kwargs - self.is_async = iscoroutinefunction(func) + self.is_async = is_async_callable(func) async def __call__(self) -> None: if self.is_async: diff --git a/starlette/endpoints.py b/starlette/endpoints.py index 6a84f96a2..156663e49 100644 --- a/starlette/endpoints.py +++ b/starlette/endpoints.py @@ -2,7 +2,7 @@ import typing from starlette import status -from starlette._utils import iscoroutinefunction +from starlette._utils import is_async_callable from starlette.concurrency import run_in_threadpool from starlette.exceptions import HTTPException from starlette.requests import Request @@ -37,7 +37,7 @@ async def dispatch(self) -> None: handler: typing.Callable[[Request], typing.Any] = getattr( self, handler_name, self.method_not_allowed ) - is_async = iscoroutinefunction(handler) + is_async = is_async_callable(handler) if is_async: response = await handler(request) else: diff --git a/starlette/middleware/errors.py b/starlette/middleware/errors.py index 37580bc93..052b885f4 100644 --- a/starlette/middleware/errors.py +++ b/starlette/middleware/errors.py @@ -3,7 +3,7 @@ import traceback import typing -from starlette._utils import iscoroutinefunction +from starlette._utils import is_async_callable from starlette.concurrency import run_in_threadpool from starlette.requests import Request from starlette.responses import HTMLResponse, PlainTextResponse, Response @@ -170,7 +170,7 @@ async def _send(message: Message) -> None: response = self.error_response(request, exc) else: # Use an installed 500 error handler. - if iscoroutinefunction(self.handler): + if is_async_callable(self.handler): response = await self.handler(request, exc) else: response = await run_in_threadpool(self.handler, request, exc) diff --git a/starlette/middleware/exceptions.py b/starlette/middleware/exceptions.py index a3b4633d2..42fd41ae2 100644 --- a/starlette/middleware/exceptions.py +++ b/starlette/middleware/exceptions.py @@ -1,6 +1,6 @@ -import asyncio import typing +from starlette._utils import is_async_callable from starlette.concurrency import run_in_threadpool from starlette.exceptions import HTTPException from starlette.requests import Request @@ -79,7 +79,7 @@ async def sender(message: Message) -> None: raise RuntimeError(msg) from exc request = Request(scope, receive=receive) - if asyncio.iscoroutinefunction(handler): + if is_async_callable(handler): response = await handler(request, exc) else: response = await run_in_threadpool(handler, request, exc) diff --git a/starlette/routing.py b/starlette/routing.py index 46afec4ba..7e10b16f9 100644 --- a/starlette/routing.py +++ b/starlette/routing.py @@ -9,7 +9,7 @@ from contextlib import asynccontextmanager from enum import Enum -from starlette._utils import iscoroutinefunction +from starlette._utils import is_async_callable from starlette.concurrency import run_in_threadpool from starlette.convertors import CONVERTOR_TYPES, Convertor from starlette.datastructures import URL, Headers, URLPath @@ -57,7 +57,7 @@ def request_response(func: typing.Callable) -> ASGIApp: Takes a function or coroutine `func(request) -> response`, and returns an ASGI application. """ - is_coroutine = iscoroutinefunction(func) + is_coroutine = is_async_callable(func) async def app(scope: Scope, receive: Receive, send: Send) -> None: request = Request(scope, receive=receive, send=send) @@ -608,7 +608,7 @@ async def startup(self) -> None: Run any `.on_startup` event handlers. """ for handler in self.on_startup: - if iscoroutinefunction(handler): + if is_async_callable(handler): await handler() else: handler() @@ -618,7 +618,7 @@ async def shutdown(self) -> None: Run any `.on_shutdown` event handlers. """ for handler in self.on_shutdown: - if iscoroutinefunction(handler): + if is_async_callable(handler): await handler() else: handler() diff --git a/starlette/testclient.py b/starlette/testclient.py index 7cd76e569..efe2b493b 100644 --- a/starlette/testclient.py +++ b/starlette/testclient.py @@ -15,7 +15,7 @@ import requests from anyio.streams.stapled import StapledObjectStream -from starlette._utils import iscoroutinefunction +from starlette._utils import is_async_callable from starlette.types import Message, Receive, Scope, Send from starlette.websockets import WebSocketDisconnect @@ -84,7 +84,7 @@ def _get_reason_phrase(status_code: int) -> str: def _is_asgi3(app: typing.Union[ASGI2App, ASGI3App]) -> bool: if inspect.isclass(app): return hasattr(app, "__await__") - return iscoroutinefunction(app) + return is_async_callable(app) class _WrapASGI2: diff --git a/tests/test__utils.py b/tests/test__utils.py index 303fbfc27..fac57a2e5 100644 --- a/tests/test__utils.py +++ b/tests/test__utils.py @@ -1,6 +1,6 @@ import functools -from starlette._utils import iscoroutinefunction +from starlette._utils import is_async_callable def test_async_func(): @@ -10,8 +10,8 @@ async def async_func(): def func(): ... # pragma: no cover - assert iscoroutinefunction(async_func) - assert not iscoroutinefunction(func) + assert is_async_callable(async_func) + assert not is_async_callable(func) def test_async_partial(): @@ -22,10 +22,10 @@ def func(a, b): ... # pragma: no cover partial = functools.partial(async_func, 1) - assert iscoroutinefunction(partial) + assert is_async_callable(partial) partial = functools.partial(func, 1) - assert not iscoroutinefunction(partial) + assert not is_async_callable(partial) def test_async_method(): @@ -37,8 +37,8 @@ class Sync: def method(self): ... # pragma: no cover - assert iscoroutinefunction(Async().method) - assert not iscoroutinefunction(Sync().method) + assert is_async_callable(Async().method) + assert not is_async_callable(Sync().method) def test_async_object_call(): @@ -50,8 +50,8 @@ class Sync: def __call__(self): ... # pragma: no cover - assert iscoroutinefunction(Async()) - assert not iscoroutinefunction(Sync()) + assert is_async_callable(Async()) + assert not is_async_callable(Sync()) def test_async_partial_object_call(): @@ -64,10 +64,10 @@ def __call__(self, a, b): ... # pragma: no cover partial = functools.partial(Async(), 1) - assert iscoroutinefunction(partial) + assert is_async_callable(partial) partial = functools.partial(Sync(), 1) - assert not iscoroutinefunction(partial) + assert not is_async_callable(partial) def test_async_nested_partial(): @@ -76,4 +76,4 @@ async def async_func(a, b): partial = functools.partial(async_func, b=2) nested_partial = functools.partial(partial, a=1) - assert iscoroutinefunction(nested_partial) + assert is_async_callable(nested_partial)