ivankorobkov / python-inject

Python dependency injection
Apache License 2.0
672 stars 77 forks source link

[Feature Request/Question] Asyncio Support #40

Closed givo closed 4 years ago

givo commented 4 years ago

Hi,

First of all, great job on this project. I've been testing pinject, injector and dependency-injector, and in my opinion your DI framework is more convenient and stable then the others. Most important, you support hashable objects as binding keys.

Are there any plans on implementing support for asyncio? It could be very useful to have an asyncio support.

An example for having an async Provider Binding:

# one module
async def get_cache():
    return await cache.get('something')

# DI configuration module
def config(binder):
    # Executes the provider on each injection.
    binder.bind_to_provider('CACHE', get_cache) 

# module that depends on db connection
@inject.params(cache='CACHE')
async def get_users(cache):
    # do something with cache

Thanks in advance :)

ivankorobkov commented 4 years ago

Hi!

In my opinion the whole point of DI is to be able to represent an application as a static graph of interconnected objects/services. That's why there are not scopes or dynamic binding. Static/immutable also leads to simple and cheap concurrency and thread-safety.

The idea to add asyncio support seems logic. However, it will make the whole application graph async. This will lead to a partially initialized app graph. This will make thread-safety a very difficult problem to solve.

All in all, thanks for the idea. But it will dramatically complicate the injector.

Also, you can always fallback to something like this:

class Cache:
  async def load():
    pass

@inject.params(cache=Cache)
async def get_users(cache):
  return await cache.load().get_users()
givo commented 4 years ago

Hi!

First of all, thanks for your response.

I agree with your opinion of having a static graph and I understand the implications of adding asyncio support in the injector.

I think that today most of the applications are and going to be implemented using asyncio or similar framework. Therefore, developers may find it very useful to have an async DI framework, exactly like yours, that will create a static object graph asynchronously (only the creation).

My previous example wasn't representing a practical usage of this kind of feature. I'll just put here another example that hopefully emphasizes the idea.

import inject
from db_package import DatabaseClient

async def connect_to_db():
    db_client = DatabaseClient('localhost', 4444)

    await db_client.connect()

    return db_client

def config(binder):
    binder.bind_async('DB_CONNECTION', connect_to_db)

    # you can even support running the coroutine on a specific event loop
    loop = asyncio.get_event_loop()
    binder.bind_async('DB_CONNECTION', connect_to_db, loop=loop)

inject.configure(config)

# dependency injection
class UsersService:
    @inject.params(db_connection='DB_CONNECTION')
    def __init__(self, db_connection):
        self.db_connection = db_connection

    def get_users(self):
        self.db_connection.get('SELECT * FROM users')

Thank you again :)

ivankorobkov commented 4 years ago

Sorry, but your example is conceptually right, but engineeringly wrong. One should not inject a db connection but instead use a db pool of connections. So it means that one should use async not to construct an application graph, but to acquire async resources.

I'll close the issue. If you want, you can always fork and modify the lib.

All in all, thanks for the idea.