Skip to content
This repository was archived by the owner on May 24, 2022. It is now read-only.
This repository was archived by the owner on May 24, 2022. It is now read-only.

Redesigning aiodine? #26

@florimondmanca

Description

@florimondmanca

Coming back at aiodine after a few months off, the initial design and API don't seem as optimal as they could be from a developer experience point of view.

The most serious problems I see are:

  • Lack of editor support, mainly because @consumer modifies the signature of its decorated function (Consumer API and signature modification #23)
  • Complexity: there are a lot of concepts, e.g. "provider", "consumer", "store", "provider freezing", etc. All of them seem overly complicated and potentially leaky abstractions.

The path that FastAPI took for its dependency injection mechanism seems to solve both of these problems. I'm curious at whether we could replicate the design here, although that should probably mean aiodine is spun up as a different library altogether (code-named asyncdeps below).

import asyncio
from asyncdeps import depends

async def get_hello() -> str:
    return "Hello"

async def show(hello: str = depends(get_hello)) -> None:
    print(hello)

asyncio.run(show())

We should make sure depends works well with static type checkers, e.g. by casting its return value to the return value type of its function.

If we go for this style, there's one question to tackle: can we keep scopes with this API?

My intuition is that we can keep the idea of caching dependencies for reuse (which is what the "session" scope is all about), but propose a more straight-forward API.

The default behavior would be to evaluate dependencies on every call (equivalent of the current "function" scope). The behavior of the "session" scope could be obtained via an @cached marker, e.g.:

from asyncdeps import depends, cached

@cached
async def get_hello() -> str:
    print("Evaluating hello…")
    return "Hello"

async def show(hello: str = depends(get_hello)) -> None:
    print(hello)

async def main():
    await show()  # Prints "Evaluating hello…"
    await show()  # Nothing printed (hello already evaluated in this scope)

Besides, we can force-turn on/off caching on callees, e.g. depends(get_hello, cached=False).

Combined with a generator-based API, this would allow us to support the database provisioning use case for async web frameworks, e.g. in Bocadillo:

from bocadillo import App
from asyncdeps import depends, cached
from databases import Database

app = App()

@cached
async def get_db() -> Database:
    async with Database("sqlite://:memory:") as db:
        yield db

@app.route("/")
async def home(req, res, message: Database = depends(get_db)):
    res.text = message

Overall, I think this API would greatly reduce the scope and footprint of this library, as well as result in much simpler usage patterns for users.

I may sketch out a PoC for async-only dependencies (w/o generator support) to see how this could look like.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions