pythondependency-injectionpython-typing

Python injector doesn't work for NewType based on tuple[str, str]


I'm trying to use injector to inject a tuple[str, str], but it doesn't work.

import injector
from typing import NewType

Foo = NewType("Foo", tuple[str, str])


class MyModule(injector.Module):
    def configure(self, binder: injector.Binder) -> None:
        binder.bind(Foo, Foo(("x", "y")))


injector.Injector(modules=[MyModule]).get(Foo)

leads to error

Traceback (most recent call last):
  File "/tmp/injector_demo/scratch.py", line 12, in <module>
    injector.Injector(modules=[MyModule]).get(Foo)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/injector_demo/.venv/lib/python3.12/site-packages/injector/__init__.py", line 925, in __init__
    self.binder.install(module)
  File "/tmp/injector_demo/.venv/lib/python3.12/site-packages/injector/__init__.py", line 564, in install
    instance(self)
  File "/tmp/injector_demo/.venv/lib/python3.12/site-packages/injector/__init__.py", line 871, in __call__
    self.configure(binder)
  File "/tmp/injector_demo/scratch.py", line 9, in configure
    binder.bind(Foo, Foo(("x", "y")))
  File "/tmp/injector_demo/.venv/lib/python3.12/site-packages/injector/__init__.py", line 465, in bind
    self._bindings[interface] = self.create_binding(interface, to, scope)
                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/injector_demo/.venv/lib/python3.12/site-packages/injector/__init__.py", line 569, in create_binding
    provider = self.provider_for(interface, to)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/injector_demo/.venv/lib/python3.12/site-packages/injector/__init__.py", line 631, in provider_for
    raise UnknownProvider('couldn\'t determine provider for %r to %r' % (interface, to))
injector.UnknownProvider: couldn't determine provider for __main__.Foo to ('x', 'y')

I've been injecting NewType('Bar', str) many times and they all work, so it has to be due to something special related to tuple[str, str]. I wonder if anyone has insights into why.


Solution

  • NewType is a red herring. The real problem is that at runtime, parameterized generics like tuple[str, str] aren't actually types but instances of types.GenericAlias. They can't even be the second argument to an isinstance call.

    >>> type(tuple[str, str])
    <class 'types.GenericAlias'>
    >>> isinstance(tuple[str, str], type)
    False
    >>> isinstance(tuple, type)
    True
    >>> isinstance(("foo", "bar"), tuple[str, str])
    Traceback (most recent call last):
      File "<python-input-4>", line 1, in <module>
        isinstance(("foo", "bar"), tuple[str, str])
        ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    TypeError: isinstance() argument 2 cannot be a parameterized generic
    

    Put differently, you can never have an instance of tuple[str, str], so it doesn't make much sense to try to inject one.

    You can get around this with an if TYPE_CHECKING guard. That will make the injector work properly at runtime but holds up under static analysis.

    import injector
    from typing import NewType, TYPE_CHECKING, reveal_type
    
    if TYPE_CHECKING:
        Foo = NewType("Foo", tuple[str, str])
    else:
        Foo = NewType("Foo", tuple)
    
    class MyModule(injector.Module):
        def configure(self, binder: injector.Binder) -> None:
            binder.bind(Foo, Foo(("x", "y")))
    
    reveal_type(injector.Injector(modules=[MyModule]).get(Foo)) # tuple at runtime, Foo or tuple[str, str] while checking