reagento / dishka

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

defer type annotation analysis #234

Open RonnyPfannschmidt opened 2 months ago

RonnyPfannschmidt commented 2 months ago

im currently migrating a codebase with quite some import cycles to dishka to resolve this,

unfortunately i frequently see errors like

  File "$MYLIB/base/_dependency_inject.py", line 118, in ApplicationProvider
    @provide
     ^^^^^^^
  File "$SITE/dishka/dependency_source/make_factory.py", line 548, in provide
    return _provide(
           ^^^^^^^^^
  File "$SITE/dishka/dependency_source/make_factory.py", line 475, in _provide
    factory = make_factory(
              ^^^^^^^^^^^^^
  File "$SITE/dishka/dependency_source/make_factory.py", line 433, in make_factory
    return _make_factory_by_function(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "$SITE/dishka/dependency_source/make_factory.py", line 272, in _make_factory_by_function
    hints = get_type_hints(source, include_extras=True)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.11/typing.py", line 2414, in get_type_hints
    hints[name] = _eval_type(value, globalns, localns)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.11/typing.py", line 395, in _eval_type
    return t._evaluate(globalns, localns, recursive_guard)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.11/typing.py", line 905, in _evaluate
    eval(self.__forward_code__, globalns, localns),
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<string>", line 1, in <module>
AttributeError: partially initialized module '$MYLIB.base.application' has no attribute 'Application' (most likely due to a circular import)
RonnyPfannschmidt commented 2 months ago

note this is not a pressing issue, i found a way to resolve this by moving a provider class to a especially ugly place and lazy importing it when making the root container

Tishka17 commented 2 months ago

You can try registering classes in provider instance instead of class-based approach with decorators

RonnyPfannschmidt commented 2 months ago

the providers are actual type-annotated functions, so i'd like to avoid pulling things apart even more if there was way to wait with the type annotation parsing until its instantiation time for the provider, then it would work in the initial case (im using deferred type annotations as the import is not yet available, the decorator going after them at definition time is kind of beating that

Tishka17 commented 2 months ago

I mean instead of this:

class MyProvider(Provider):
   @provide
   def create_x(self, dep: Y) -> X: ...  # immediate resolving

You can do this:

def create_x(dep: "Y") -> "X": ... 
..

from x import X
from y import Y
provider = Provider()
provider.provide(create_x)

But from my side it looks like a problem with your code structure. Try splitting providers into more parts. Just in case something is missed: your providers do not need to know about each other and who produces what, they are wired only inside container.

RonnyPfannschmidt commented 2 months ago

If evaluating the type annotations was deferred until wireing time I wouldn't see any issues

I'm in the process of resolving historical grown recursive dependencies,

I'm happy to provide an implementation that retains eagerness but makes wire time available but import time unavailable types a warning instead of a error as it's not always easy to quick detangle