python-injector / python-injector/injector

[Question] Circular imports and "deferred"

Open
#170 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
1.5k
Forks
94
PR merge metrics
No merged PRs in 30d

Description

I have an issue with circular imports that I am having trouble resolving.

Here is a simple repro case which is similar to what I am doing:

$ tree
.
└── circular
    ├── a.py
    ├── b.py
    ├── c.py
    └── main.py
1 directory, 4 files

$ cat circular/a.py 
# from circular import b
import circular.b as b

class A:
    def __init__(self):
        print(f'A.__init__ {id(self)=}')

$ cat circular/b.py 
from injector import inject, Injector

import circular.c as c


class B:
    @inject
    def __init__(self, injector: Injector):
        print(f'B.__init__ {id(self)=} {id(injector)=}')

$ cat circular/c.py 
from injector import inject

import circular.b as b


class C:
    @inject
    def __init__(self, b: 'b.B'):
        print(f'C.__init__ {id(self)=} {id(b)=}')

$ cat circular/main.py
# add to path to make this runnable from anywhere
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))

from injector import Injector

import circular.b as b


def main():
    injector = Injector()
    b_inst = injector.get(b.B)


if __name__ == '__main__':
    main()

When I run this, I get an AttributeError related to circular imports caused by a call to get_type_hints. I hoped this would defer, due to the use of a forward reference in c.py and module imports, but maybe I am misunderstanding?

Traceback (most recent call last):
  File "inj\circular\main.py", line 8, in <module>
    import circular.b as b
  File "inj\circular\b.py", line 3, in <module>
    import circular.c as c
  File "inj\circular\c.py", line 6, in <module>
    class C:
  File "inj\circular\c.py", line 8, in C
    def __init__(self, b: 'b.B'):
  File "[venv]\lib\site-packages\injector\__init__.py", line 1367, in inject
    bindings = _infer_injected_bindings(function, only_explicit_bindings=False)
  File "[venv]\lib\site-packages\injector\__init__.py", line 1176, in _infer_injected_bindings
    bindings = get_type_hints(callable, include_extras=True)
  File "[venv]\lib\site-packages\typing_extensions.py", line 1880, in get_type_hints
    hint = typing.get_type_hints(obj, globalns=globalns, localns=localns)
  File "[python]\Python38\lib\typing.py", line 1261, in get_type_hints
    value = _eval_type(value, globalns, localns)
  File "[python]\Python38\lib\typing.py", line 270, in _eval_type
    return t._evaluate(globalns, localns)
  File "[python]\Python38\lib\typing.py", line 518, in _evaluate
    eval(self.__forward_code__, globalns, localns),
  File "<string>", line 1, in <module>
AttributeError: partially initialized module 'circular.b' has no attribute 'B' (most likely due to a circular import)

Since I don't have a full understanding of Injector internals, I tried a longshot of adding AttributeError to the try/except (in addition to NameError that's already caught) in _infer_injected_bindings at line 1164. This actually works for both the simplified repro and my actual application. However, I don't know what other implications this change might have and I wanted to seek you advice.

Thanks!

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the circular import using circular/a.py, b.py, c.py, and main.py, then inspect injector/init.py around line 1164 and _infer_injected_bindings. Start with the get_type_hints call and its existing exception handling. Done means determining whether the reported AttributeError should be handled and documenting or testing the resulting behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.