reagento / dishka

Cute DI framework with agreeable API and everything you need
https://dishka.readthedocs.io
Apache License 2.0
381 stars 38 forks source link

Pytest integration #124

Open Tishka17 opened 5 months ago

Tishka17 commented 5 months ago

Allow using dishka container to provide fixtures

RonnyPfannschmidt commented 2 months ago

im one of the pytest maintainers, im very curious about enabling this as i want to start using dishka more in a number of projects that have some needs and we will need pytest fixtures

we are plying with ideas on how to repressent pytest fixtures in a more interoperable manner and would love to get input on both - making pytest fixutres more easy to integrate as well as easing other di tools to cooperate with pytest nicely

a key pain point is probably that pytest currently works name based instead of type based

Tishka17 commented 2 months ago

I though about several approaches here:

  1. Generate fixures manually. So the code will be like

    x = dishka_fixture("x", Class)

    which will be transformed to

    def x(dishka_container):
    return dishka_container.get(Class)
  2. Decorate tests and fixtures using @inject decorator as in all integrations. So it will erase all FromDishka arguments and add dishka_container

@inject
def test(a, x: FromDishka[Class]):
   ...

will be transofmed to something similar to

def test(a, dishka_container):
   _real_test(a, dishka_container.get(Class))
  1. Automatically wrap all tests and fixtures like in 2. I tried to achieve it here: https://github.com/reagento/dishka/commit/dc8c8a099dd1079a248205c6e383aabc606723dd but it works only with tests, not fixtures

Another task here is pytest_asyncio integration

Tishka17 commented 2 months ago

As a PoC

def dishka_fixture(cls: Any):
    def temp_fixture(dishka_container):
        return dishka_container.get(cls)

    return pytest.fixture(temp_fixture)

@pytest.fixture
def dishka_container():
    p = Provider(scope=Scope.APP)
    p.provide(lambda: 12, provides=int)
    return make_container(p)

someint = dishka_fixture(int)

def test_x(someint):
    assert someint == 12
RonnyPfannschmidt commented 2 months ago

It's critical to set the name, else thing's will turn bleak

Tishka17 commented 2 months ago

@RonnyPfannschmidt is it? I did only small test and that code looks working. I would prefer not to set name twice for same fixture

RonnyPfannschmidt commented 2 months ago

I may be missremember the exact factory parsing behavior

I'll validate later

Tishka17 commented 2 months ago

@RonnyPfannschmidt is there any hook to modify any fixture/test?

RonnyPfannschmidt commented 2 months ago

There's no hook to modify dependency injection yet

Same goes for discovery of fixtures

I'd like a discussion on enabling discovery of more fixtures and matching them to parameters

Tishka17 commented 2 months ago

@RonnyPfannschmidt a) we can patch fixtures and tests so they will call container by themselves. It doesn't require modification of pytest DI engine. But it requires some kind middlewares/hooks which will be applied to any created test/fixture. I showed prototype for wrapping tests here https://github.com/reagento/dishka/commit/dc8c8a099dd1079a248205c6e383aabc606723dd

b) probably pytest can be modified to lookup fixtures not only by name, but by types. In this case we need some fixture factory protocol in pytest implemented by pytest plugin (dishka). This looks much more complicated for me. We need to keep in mind here, that there will be multiple containers in different packages/modules

RonnyPfannschmidt commented 2 months ago

not to mention that pytest has own fixture scopes

instead of patching tests and fixtures, i'd like to have a way to be a intermediate between the container and the pytest system (so the normal fill fixture mechanisms can be used)

my current impression is that in the case of pytest, a certain focus on controlling the call signatures from the test framework is necessary

Tishka17 commented 2 months ago

Regarding scopes my point is that main logic of scopes is related to application running inside test, so here we do not need anything. If user needs some logic on synchronizing container scope (e.g, Scope.RUNTIME=="session" and Scope.APP=="funcion") it can be implemented in his dishka_container fixture

On topic of container managements I am interested in implementing integration which will be available on current versions of pytest, but it will be manual (using @inject or dishka_fixture as shown above). But for autoinjected I definitely need changes in pytest logic.

Do you have any issues/plans for this in pytest repo?

RonnyPfannschmidt commented 2 months ago

No plans or issues yet, the pre planning stage is literally starting now (in part because dishka is nicely inspiring)

Tishka17 commented 2 months ago

Another open question here is pytest-asyncio support. We have 2 versions of container now: sync and async. I guess, user might want to use async container to get fixtures for non-async test. And vice versa.

RonnyPfannschmidt commented 2 months ago

thats one of the reasons why the full-fillment of the injection needs to be able to be deferred to some kind of controller - in particualr since pytest is absolutlely unable to re-color test nodes in any sensible amount of time

RonnyPfannschmidt commented 1 month ago

https://github.com/pytest-dev/pytest/pull/12473 is working on replacing fixtures with a more specific object

i believe that can be a starting point for more