Sanic: Components instead of globals

Created on 12 Oct 2018  路  6Comments  路  Source: sanic-org/sanic

I like how apistar and vibora implemented components and I would like to see something similar in Sanic, in stead of using globals.

More information: https://docs.vibora.io/initial

There is no need to reinvent whole dependency injection thing and components can be injected using https://github.com/ivankorobkov/python-inject for example.

Components also could be used for things like authentication.

enhancement

Most helpful comment

On the other hand @vltr is working on something just like this as a plugin. I'll let him explain further.

Thanks for the heads up, @ahopkins !

@sirex @yunstanford I'm working on a side project called sanic-boom, which deals exactly with this kind of "Component" system (but with some other goodies as "layered" middlewares, etc). You can take a look at the repository (it's not published on PyPI yet because I'm finding some hard time to write more tests (although it's around 85% covered) and documentation).

For what it is all about, here's a simple example from the tests:

import inspect
import json
import typing as t
import uuid

from sanic.response import text

from sanic_boom import Component, ComponentCache


class RequestIdentifier:
    pass


class ComplexJSONBody(t.Generic[t.T_co]):
    pass


class ComplexJSONBodyComponent(Component):
    def resolve(self, param: inspect.Parameter) -> bool:
        if hasattr(param.annotation, "__origin__"):
            return param.annotation.__origin__ == ComplexJSONBody
        return False

    async def get(self, request, param: inspect.Parameter) -> object:
        inferred_type = param.annotation.__args__[0]
        return inferred_type(**request.json)


class RequestIdentifierComponent(Component):
    def resolve(self, param: inspect.Parameter) -> bool:
        return param.annotation == RequestIdentifier

    async def get(self, request, param: inspect.Parameter) -> object:
        return str(uuid.uuid4())

    def get_cache_lifecycle(self) -> ComponentCache:
        return ComponentCache.REQUEST


def test_complex_component(app, srv_kw):
    class TestBody:
        def __init__(self, name=None, age=None):
            self.name = name
            self.age = age

        def say_hi(self):
            return "{}, aged {}, says hi".format(self.name, self.age)

    app.add_component(ComplexJSONBodyComponent)

    @app.post("/")
    async def handler(complex_body: ComplexJSONBody[TestBody]):
        assert isinstance(complex_body, TestBody)
        assert complex_body.name == "John"
        assert complex_body.age == 42
        return text(complex_body.say_hi())

    request, response = app.test_client.post(
        "/", data=json.dumps({"name": "John", "age": 42}), **srv_kw
    )
    assert response.status == 200
    assert response.text == "John, aged 42, says hi"


def test_cached_component(app, srv_kw):

    app.add_component(RequestIdentifierComponent)

    @app.middleware  # global
    async def req_middleware(request, req_uuid: RequestIdentifier):
        request[req_uuid] = 1

    @app.get("/uuid")
    async def handler(request, req_uuid: RequestIdentifier):
        request[req_uuid] += 1
        return text(req_uuid)

    request, response = app.test_client.get("/uuid", **srv_kw)
    assert response.status == 200
    assert request[response.text] == 2

    # the cache is only valid for the request lifecycle
    request, response = app.test_client.get("/uuid", **srv_kw)
    assert response.status == 200
    assert request[response.text] == 2

I would love to see this directly on Sanic, I'll just find it hard to accomplish a few things the way Sanic is today ... But anyway, it's an option :wink:

All 6 comments

This is likely not something that is on the near term development list (while I do agree that component injection is awesome). On the other hand @vltr is working on something just like this as a plugin. I'll let him explain further.

+1 for the idea though!

I don't see anything needs to be global if request has attached app.

@yunstanford I think what he means by globals is that you create "something" in a global namespace and then have to figure out how to pass it around. Sure, you can use request.app, and in fact that is what sanic-jwt does by extending the authentication module to request.app.auth.

But, what component injection on a project like vibora adds is the ability to just declaratively tell your route what you want available:

@bp.get(...)
async def myroute(request, user, db, ...)

I have not looked at the code that vibora uses, but I am guessing that "registering" a component basically puts it on your app instance and the decorator class takes a look at something like getfullargspec and injects across matches.

Essentially ... doing what you suggested. Basically a fancier way to do setattr(request, ...)

On the other hand @vltr is working on something just like this as a plugin. I'll let him explain further.

Thanks for the heads up, @ahopkins !

@sirex @yunstanford I'm working on a side project called sanic-boom, which deals exactly with this kind of "Component" system (but with some other goodies as "layered" middlewares, etc). You can take a look at the repository (it's not published on PyPI yet because I'm finding some hard time to write more tests (although it's around 85% covered) and documentation).

For what it is all about, here's a simple example from the tests:

import inspect
import json
import typing as t
import uuid

from sanic.response import text

from sanic_boom import Component, ComponentCache


class RequestIdentifier:
    pass


class ComplexJSONBody(t.Generic[t.T_co]):
    pass


class ComplexJSONBodyComponent(Component):
    def resolve(self, param: inspect.Parameter) -> bool:
        if hasattr(param.annotation, "__origin__"):
            return param.annotation.__origin__ == ComplexJSONBody
        return False

    async def get(self, request, param: inspect.Parameter) -> object:
        inferred_type = param.annotation.__args__[0]
        return inferred_type(**request.json)


class RequestIdentifierComponent(Component):
    def resolve(self, param: inspect.Parameter) -> bool:
        return param.annotation == RequestIdentifier

    async def get(self, request, param: inspect.Parameter) -> object:
        return str(uuid.uuid4())

    def get_cache_lifecycle(self) -> ComponentCache:
        return ComponentCache.REQUEST


def test_complex_component(app, srv_kw):
    class TestBody:
        def __init__(self, name=None, age=None):
            self.name = name
            self.age = age

        def say_hi(self):
            return "{}, aged {}, says hi".format(self.name, self.age)

    app.add_component(ComplexJSONBodyComponent)

    @app.post("/")
    async def handler(complex_body: ComplexJSONBody[TestBody]):
        assert isinstance(complex_body, TestBody)
        assert complex_body.name == "John"
        assert complex_body.age == 42
        return text(complex_body.say_hi())

    request, response = app.test_client.post(
        "/", data=json.dumps({"name": "John", "age": 42}), **srv_kw
    )
    assert response.status == 200
    assert response.text == "John, aged 42, says hi"


def test_cached_component(app, srv_kw):

    app.add_component(RequestIdentifierComponent)

    @app.middleware  # global
    async def req_middleware(request, req_uuid: RequestIdentifier):
        request[req_uuid] = 1

    @app.get("/uuid")
    async def handler(request, req_uuid: RequestIdentifier):
        request[req_uuid] += 1
        return text(req_uuid)

    request, response = app.test_client.get("/uuid", **srv_kw)
    assert response.status == 200
    assert request[response.text] == 2

    # the cache is only valid for the request lifecycle
    request, response = app.test_client.get("/uuid", **srv_kw)
    assert response.status == 200
    assert request[response.text] == 2

I would love to see this directly on Sanic, I'll just find it hard to accomplish a few things the way Sanic is today ... But anyway, it's an option :wink:

sanic-boom version 0.1.0 released on PyPI today :tada: I hope you enjoy it and sorry if it still lacks some documentation ... But you can find some interesting pieces of code in the tests folder :wink:

Thanks! Gonna close this issue.

Was this page helpful?
0 / 5 - 0 ratings