diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc8c906..078d9d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,15 +6,14 @@ on: types: - created -permissions: - contents: read - jobs: test: runs-on: ubuntu-latest + permissions: + contents: read strategy: matrix: - python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13', '3.14' ] + python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14' ] steps: - uses: actions/checkout@v6 @@ -38,9 +37,11 @@ jobs: lint: runs-on: ubuntu-latest + permissions: + contents: read strategy: matrix: - python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13', '3.14' ] + python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14' ] steps: - uses: actions/checkout@v6 @@ -66,6 +67,8 @@ jobs: needs: [ test, lint ] runs-on: ubuntu-latest if: ${{ github.event_name == 'release' }} + permissions: + contents: read steps: - uses: actions/checkout@v6 diff --git a/CHANGELOG.md b/CHANGELOG.md index 473f3c5..d73a4b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ Opyoid follows [semver guidelines](https://semver.org) for versioning. ## Unreleased +## 3.0.4 +### Fixes +- Fix dependency loops not always raising a `CyclicDependencyError` and crashing with a `maximum recursion depth exceeded` + ## 3.0.3 ### Fixes - Fixed a crash when using Instance Bindings with objects without an equality operator, such as pandas DataFrames or diff --git a/opyoid/bindings/self_binding/callable_to_provider_adapter.py b/opyoid/bindings/self_binding/callable_to_provider_adapter.py index 1950702..cd8188c 100644 --- a/opyoid/bindings/self_binding/callable_to_provider_adapter.py +++ b/opyoid/bindings/self_binding/callable_to_provider_adapter.py @@ -133,9 +133,12 @@ def _get_provider( targets: List[Target[InjectedT]], parent_context: InjectionContext[Any] ) -> Optional[Provider[InjectedT]]: for target_index, target in enumerate(targets): - context = parent_context.get_child_context(target, allow_jit_provider=target_index == len(targets) - 1) - context.current_class = parent_context.current_class - context.current_parameter = parent_context.current_parameter + context = parent_context.get_child_context( + target, + allow_jit_provider=target_index == len(targets) - 1, + current_class=parent_context.current_class, + current_parameter=parent_context.current_parameter, + ) try: return context.get_provider() except NoBindingFound: diff --git a/opyoid/injection_context.py b/opyoid/injection_context.py index 739abb5..f9c178c 100644 --- a/opyoid/injection_context.py +++ b/opyoid/injection_context.py @@ -10,7 +10,7 @@ from .utils import InjectedT if TYPE_CHECKING: - from .bindings import Binding, RegisteredBinding + from .bindings import RegisteredBinding from .injection_state import InjectionState @@ -47,9 +47,16 @@ def _dependency_chain(self) -> List[Target[Any]]: return chain def get_child_context( - self, new_target: Target[InjectedSubT], allow_jit_provider: bool = True + self, + new_target: Target[InjectedSubT], + *, + allow_jit_provider: bool = True, + current_class: Optional[Type[InjectedSubT]] = None, + current_parameter: Optional[Parameter] = None, ) -> "InjectionContext[InjectedSubT]": - return InjectionContext(new_target, self.injection_state, self, allow_jit_provider) + return InjectionContext( + new_target, self.injection_state, self, allow_jit_provider, current_class, current_parameter + ) def get_new_state_context(self, new_state: "InjectionState") -> "InjectionContext[InjectedT]": return InjectionContext(self.target, new_state, self.parent_context, self.allow_jit_provider) diff --git a/opyoid/providers/providers_factories/union_provider_factory.py b/opyoid/providers/providers_factories/union_provider_factory.py index 681be16..5d22e62 100644 --- a/opyoid/providers/providers_factories/union_provider_factory.py +++ b/opyoid/providers/providers_factories/union_provider_factory.py @@ -18,9 +18,9 @@ def create(self, context: InjectionContext[InjectedT]) -> Provider[InjectedT]: for subtype in cast(Union[InjectedT], context.target.type).__args__: try: new_target: Target[InjectedT] = Target(subtype, context.target.named) - new_context = context.get_child_context(new_target) - new_context.current_class = context.current_class - new_context.current_parameter = context.current_parameter + new_context = context.get_child_context( + new_target, current_class=context.current_class, current_parameter=context.current_parameter + ) return new_context.get_provider() except NoBindingFound: pass diff --git a/tests_e2e/test_injection.py b/tests_e2e/test_injection.py index f155739..120ad45 100644 --- a/tests_e2e/test_injection.py +++ b/tests_e2e/test_injection.py @@ -1189,6 +1189,29 @@ def __init__(self, arg: MyOtherClass): with self.assertRaises(CyclicDependencyError): Injector(bindings=[ClassBinding(MyClass, MyImpl), SelfBinding(MyOtherClass)]) + def test_list_circular_injection_raises_error(self): + class ParentClass: + def __init__(self, children: List[MyClass]): + self.children = children + + class SubClass2(MyClass): + def __init__(self, parent: ParentClass): + self.parent = parent + + class SubClass1(MyClass): + pass + + class MyModule(Module): + def configure(self): + self.bind(ParentClass) + self.multi_bind( + MyClass, + [self.bind_item(to_class=SubClass1), self.bind_item(to_class=SubClass2)], + ) + + with self.assertRaises(CyclicDependencyError): + Injector([MyModule]) + def test_cyclic_dependencies_with_private_module_are_handled(self): class MyOtherClass: def __init__(self, arg: MyClass):