Skip to content
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

Merged
merged 10 commits into from
May 28, 2022

Conversation

Copy link
Contributor

@bibajz bibajz left a 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 (
Copy link
Contributor

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.

tests/test__utils.py Show resolved Hide resolved
@bibajz
Copy link
Contributor

bibajz commented Jan 29, 2022

By the way, the unwrapping pattern of while isinstance(obj, functools.partial), I have run a grep on the main branch and

$ rg 'isinstance\(\w+, (partial|functools.partia)l\)'
starlette/routing.py
46:    while isinstance(obj, functools.partial):
202:        while isinstance(endpoint_handler, functools.partial):
283:        while isinstance(endpoint_handler, functools.partial):

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 iscoroutinefunction_or_partial by your checker actually solve this issue #734 ? Especially this one: #734 (comment)

^ @Kludex

@Kludex
Copy link
Member Author

Kludex commented Jan 29, 2022

Edit: Looking at it further, wouldn't the replacement of iscoroutinefunction_or_partial by your checker actually solve this issue #734 ? Especially this one: #734 (comment)

Doesn't look like 😞

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?

One of the goals for this PR was to remove/deprecate that, and I forgot! 😆
Thanks!

@Kludex
Copy link
Member Author

Kludex commented Jan 29, 2022

Edit: Looking at it further, wouldn't the replacement of iscoroutinefunction_or_partial by your checker actually solve this issue #734 ? Especially this one: #734 (comment)

Doesn't look like disappointed

Hmmm.. Now I understand why ismethod is included on asgiref.

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 find_function on the WebSocketRoute and Route.

@bibajz
Copy link
Contributor

bibajz commented Jan 29, 2022

And not even like this? I have reproduced the behaviour from your branch and it works -

as in, /a prints A and /b prints B in browser

from starlette import applications, responses, routing                                                                                                                                        
    
    
class Ping:    
    def __init__(self, message):    
        self._message = message    
    
    async def __call__(self, scope, request, response):    
        plain_text_resp = responses.PlainTextResponse(self._message)    
        return await plain_text_resp(scope, request, response)    
      
      
app = applications.Starlette(      
    routes=[      
        routing.Route("/a", Ping("A"), methods=["GET"]),      
        routing.Route("/b", Ping("B"), methods=["GET"]),      
    ]      
) 

Or am I mixing different concepts here?

@Kludex
Copy link
Member Author

Kludex commented Jan 29, 2022

And not even like this? I have reproduced the behaviour from your branch and it works -

as in, /a prints A and /b prints B in browser

from starlette import applications, responses, routing                                                                                                                                        
    
    
class Ping:    
    def __init__(self, message):    
        self._message = message    
    
    async def __call__(self, scope, request, response):    
        plain_text_resp = responses.PlainTextResponse(self._message)    
        return await plain_text_resp(scope, request, response)    
      
      
app = applications.Starlette(      
    routes=[      
        routing.Route("/a", Ping("A"), methods=["GET"]),      
        routing.Route("/b", Ping("B"), methods=["GET"]),      
    ]      
) 

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

@bibajz
Copy link
Contributor

bibajz commented Jan 29, 2022

Ad #1444 (comment) : Yeah, I figured. xD

Anyway, as you pointed to asgiref - the correct way is then

  1. unmethod
  2. unpartial
  3. iscoroutinefunction is that so?

@Kludex
Copy link
Member Author

Kludex commented Jan 29, 2022

Actually, I'm going to let the state of this PR as is.

My previous statement about the ismethod was wrong.

@Kludex
Copy link
Member Author

Kludex commented Jan 29, 2022

Thanks for the review @bibajz ! :)

@bibajz
Copy link
Contributor

bibajz commented Jan 29, 2022

Ad #1444 (comment)

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 encode ecosystem, so if you can point me to some issue that would be worth to look at here, let me know!

Cheers
Libor

@Kludex
Copy link
Member Author

Kludex commented Jan 29, 2022

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.

@bibajz
Copy link
Contributor

bibajz commented Jan 29, 2022

Thanks, I will look around and pick something. :)

@tomchristie
Copy link
Member

What's the simplest possible example of an issue that this resolves from a user perspective?

@adriangb adriangb added bug Something isn't working refactor Refactor code labels Feb 2, 2022
@frerikandriessen
Copy link

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

@Kludex Kludex added this to the Version 0.21.0 milestone Apr 26, 2022
@Kludex Kludex mentioned this pull request May 22, 2022
5 tasks
import typing


def iscoroutinefunction(obj: typing.Any) -> bool:
Copy link
Member

@florimondmanca florimondmanca May 24, 2022

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__.

Copy link
Member Author

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. 🙏

@Kludex Kludex changed the title Improve detection of coroutine functions Improve detection of async callables May 27, 2022
@Kludex
Copy link
Member Author

Kludex commented May 27, 2022

What's the simplest possible example of an issue that this resolves from a user perspective?

As a user, I'll be able to create an endpoint with async callable objects, async callable methods, and functool.partial:

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.

@Kludex Kludex requested review from florimondmanca and a team May 27, 2022 05:26
Copy link
Member

@abersheeran abersheeran left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job!

@Kludex Kludex merged commit 5b058fa into encode:master May 28, 2022
@Kludex Kludex deleted the feat/coroutine-functions branch May 28, 2022 07:29
@Kludex
Copy link
Member Author

Kludex commented May 28, 2022

Thanks everybody! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working refactor Refactor code
Projects
None yet
Development

Successfully merging this pull request may close these issues.

objects defining async __call__ not run/awaited when used in on_startup
7 participants