ets-labs / python-dependency-injector

Dependency injection framework for Python
https://python-dependency-injector.ets-labs.org/
BSD 3-Clause "New" or "Revised" License
3.96k stars 305 forks source link

Resource provider to support context manager interface (__enter__/__exit__ and __aenter__/__aexit__) #444

Open ecs-jnguyen opened 3 years ago

ecs-jnguyen commented 3 years ago

I noticed that using provider.Resource() with container.init_resources() and container.shutdown_resources() does not call the __enter__ and/or __exit__ methods for a class or the @contextlib.contextmanager. For initializing and shutting down resources I had to make a wrapper method without @contextlib.contextmanager in order to call the __enter__ and __exit__ methods. Is this intended?

@contextlib.contextmanager
def this_does_not_work_by_it_self():
    print('entering this_does_not_work')
    yield None
    print('exiting this_does_not_work')

def this_works():
    print('entering this_works')
    with this_does_not_work_by_it_self() as x:
        yield x
    print('exiting this_works')

class SampleContainer(containers.DeclarativeContainer):

    # uncomment to see behavior
    # one = providers.Resource(this_does_not_work_by_it_self)

    """
    Output:
    main start init resources
    main done init resources
    main start shutdown resources
    main done shutdown resources
    """

    # uncomment to see behavior
    # two = providers.Resource(this_works)

    """
    Output:
    main start init resources
    entering this_works
    entering this_does_not_work
    main done init resources
    main start shutdown resources
    exiting this_does_not_work
    exiting this_works
    main done shutdown resources
    """

def main():
    container = SampleContainer()
    print('main start init resources')
    container.init_resources()
    print('main done init resources')
    print('main start shutdown resources')
    container.shutdown_resources()
    print('main done shutdown resources')

if __name__ == '__main__':
    main()
rmk135 commented 3 years ago

Hi @ecs-jnguyen ,

Yes, Resource provider supports generators but not support context managers: https://python-dependency-injector.ets-labs.org/providers/resource.html#generator-initializer

Generator-based initializer is a 2 phase generator. First phase is responsible for resource initialization and should yield resource object. Second phase is responsible for resource shutdown.

Do you look for the support of context managers?

ecs-jnguyen commented 3 years ago

@rmk135

ah i see the difference is between generators and context managers.

From my understanding the Resource provider is used to initialize the resource with the init_resources() method and clean up resources with the shutdown_resources() method.

Generators can return multiple values i.e. have multiple yield statements in a method. Would it be better for the Resource provider to make use of the with keyword i.e. things with __enter__ and __exit__ instead?

rmk135 commented 3 years ago

Generators can return multiple values i.e. have multiple yield statements in a method.

That's true. Generators can return multiple values or be infinite. Dependency Injector uses 2-phase generators as I described before:

This design solution mimics @contextlib.contextmanager decorator and pytest yield fixtures and I don't see a problem with it.

Also this is a part of API and I could not remove it cause it will break other's people code.


I could add context manager interface support to the Resource provider. In that case it will support all objects with __enter__ and __exit__, @contextlib.contextmanager decorated generators, async context managers etc.


What's a practical use case how you faced this issue?

ecs-jnguyen commented 3 years ago

@rmk135

True you would be breaking existing code if you change the behavior. It would be best not to do that.

I'm trying to see how to use the Resource provider properly with a context manager. If I wanted to use a class that has an __enter__ and __exit__ method, would I have to create a wrapper method like below?

class DataBaseConnector:
    def __init__():
        # initialize db connection
        self.connection = None

    def __enter__():
        return self

    def __exit__():
        # clean up resources
        self.connection.close()

def db_connector_wrapper():
    db_connector = DataBaseConnector()
    yield db_connector
    db_connector.__exit__()

def db_connector_wrapper_alternative():
    with DataBaseConnector() as db_connector:
        yield db_connector
rmk135 commented 3 years ago

Yeah, Resource provider doesn't support context manager interface so you need a wrapper.

I'll add a backlog item to implement context manager interface support.

As a temp alternative you might want to take a look at this subclassing solution:

from dependency_injector import resources

class MyResource(resources.Resource):

    def init(self, argument1=..., argument2=...) -> SomeResource:
        return SomeResource()

    def shutdown(self, resource: SomeResource) -> None:
        # shutdown
        ...

class Container(containers.DeclarativeContainer):

    resource = providers.Resource(
        MyResource,
        argument1=...,
        argument2=...,
    )

Docs: https://python-dependency-injector.ets-labs.org/providers/resource.html#subclass-initializer

jalvespinto commented 1 year ago

Was this feature added?

lagunovartur commented 2 months ago

4.41 asynccontextmanager donw work