python-dependency-injector icon indicating copy to clipboard operation
python-dependency-injector copied to clipboard

Confusing behavior of Singletons with pytest scopes

Open chbndrhnns opened this issue 3 years ago • 0 comments

I looked at a related issue but found that the solution there was not working well for me. What I understand is that singletons need to be reset.

Here is my code which has a root container with a services and controllers sub-container.

The expected behavior is like this:

  • container fixture in function scope -> no problem
  • container fixture in module scope with reset_singletons fixture in auto-use -> no problems

The strange behaviour is as follows:

  • container fixture in module scope with reset_services fixture in auto-use -> 5 failing tests
       5 failed
         - tests/test_override.py:76 TestReset.test_can_reset
         - tests/test_override.py:79 TestReset.test_can_change_result
         - tests/test_override.py:99 TestOverride.test_can_override
         - tests/test_override.py:102 TestOverride.test_can_change_result
         - tests/test_override.py:106 TestOverride.test_can_override_within

Can you help me understand what's going on here and if it is a bug? Resetting only parts of the singletons is my preferred way of doing it as I do not want to reset all singletons in my (bigger) app but only these.

Click to toggle
import pytest
from dependency_injector import containers, providers


class MyService:
    def __init__(self):
        self._result = "real"

    def run(self):
        return self._result


class MyController:
    def __init__(self, service):
        self._service = service

    def run(self):
        return self._service.run()


class Services(containers.DeclarativeContainer):
    my_service = providers.Singleton(MyService)


class Controllers(containers.DeclarativeContainer):
    services: Services = providers.DependenciesContainer()
    my_controller = providers.ContextLocalSingleton(
        MyController, service=services.my_service
    )


class MainContainer(containers.DeclarativeContainer):
    services = providers.Container(Services)
    controllers = providers.Container(Controllers, services=services)


@pytest.fixture(autouse=True)
def reset_singletons(container):
    with container.reset_singletons():
        yield


@pytest.fixture(autouse=True)
def reset_services(container):
    with container.services.reset_singletons():
        yield


@pytest.fixture
def container():
    return MainContainer()


@pytest.fixture
def service():
    ...


@pytest.fixture
def controller(container, service):
    return container.controllers.my_controller()


def test_can_run(controller):
    assert controller.run() == "real"


class TestReset:
    @pytest.fixture
    def service(self, container):
        with container.services.my_service.reset() as s:
            instance = s()
            instance._result = "reset"
            yield instance

    def test_can_reset(self, controller):
        assert controller.run() == "reset"

    def test_can_change_result(self, service, controller):
        service._result = "changed"
        assert controller.run() == "changed"

    def test_can_reset_within(self, container):
        with container.services.my_service.reset() as s:
            s._result = "reset"
            assert container.controllers.my_controller().run() == "reset"


class TestOverride:
    @pytest.fixture
    def service(self, container):
        override = MyService()
        override._result = "override"

        container.services.my_service.reset()
        with container.services.my_service.override(override):
            yield override

    def test_can_override(self, controller):
        assert controller.run() == "override"

    def test_can_change_result(self, service, controller):
        service._result = "override-changed"
        assert controller.run() == "override-changed"

    def test_can_override_within(self, container):
        service = MyService()
        service._result = "override"

        with container.services.my_service.override(service):
            controller = container.controllers.my_controller()

        assert controller.run() == "override"

chbndrhnns avatar Dec 13 '21 10:12 chbndrhnns