python-injector/injector

Async support

askerka opened this issue · 6 comments

Hi! Thank you for the great library.

I just wanted to ask if there any plans for support async version of it.

Thanks!

Hey, thank you for the kind words. I have no plans to implement this as I don't need it but I'm happy to accept a pull request (if someone desires to create one there's some design discussion to have beforehand).

Hi, I've been using injector in most of my projects for a couple of months now, and found 2 different approaches to define providers of awaitable types, which I think you may find helpful. A most basic example would be aioredis, as its create_connection and create_redis_pool are both coroutines.

First, most obvious approach:

Using injector.binder.bind(Redis, to=redis) in an async context:

async def create_app():
    """Create and return aiohttp application."""
    module = MyModule()
    injector = Injector(module)
    # Use async context to bind awaitables
    await injector.call_with_injection(module.setup_aio_providers)
    return injector.get(App)

And in the module:

Class MyModule(Module):
    ...
    @inject
    async def setup_aio_providers(self, injector: Injector, config: Dynaconf):
        redis = await create_redis_pool(
            config.redis.address,
            db=config.redis.db_index,
            encoding=config.redis.encoding,
        )
        injector.binder.bind(Redis, to=redis)

The downside is, we're losing the lazy-loaded nature of providers themselves, meaning redis is always created, event if we don't use it (important if you reuse the code for multiple apps / microservices).

Second approach

Use thread pool to set-up redis, while passing your loop explicitly to the constructor. I think it's a cleaner solution, which regains the lazy nature and keeps the module more vanilla.

Class MyModule(Module):
    ...
    @provider
    def provide_event_loop(self) -> Loop:
        return asyncio.get_event_loop()

    @provider
    @singleton
    def provide_redis(self, config: Dynaconf, loop: Loop, pool: ThreadPoolExecutor) -> Redis:
        coroutine = create_redis_pool(
            config.redis.address,
            db=config.redis.db_index,
            loop=loop,
            encoding=config.redis.encoding,
        )
        return pool.submit(asyncio.run, coroutine).result()

The second approach I haven't field tested anywhere, please let me know if It's not safe / a bad practice.
Hope this helps.

The second approach should work and is a good way to achieve on demand object creation, yeah.

Thank you, @Litery!
We went with the first option because with the second one we have a problem like "Task attached to a different loop" because asyncio.run creates a new loop and close it after. We've tried to fix it but unsuccessfully.

I believe I can close the issue. Thank you!

Hi, I've been using injector in most of my projects for a couple of months now, and found 2 different approaches to define providers of awaitable types, which I think you may find helpful. A most basic example would be aioredis, as its create_connection and create_redis_pool are both coroutines.

First, most obvious approach:

Using injector.binder.bind(Redis, to=redis) in an async context:

async def create_app():
    """Create and return aiohttp application."""
    module = MyModule()
    injector = Injector(module)
    # Use async context to bind awaitables
    await injector.call_with_injection(module.setup_aio_providers)
    return injector.get(App)

And in the module:

Class MyModule(Module):
    ...
    @inject
    async def setup_aio_providers(self, injector: Injector, config: Dynaconf):
        redis = await create_redis_pool(
            config.redis.address,
            db=config.redis.db_index,
            encoding=config.redis.encoding,
        )
        injector.binder.bind(Redis, to=redis)

The downside is, we're losing the lazy-loaded nature of providers themselves, meaning redis is always created, event if we don't use it (important if you reuse the code for multiple apps / microservices).

Second approach

Use thread pool to set-up redis, while passing your loop explicitly to the constructor. I think it's a cleaner solution, which regains the lazy nature and keeps the module more vanilla.

Class MyModule(Module):
    ...
    @provider
    def provide_event_loop(self) -> Loop:
        return asyncio.get_event_loop()

    @provider
    @singleton
    def provide_redis(self, config: Dynaconf, loop: Loop, pool: ThreadPoolExecutor) -> Redis:
        coroutine = create_redis_pool(
            config.redis.address,
            db=config.redis.db_index,
            loop=loop,
            encoding=config.redis.encoding,
        )
        return pool.submit(asyncio.run, coroutine).result()

The second approach I haven't field tested anywhere, please let me know if It's not safe / a bad practice. Hope this helps.

In tests, how would you replace Redis with some mock e.g.?