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

On modularity & reusable ASGI components #99

Closed
tomchristie opened this issue Oct 18, 2018 · 2 comments
Closed

On modularity & reusable ASGI components #99

tomchristie opened this issue Oct 18, 2018 · 2 comments
Labels
feature New feature or request

Comments

@tomchristie
Copy link
Contributor

So I've been following a general approach with the design of Starlette, that I'd love to see Responder follow on with. The aims are to keep complexity nicely bounded into smallish components, that then end up being reusable across different ASGI frameworks. Some examples:

  • Starlette's router implementation is, itself, just a plain old ASGI app - you can run it directly, or plug in the test client to it, or submount it or whatever. There's a little light syntactic sugar if you're adding routes on an application instance, but all the complexity is neatly partitioned.
  • Similarly Starlette's GraphQL implementation is another plain old ASGI app. You can use it in any ASGI framework, or just use it directly without the application instance. Responder could use it if it wanted. (It deals with either running in a threadpool, or using an ASync executor, which is important.)
  • Starlette's class based views. Again, they're just tiny ASGI apps - it's nice because it keeps the complexity nicely bounded away from the application instance, and again, you can use them with any ASGI framework, or use them directly. Being able to plug the test client directly in at that level of interface means you end up with really simple test cases for testing the core functionality.

So, none of this means that you can't at the top level have an application interface that exposes a single point of configuration, but under the hood it all ends up just being composition of small nicely bounded components and middleware.

Places where this could be applied to responder:

  • Pull the GraphQL implementation out of the app class itself, and have an standalone ASGI implementation, that the app instance uses composition to add in.
  • Design the Router so that it's exposing an ASGI interface. Have router instances that expose the same routing API as the top-level application class, but don't come with any of the other stuff that the application gives you. That'd then be independently usable/testable. You can do neat things like mount a router instance under another app, or wrap it up in middleware, etc...
  • Similarly with the class based views. Have the on_request(req/resp) interface stuff be implemented as a tiny ASGI app. It's independently usable/testable, you can apply middleware to individual views, you can reuse it in other frameworks, or you can use it as the basis for writing alternate class based view implementations (without the implementation all being tightly bound to the rest of the application class)
@kennethreitz kennethreitz added the feature New feature or request label Oct 19, 2018
@kennethreitz
Copy link
Owner

I like this as a goal, but I don't think we need to put too much focus on it, early on.

@tomchristie
Copy link
Contributor Author

tomchristie commented Oct 19, 2018

I don't think we need to put too much focus on it, early on.

Sure. To me it's super important, because it's potentially the difference between "let's design monolithic frameworks that have to completely own the space to succeed" vs. "let's build a better ecosystem, throughout".

I think ASGI has a tremendous opportunity here to build a more collaborative ecosystem, and establish patterns whereby high-level frameworks are composed of nicely isolated units.

For example, if responder designs it's request and response models like so...

class Request:
    def __init__(self, scope, receive):
        ...
class Response
    def __init__(self, scope):
        ...

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

Then they become nicely reusable in any ASGI app. You can drop all the way down to writing a simple "hello, world" ASGI application, without any of the rest of responder, but using a Responder request and/or response classes, and automatically getting it's content negotiated behavior etc.. as a result.

from responder.models import Request, Response

# I'm a standard ASGI app, you can run me directly with `daphne`, `uvicorn`, or `hypercorn`.
# You can test me with any standard ASGI TestClient.
# You can wrap me in middleware, or dispatch to me from any ASGI framework's routing.
class HelloWorld:
    def __init__(self, scope):
        self.scope = scope

    async def __call__(self, receive, send):
        request = Request(self.scope, receive)
        response = Response(self.scope)
        ...  # Do stuff
        await response(receive, send)

Working at those lower levels of abstraction isn't necessarily what most of us would be doing day-to-day, most of the time, but being able to is super important, and breaks down the "I need to understand how the complete framework functions in order to understand one of it's components"

I think it's important to start thinking about at least reasonably early on, otherwise you start introducing points of coupling that later become entrenched.

(I'll park these thoughts for now)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants