question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

On ASGI's double-callable interface.

See original GitHub issue

This isn’t intended as an actionable point right now, rather just a point of conversation. I think it’s worth bringing up given that ASGI is pre-PEP at this point. (And that we might still just about be at a point where we could adapt things if we felt it worthwhile enough.)

I’m starting to wonder (again) if the double-callable structure if neccessarily a better trade-off than a single-callable. eg. the difference between:

ASGI as it stands:

class App:
    def __init__(self, scope):
        self.scope = scope

    async def __call__(self, receive, send):
        await send({
            'type': 'http.response.start',
            'status': 200,
            'headers': [
                [b'content-type', b'text/plain'],
            ]
        })
        await send({
            'type': 'http.response.body',
            'body': b'Hello, world!',
        })

An alternative interface:

async def app(scope, receive, send):
    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [
            [b'content-type', b'text/plain'],
        ]
    })
    await send({
        'type': 'http.response.body',
        'body': b'Hello, world!',
    })

The double-callable does allow classes to directly implement the ASGI interface, and for class based endpoint implementations it ends up being simpler.

However it’s more complex for function based endpoint implementations, for middleware implementation patterns, and for instantiate-to-configure application implementations.

In each of those cases you end up either with a closure around an inner function, or partially-bound functools.partial. Both of those cases make for less easy-to-understand concepts and more awkward flows.

E.g. A typical middleware pattern with ASGI

class Middleware:
    def __init__(self, app, **middleware_config):
        self.app = app
        ...

    def __call__(self, scope):
        return functools.partial(self.asgi, scope=scope)

    async def asgi(self, receive, send, scope):
        ...

An alternative:

class Middleware:
    def __init__(self, app, **middleware_config):
        self.app = app
        ...

    async def __call__(self, scope, receive, send):
        ...

Similarly with instantiate-to-configure ASGI applications, you currently end up having to return a partially bound method:

StaticFiles as ASGI

class StaticFiles:
    def __init__(self, directory):
        self.directory = directory
        ...

    def __call__(self, scope):
        return functools.partial(self.asgi, scope=scope)

    async def asgi(self, receive, send, scope):
        ...

Alternative:

class StaticFiles:
    def __init__(self, directory):
        self.directory = directory
        ...

    async def __call__(self, scope, receive, send):
        ...

The flip side with a single-callable is that you can’t point directly at a class as an ASGI interface. For endpoint implementations, you end up needing to do something like this pattern instead…

class App:
    def __init__(self, scope, receive, send):
        self.scope = scope
        self.receive = receive
        self.send = send

    async def dispatch(self):
        await self.send({'type': 'http.response.start', ...})
        await self.send({'type': 'http.response.body', ...})

    @classmethod
    async def asgi(cls, scope, receive, send):
        instance = cls(scope, receive, send)
        await instance.dispatch()

# app = App.asgi

This all starts matters a little in Starlette, where ASGI is the interface all the way through - you end up with a more complex flow, and less easy-to-understand primitives. It also ends up with more indirect tracebacks.

(I noticed this in particular when considering how to improve the stack traces for 500 responses. I was hoping to delimit each frame as “Inside the middleware stack”, “Inside the router”, “Inside the endpoint” - that’s easy to do if the traceback frames match up directly to the ASGI functions themselves, but it’s not do-able if the frames are from a function that’s been returned from another function.)

Anyways, not asking for any action on this neccessarily, but it’s something I’ve been thinking over that I wanted to make visible while ASGI is still pre-PEP.

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:38 (35 by maintainers)

github_iconTop GitHub Comments

1reaction
tomchristiecommented, Mar 4, 2019

Summary of what I now think would be sufficient for a 3.0 release:

  • Single callable async def app(scope, receive, send), with backwards support for the double callable style.
  • Supplement the existing lifespan.startup.complete and lifespan.shutdown.complete messages with complementary lifespan.startup.failed and lifespan.shutdown.failed {"type": ..., "exc": ...} messages.

That’d enough that we don’t neccessarily need to include any handshaking or supported-protocol info beyond the currently existing spec.

1reaction
tomchristiecommented, Feb 6, 2019

Okay, had “Da-da-DAAAA” moment this morning.

Class instances can be awaitable.

class App:
    def __init__(self, scope, receive, send):
        self.scope = scope
        self.receive = receive
        self.send = send

    def __await__(self):
        return self.dispatch().__await__()

    async def dispatch(self):
        ...

This gives you: await App(scope, receive, send), which will instantiate the class, and then run dispatch(). So the single-callable style can still use an “instantiate then run” pattern, and can support uninstantiated classes as applications.

One nice property about the __init__ + __call__ pattern is that Flask can tell whether it’s in WSGI or ASGI mode automatically

Sure, so:

class App:
    def __init__(self, **config):
        pass

    def wsgi(self, environ, start_response):
        print({"environ": environ, "start_response": start_response})

    async def asgi(self, scope, receive, send):
        print({"scope": scope, "receive": receive, "send": send})

    def __call__(self, *args):
        assert len(args) in (2, 3)
        if len(args) == 2:
            return self.wsgi(*args)
        return self.asgi(*args)


async def run():
    app = App()
    print("WSGI:")
    app("environ", "start_response")
    print("ASGI:")
    await app("scope", "receive", "send")


loop = asyncio.get_event_loop()
loop.run_until_complete(run())
Read more comments on GitHub >

github_iconTop Results From Across the Web

How to Implement Callable Interface in Java - Edureka
This article will provide you with a detailed and comprehensive knowledge of how to implement Callable Interface in Java with examples.
Read more >
Callable (Java Platform SE 8 ) - Oracle Help Center
The Callable interface is similar to Runnable , in that both are designed for classes whose instances are potentially executed by another thread....
Read more >
LAMBDA Expression in Java | Aegis Softtech
Code with Runnable interface, in the below example I am creating a Thread class, and assigning the Runnable anonymous inner class object as...
Read more >
Calling a method in ExecuterService using double colon (::)
I have a class constitues 2 methods static and non static respectively, as per my limited knowledge submit method accepts runnable,callable ...
Read more >
GLPK - GNU Project - Free Software Foundation (FSF)
The GLPK (GNU Linear Programming Kit) package is intended for solving large-scale linear programming (LP), mixed integer programming (MIP), and other related ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found