Description
The type annotation for Binder.bind
is as follows:
def bind(
self,
interface: Type[T],
to: Union[None, T, Callable[..., T], Provider[T]] = None,
scope: Union[None, Type['Scope'], 'ScopeDecorator'] = None,
) -> None:
...
The following test case however proves, that Callable
s are also a valid dependency:
def test_injector_can_inject_a_protocol():
class Func(Protocol):
def __call__(self, a: int) -> int:
...
def func_matching_protocol(a: int) -> int:
return a + 1
def configure(binder: Binder):
binder.bind(Func, to=InstanceProvider(func_matching_protocol))
inj = Injector([configure])
func = inj.get(Func)
assert func(1) == 2
[ The Protocol
doesn't really have a purpose here other than to show that it's also allowed ]
➡️ Two situations:
- This is a bug. You should not be able to declare callables or coroutines as dependencies (I have no idea why that should be true, it's very useful).
- It's an intended feature. Then the type annotation should not say
interface: Type[T]
, but rather something else that also includesCallable
s andCoroutine
s, and probably also a few other cases.
Continuing 2., Pylance (the new VSCode type checker based on pyright) reports the Func
protocol being unassignable to a generic Type
:
Consequently, inj.get(Func)
and func(1)
are also being inferred as T
, which is a "class instance-like type", not sure what the right term is.
A similar issue arises with @inject
, thought there it's more subtle - As far as I understand, the InitializerOrClass
helper should be a ClassVar
instead of a TypeVar
..? I'd have to dig deeper.
Aaand last but not least another related, but different question:
It is abundantly clear that support for @inject
on callables has been removed, and i guess you must have had good reasons for that. Assuming I want to shoot myself in the foot, is there a simple workaround to get this feature back with the existing code base? Is it as simple as injector.call_with_injection(...)
or is there more to it, and things that might get removed aswell?
I'm thinking that for more performance-critical things that get called a ton, I'd rather not succumb to the DI framework's iron chains and create per-request scoped classes where a bunch of functions would totally suffice.
For context, here is what I'm trying to do:
class Middleware(Protocol[TContext]):
"""
A protocol type is easier to document and has slightly better tooling support than declaring `Callable`s inline.
It can also host an `@abstractmethod` decorator.
"""
@abstractmethod
def __call__(
self, context: TContext, call_next: NextDelegate[TContext]
) -> Union[Any, Coroutine[Any, Any, Any]]:
...
Library users should be able to bind their own Middleware
functions using injector, but also be able to receive additional request-scoped instances. These get then chained together like this:
This is going to be a realtime chatbot, so performance matters - and I've seen how nicely FastAPI integrated dependencies nested arbitrarily deep into functions, which is what I'm aiming for. But without the clutter that this arg: Type = Depends(XYZ)
syntx brings with it, simply every parameter should be coming from DI or be annotated as NoInject
, that pattern is nicer.