python-injector / injector

Python dependency injection framework, inspired by Guice
BSD 3-Clause "New" or "Revised" License
1.3k stars 81 forks source link

Pytest integration #114

Open sonthonaxrk opened 5 years ago

sonthonaxrk commented 5 years ago

How would one use this library with pytest without actually doing

def test_something(injector):
    dependency = injector.get(Class)

It'd be nice if I could use the injector with pytest.

@inject
def test_something(dependency : Class):
    pass

(I get that using a type annotation is probably not possible)

Does anyone have any nice patterns for this that don't break the pytest paradigm.

jstasiak commented 5 years ago

To the degree I'm familiar with pytest fixtures (which is not much, I avoid them) I don't think I have a solution, maybe someone else reading this will be able to help.

davidparsson commented 5 years ago

I've used pytest quite a bit, and I don't think this is currently possible. Perhaps it would be possible to solve it with a plugin somehow. It would get even more complicated if the Injector instance would need to have test-specific configuration, which in my experience is likely.

I've usually resorted to a fixture for injector and sometimes other fixtures that gets instances of classes from that injector instance.

proofit404 commented 5 years ago

Hi,

I'm the author of an alternative dependency injector library (dependencies part of the dry-python project).

We already have integration with pytest suite.

We decide not to fight with pytest itself but provide a way to register fixture with injector outside of the pytest run function.

If anyone is interested to backport this to this project, I invite you to take a look at our implementation.

Best regards, Artem.

randomstuff commented 4 years ago

What about this solution?

def with_injection(f):
    f = inject(f)
    def wrapper():
        injector = Injector([BaseModule(), StubModule()])
        return injector.call_with_injection(f)
    return wrapper

@with_injection
def test_foo(session: Session):
   # session is injected!
   ...
jstasiak commented 4 years ago

Yeah, this is a good way to go about doing this.

Edit: As long as one's fine with separate Injector (with new scopes) per test.

randomstuff commented 4 years ago

Edit: As long as one's fine with separate Injector (with new scopes) per test.

Well yes, if that's not good for you, you can:

injector = Injector([BaseModule(), StubModule()])

def with_injection(f):
    f = inject(f)
    def wrapper():
        return injector.call_with_injection(f)
    return wrapper

@with_injection
def test_foo(session: Session):
   # session is injected!
   ...

or maybe:

def with_injector(injector):
    def decorator(f):
        f = inject(f)
        def wrapper():
            return injector.call_with_injection(f)
        return wrapper
    return decorator

injector = Injector([BaseModule(), StubModule()])

@with_injector(injector)
def test_foo(session: Session):
   # session is injected!
   ...

# alternative:

with_injection = with_injector(injector)

@with_injection
def test_foo(session: Session):
   # session is injected!
   ...
jstasiak commented 4 years ago

Yes, absolutely.

mofr commented 1 year ago

One more option which allows mixing pytest-injected and Injector-injected arguments.

def with_injection(f):
    import functools
    from injector import inject
    import inspect

    f = inject(f)

    @functools.wraps(f)
    def wrapper(**kwargs):  # This function is inspected and called by pytest
        return injector.call_with_injection(f, kwargs=kwargs)

    # Remove parameters which will be provided by injector, so that pytest is not confused
    sig = inspect.signature(wrapper)
    not_injected_parameters = tuple(param for param in sig.parameters.values() if param.name not in f.__bindings__)
    wrapper.__signature__ = sig.replace(parameters=not_injected_parameters)

    return wrapper