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.
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.
Most helpful comment
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:
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: