-
-
Notifications
You must be signed in to change notification settings - Fork 948
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve detection of async callables #1444
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
Quick check with grep reveals that asyncio
is now mentioned only in starlette/_utils
and tests ;)
while isinstance(obj, functools.partial): | ||
obj = obj.func | ||
|
||
return asyncio.iscoroutinefunction(obj) or ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @Kludex, would it make sense to use inspect.iscoroutinefunction
here?
I understand why we cannot just use the said function alone, since async def __call__(...)
is not captured by it:
>>> import inspect
>>> class A:
... async def __call__(self):
... ...
...
>>> inspect.iscoroutinefunction(A())
False
I have looked at the code of asyncio.iscoroutinefunction
and it only additionally check for the deprecated @coroutine
decorator.
Cheers,
Libor
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
By the way, the unwrapping pattern of
Of the cases found, the one relevant here is line 46 in 'iscoroutinefunction_or_partial' function. Does it make sense to use your newly defined function instead of it? Edit: Looking at it further, wouldn't the replacement of ^ @Kludex |
Doesn't look like 😞
One of the goals for this PR was to remove/deprecate that, and I forgot! 😆 |
Hmmm.. Now I understand why It looks like it solves that comment, yes. But I'll have to do something like: def find_function(obj: typing.Any) -> typing.Callable:
while inspect.ismethod(obj):
obj = obj.__func__
while isinstance(obj, functools.partial):
obj = obj.func
return obj
def iscoroutinefunction(obj: typing.Any) -> bool:
obj = find_function(obj)
return asyncio.iscoroutinefunction(obj) or (
callable(obj) and asyncio.iscoroutinefunction(obj.__call__)
) And then use the |
And not even like this? I have reproduced the behaviour from your branch and it works - as in,
Or am I mixing different concepts here? |
Yeah, you're mixing things. That's not the same as the example provided on the comment you mentioned above. Your example (on the quoted of this message) should never be supported... |
Ad #1444 (comment) : Yeah, I figured. xD Anyway, as you pointed to asgiref - the correct way is then
|
Actually, I'm going to let the state of this PR as is. My previous statement about the |
Thanks for the review @bibajz ! :) |
Yeah, it looks like distinguishing coroutine/normal functions is pretty tricky, so good idea to leave for another day. Glad to help, I really dig Starlette and other stuff from Cheers |
We need to organize ourselves a little better. It's hard for me to tell you where the help is needed... You can check the issues and PRs and see if you can help on them. I'm available on our Gitter if you need something. |
Thanks, I will look around and pick something. :) |
What's the simplest possible example of an issue that this resolves from a user perspective? |
@tomchristie I ran into this issue while working on a custom route_handler in fastapi. I'm guessing the exact specifics of what and why are not that interesting to you, but this fix would actually be used (at least by me). I could of course solve it by not using a class, or wrapping the class in a async function, but I think in the spirit of duck-typing, an async callable class instance should be handled the same way as the normal async functions. |
starlette/_utils.py
Outdated
import typing | ||
|
||
|
||
def iscoroutinefunction(obj: typing.Any) -> bool: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coming from reviewing #1644
This is a nit, but should we call this is_async_callable
?
AFAICT, having "something we can call and await
" is all we care about when checking for apps or endpoints.
"Coroutine function" is a well-defined word in the Python glossary: a "function which returns a coroutine object", defined with "async def
".
https://docs.python.org/3/glossary.html#term-coroutine-function
Also, the current naming makes it look as a compatibility shim on asyncio.iscoroutinefunction
which focuses on checking just the definition above^, whereas we do some more checks, such as looking for __call__
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a nit, but should we call this is_async_callable?
Sure. 👍 I'll adapt later today. Thanks for the review. 🙏
3662c96
to
7c74668
Compare
As a user, I'll be able to create an endpoint with async callable objects, async callable methods, and class Foo:
async def __call__(self, request):
return Response(...)
class Foo:
async def method(self, request):
return Response(...) This also includes creating startup/shutdown events, background tasks and exception handlers as such. See more on the test. As a user, I don't see any reason for the framework to restrict my choices here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good job!
Thanks everybody! 🎉 |
References: