diff --git a/CHANGELOG.md b/CHANGELOG.md index bb52a99..8b37387 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ Opyoid follows [semver guidelines](https://semver.org) for versioning. ## Unreleased +## 1.0.1 +### Fixes +- Using auto bindings does not ignore existing bindings anymore (bug introduced in `0.10.0`) +- The error message in cyclic dependency errors is now displaying the dependencies in a more logical order + (from the parent class to the dependencies) + + ## 1.0.0 ### Breaking changes - Renamed `bound_type` argument in `ClassBinding` and `ItemBinding` constructors into `bound_class` @@ -22,6 +29,7 @@ Opyoid follows [semver guidelines](https://semver.org) for versioning. ### Features - Opyoid is now considered stable + ## 0.10.3 ### Fixes - Improved error message when missing a named binding, the underlying type and name is used diff --git a/opyoid/bindings/self_binding/self_binding_to_provider_adapter.py b/opyoid/bindings/self_binding/self_binding_to_provider_adapter.py index cd4a68c..6067329 100644 --- a/opyoid/bindings/self_binding/self_binding_to_provider_adapter.py +++ b/opyoid/bindings/self_binding/self_binding_to_provider_adapter.py @@ -107,8 +107,8 @@ def _get_positional_parameter_provider(self, @staticmethod def _get_provider(targets: List[Target[InjectedT]], parent_context: InjectionContext) -> Optional[Provider[InjectedT]]: - for target in targets: - context = parent_context.get_child_context(target) + for target_index, target in enumerate(targets): + context = parent_context.get_child_context(target, allow_jit_provider=target_index == len(targets) - 1) try: return context.get_provider() except NoBindingFound: diff --git a/opyoid/injection_context.py b/opyoid/injection_context.py index 1c6ec2f..de45f42 100644 --- a/opyoid/injection_context.py +++ b/opyoid/injection_context.py @@ -20,6 +20,7 @@ class InjectionContext(Generic[InjectedT]): target: Target[InjectedT] injection_state: "InjectionState" parent_context: Optional["InjectionContext"] = attr.ib(default=None, eq=False) + allow_jit_provider: bool = True def __attrs_post_init__(self): context = self @@ -27,7 +28,7 @@ def __attrs_post_init__(self): if self == context.parent_context: dependency_chain = "\n".join( f"-> {target!r}" - for target in self._dependency_chain + for target in reversed(self._dependency_chain) ) self.logger.error(f"Cyclic dependency detected, injection graph: \n{dependency_chain}") raise CyclicDependencyError(f"Cyclic dependency detected, injection graph: \n{dependency_chain}") @@ -42,11 +43,13 @@ def _dependency_chain(self) -> List[Target]: chain.append(context.target) return chain - def get_child_context(self, new_target: Target[InjectedT]) -> "InjectionContext[InjectedT]": - return InjectionContext(new_target, self.injection_state, self) + def get_child_context(self, + new_target: Target[InjectedT], + allow_jit_provider: bool = True) -> "InjectionContext[InjectedT]": + return InjectionContext(new_target, self.injection_state, self, allow_jit_provider) def get_new_state_context(self, new_state: "InjectionState") -> "InjectionContext[InjectedT]": - return InjectionContext(self.target, new_state, self.parent_context) + return InjectionContext(self.target, new_state, self.parent_context, self.allow_jit_provider) def get_provider(self) -> Provider[InjectedT]: return self.injection_state.provider_creator.get_provider(self) diff --git a/opyoid/providers/providers_factories/jit_provider_factory.py b/opyoid/providers/providers_factories/jit_provider_factory.py index 6b5b90c..ed73ce7 100644 --- a/opyoid/providers/providers_factories/jit_provider_factory.py +++ b/opyoid/providers/providers_factories/jit_provider_factory.py @@ -12,6 +12,7 @@ def __init__(self): def accept(self, context: InjectionContext[InjectedT]) -> bool: return context.injection_state.options.auto_bindings \ and context.target.default is EMPTY \ + and context.allow_jit_provider \ and not isinstance(context.target.type, str) def create(self, context: InjectionContext[InjectedT]) -> Provider[InjectedT]: diff --git a/tests/test_bindings/test_self_binding/test_self_binding_to_provider_adapter.py b/tests/test_bindings/test_self_binding/test_self_binding_to_provider_adapter.py index eaf9e40..c850fd8 100644 --- a/tests/test_bindings/test_self_binding/test_self_binding_to_provider_adapter.py +++ b/tests/test_bindings/test_self_binding/test_self_binding_to_provider_adapter.py @@ -113,11 +113,11 @@ def __init__(self, arg_1: str, arg_2: int, *args: float, arg_3: bool, **kwargs): self.assertEqual(["my_arg_1", 2, 1.2, 3.4], instance.args) self.assertEqual({"arg_3": True}, instance.kwargs) self.assertEqual([ - call(self.context.get_child_context(Target(str, "arg_1"))), - call(self.context.get_child_context(Target(int, "arg_2"))), - call(self.context.get_child_context(Target(List[float], "args"))), - call(self.context.get_child_context(Target(bool, "arg_3"))), - call(self.context.get_child_context(Target(SingletonScope))), + call(self.context.get_child_context(Target(str, "arg_1"), allow_jit_provider=False)), + call(self.context.get_child_context(Target(int, "arg_2"), allow_jit_provider=False)), + call(self.context.get_child_context(Target(List[float], "args"), allow_jit_provider=False)), + call(self.context.get_child_context(Target(bool, "arg_3"), allow_jit_provider=False)), + call(self.context.get_child_context(Target(SingletonScope), allow_jit_provider=True)), ], self.state.provider_creator.get_provider.call_args_list) def test_create_provider_from_named_binding(self): @@ -140,9 +140,9 @@ def __init__(self, arg: str): self.assertIsInstance(instance, MyOtherType) self.assertEqual("my_arg_1", instance.arg) self.assertEqual([ - call(self.context.get_child_context(Target(str, "arg"))), - call(self.context.get_child_context(Target(str))), - call(self.context.get_child_context(Target(SingletonScope))), + call(self.context.get_child_context(Target(str, "arg"), allow_jit_provider=False)), + call(self.context.get_child_context(Target(str), allow_jit_provider=True)), + call(self.context.get_child_context(Target(SingletonScope), allow_jit_provider=True)), ], self.state.provider_creator.get_provider.call_args_list) def test_create_provider_with_named_positional_argument(self): @@ -164,8 +164,8 @@ def __init__(self, arg: str): self.assertIsInstance(instance, MyOtherType) self.assertEqual("my_arg_1", instance.arg) self.assertEqual([ - call(self.context.get_child_context(Target(str, "my_name"))), - call(self.context.get_child_context(Target(SingletonScope))), + call(self.context.get_child_context(Target(str, "my_name"), allow_jit_provider=True)), + call(self.context.get_child_context(Target(SingletonScope), allow_jit_provider=True)), ], self.state.provider_creator.get_provider.call_args_list) def test_create_provider_with_named_args(self): @@ -187,8 +187,8 @@ def __init__(self, *arg: str): self.assertIsInstance(instance, MyOtherType) self.assertEqual(("my_arg_1",), instance.arg) self.assertEqual([ - call(self.context.get_child_context(Target(List[str], "my_name"))), - call(self.context.get_child_context(Target(SingletonScope))), + call(self.context.get_child_context(Target(List[str], "my_name"), allow_jit_provider=True)), + call(self.context.get_child_context(Target(SingletonScope), allow_jit_provider=True)), ], self.state.provider_creator.get_provider.call_args_list) def test_create_provider_with_missing_parameters_raises_exception(self): diff --git a/tests_e2e/test_injection.py b/tests_e2e/test_injection.py index b1d3a5c..c795b1f 100644 --- a/tests_e2e/test_injection.py +++ b/tests_e2e/test_injection.py @@ -47,6 +47,32 @@ def __init__(self, my_arg: MyClass = None): self.assertIsInstance(my_instance, ParentClass) self.assertIsNone(my_instance.my_arg) + def test_auto_injection_with_named_binding(self): + class ParentClass: + @named_arg("my_arg", "my_name") + def __init__(self, my_arg: MyClass): + self.my_arg = my_arg + + injector = Injector(options=InjectorOptions(auto_bindings=True)) + my_instance = injector.inject(ParentClass) + self.assertIsInstance(my_instance, ParentClass) + self.assertIsInstance(my_instance.my_arg, MyClass) + + def test_auto_injection_with_binding_override(self): + class ParentClass: + def __init__(self, my_arg: MyClass): + self.my_arg = my_arg + + class MySubClass(MyClass): + pass + + injector = Injector(bindings=[ + ClassBinding(MyClass, MySubClass) + ], options=InjectorOptions(auto_bindings=True)) + my_instance = injector.inject(ParentClass) + self.assertIsInstance(my_instance, ParentClass) + self.assertIsInstance(my_instance.my_arg, MySubClass) + def test_subtype_argument_injection(self): class MySubClass(MyClass): pass @@ -785,7 +811,6 @@ def __init__(self, arg: MyOtherClass): with self.assertRaises(CyclicDependencyError): Injector(bindings=[ClassBinding(MyClass, MyImpl), SelfBinding(MyOtherClass)]) - Injector(bindings=[ClassBinding(MyClass, MyImpl)]) def test_cyclic_dependencies_with_private_module_are_handled(self): class MyOtherClass: