diff --git a/andi/andi.py b/andi/andi.py index 051f7c0..452f8e2 100644 --- a/andi/andi.py +++ b/andi/andi.py @@ -1,8 +1,8 @@ from collections import OrderedDict, defaultdict from dataclasses import dataclass from typing import ( - Dict, List, Optional, Type, Callable, Union, Container, - Tuple, MutableMapping, Any, Mapping) + Annotated, Dict, List, Optional, Type, TypeVar, Callable, Union, Container, + Tuple, MutableMapping, Any, Mapping, get_args) from andi.typeutils import ( get_union_args, @@ -11,6 +11,7 @@ get_unannotated_params, get_callable_func_obj, get_type_hints_with_extras, + is_typing_annotated, strip_annotated, ) from andi.errors import ( @@ -21,6 +22,11 @@ ) +_T = TypeVar("T") +_REUSE_ANNOTATION = object() +Reuse = Annotated[_T, _REUSE_ANNOTATION] + + def inspect(class_or_func: Callable) -> Dict[str, List[Optional[Type]]]: """ For each argument of the ``class_or_func`` return a list of possible types. @@ -308,15 +314,26 @@ class we want to override. If ``recursive_overrides`` is True, then overrides = overrides or _empty_overrides class_or_func, overrides = _may_override(class_or_func, overrides, recursive_overrides) - plan, _ = _plan(class_or_func, - is_injectable=is_injectable, - externally_provided=externally_provided, - full_final_kwargs=full_final_kwargs, - dependency_stack=None, - overrides=overrides, - recursive_overrides=recursive_overrides, - custom_builder_fn=custom_builder_fn, - ) + plan_deps = set() + while not plan_deps or plan_deps != last_plan_deps: + last_plan_deps = plan_deps + plan, _ = _plan(class_or_func, + is_injectable=is_injectable, + externally_provided=externally_provided, + full_final_kwargs=full_final_kwargs, + dependency_stack=None, + overrides=overrides, + recursive_overrides=recursive_overrides, + custom_builder_fn=custom_builder_fn, + last_plan_deps=last_plan_deps, + ) + plan_deps = {item[0] for item in plan or []} + + # TODO: Remove logging here. + from logging import getLogger + logger = getLogger(__name__) + logger.error(plan_deps) + return plan @@ -334,7 +351,8 @@ def _plan(class_or_func: Callable, *, overrides: Callable[[Callable], Optional[Callable]], recursive_overrides: bool = False, custom_builder_fn: Callable[[Callable], Optional[Callable]] = lambda _: None, - custom_builder_result: Optional[Callable] = None + custom_builder_result: Optional[Callable] = None, + last_plan_deps: Optional[Plan] = None, ) -> Tuple[Plan, List[Tuple]]: dependency_stack = dependency_stack or [] is_root_call = not dependency_stack # For better code reading @@ -363,7 +381,7 @@ def _plan(class_or_func: Callable, *, for argname, types in arguments.items(): sel_cls, arg_overrides = _select_type( types, is_injectable, externally_provided, overrides, recursive_overrides, - custom_builder_fn + custom_builder_fn, last_plan_deps ) if sel_cls is not None: errors = [] # type: List[Tuple] @@ -432,6 +450,7 @@ def _select_type(types, overrides: Callable[[Callable], Optional[Callable]], recursive_overrides: bool, custom_builder_fn: Callable[[Callable], Optional[Callable]] = lambda _: None, + last_plan_deps: Plan = None, ) -> Tuple[Optional[Callable], OverrideFn]: """ Choose the first type that can be provided. None otherwise. Also return @@ -440,12 +459,26 @@ def _select_type(types, for candidate in types: candidate, new_overrides = _may_override( candidate, overrides, recursive_overrides) - candidate_stripped = strip_annotated(candidate) + + if is_typing_annotated(candidate): + candidate_stripped, annotation1, *_ = get_args(candidate) + if annotation1 == _REUSE_ANNOTATION: + if not last_plan_deps or candidate_stripped not in last_plan_deps: + continue + candidate = candidate_stripped + else: + candidate_stripped = candidate if ( is_injectable(candidate_stripped) or externally_provided(candidate_stripped) or custom_builder_fn(candidate_stripped) is not None ): + + # TODO: Remove logging here. + from logging import getLogger + logger = getLogger(__name__) + logger.error(f"{types} → {candidate}") + return candidate, new_overrides return None, overrides diff --git a/tests/test_plan.py b/tests/test_plan.py index 46e3075..f9f98ce 100644 --- a/tests/test_plan.py +++ b/tests/test_plan.py @@ -6,7 +6,7 @@ import andi from andi import NonProvidableError -from andi.andi import CustomBuilder +from andi.andi import CustomBuilder, Reuse from andi.errors import CyclicDependencyErrCase, \ NonInjectableOrExternalErrCase, LackingAnnotationErrCase from tests.utils import build @@ -607,3 +607,426 @@ def fn(item: Item) -> int: assert error_causes(exc_info) == [ ("item", [NonInjectableOrExternalErrCase("item", Page, [Item])]) ] + + +def test_reuse_1(): + """Reuse[A] | B → B""" + class _A: + pass + + class _B: + pass + + class Input: + + def __init__(self, a: Reuse[_A] | _B): + pass + + plan = andi.plan( + Input, + is_injectable=lambda x: True, + externally_provided={_A, _B}, + ) + assert plan == [ + (_B, {}), + (Input, {'a': _B}) + ] + assert plan.full_final_kwargs + + +def test_reuse_2(): + """Reuse[A] | B, A → A""" + class _A: + pass + + class _B: + pass + + class Input: + + def __init__(self, a: Reuse[_A] | _B, b: _A): + pass + + plan = andi.plan( + Input, + is_injectable=lambda x: True, + externally_provided={_A, _B}, + ) + assert plan == [ + (_A, {}), + (Input, {'a': _A, 'b': _A}) + ] + assert plan.full_final_kwargs + + +def test_reuse_3(): + """A, Reuse[A] | B → A""" + class _A: + pass + + class _B: + pass + + class Input: + + def __init__(self, a: _A, b: Reuse[_A] | _B): + pass + + plan = andi.plan( + Input, + is_injectable=lambda x: True, + externally_provided={_A, _B}, + ) + assert plan == [ + (_A, {}), + (Input, {'a': _A, 'b': _A}) + ] + assert plan.full_final_kwargs + + +def test_reuse_4(): + """Reuse[A] | B, B → B""" + class _A: + pass + + class _B: + pass + + class Input: + + def __init__(self, a: Reuse[_A] | _B, b: _B): + pass + + plan = andi.plan( + Input, + is_injectable=lambda x: True, + externally_provided={_A, _B}, + ) + assert plan == [ + (_B, {}), + (Input, {'a': _B, 'b': _B}) + ] + assert plan.full_final_kwargs + + +def test_reuse_5(): + """B, Reuse[A] | B → B""" + class _A: + pass + + class _B: + pass + + class Input: + + def __init__(self, a: _B, b: Reuse[_A] | _B): + pass + + plan = andi.plan( + Input, + is_injectable=lambda x: True, + externally_provided={_A, _B}, + ) + assert plan == [ + (_B, {}), + (Input, {'a': _B, 'b': _B}) + ] + assert plan.full_final_kwargs + + +def test_reuse_6(): + """Reuse[A] | Reuse[B] | C → C""" + class _A: + pass + + class _B: + pass + + class _C: + pass + + class Input: + + def __init__(self, a: Reuse[_A] | Reuse[_B] | _C): + pass + + plan = andi.plan( + Input, + is_injectable=lambda x: True, + externally_provided={_A, _B, _C}, + ) + assert plan == [ + (_C, {}), + (Input, {'a': _C}) + ] + assert plan.full_final_kwargs + + +def test_reuse_7(): + """A, Reuse[A] | Reuse[B] | C → A""" + class _A: + pass + + class _B: + pass + + class _C: + pass + + class Input: + + def __init__(self, a: _A, b: Reuse[_A] | Reuse[_B] | _C): + pass + + plan = andi.plan( + Input, + is_injectable=lambda x: True, + externally_provided={_A, _B, _C}, + ) + assert plan == [ + (_A, {}), + (Input, {'a': _A, 'b': _A}) + ] + assert plan.full_final_kwargs + + +def test_reuse_8(): + """B, Reuse[A] | Reuse[B] | C → B""" + class _A: + pass + + class _B: + pass + + class _C: + pass + + class Input: + + def __init__(self, a: _B, b: Reuse[_A] | Reuse[_B] | _C): + pass + + plan = andi.plan( + Input, + is_injectable=lambda x: True, + externally_provided={_A, _B, _C}, + ) + assert plan == [ + (_B, {}), + (Input, {'a': _B, 'b': _B}) + ] + assert plan.full_final_kwargs + + +def test_reuse_9(): + """Reuse[A] | Reuse[B] | C, A → A""" + class _A: + pass + + class _B: + pass + + class _C: + pass + + class Input: + + def __init__(self, a: Reuse[_A] | Reuse[_B] | _C, b: _A): + pass + + plan = andi.plan( + Input, + is_injectable=lambda x: True, + externally_provided={_A, _B, _C}, + ) + assert plan == [ + (_A, {}), + (Input, {'a': _A, 'b': _A}) + ] + assert plan.full_final_kwargs + + +def test_reuse_10(): + """Reuse[A] | Reuse[B] | C, B → B""" + class _A: + pass + + class _B: + pass + + class _C: + pass + + class Input: + + def __init__(self, a: Reuse[_A] | Reuse[_B] | _C, b: _B): + pass + + plan = andi.plan( + Input, + is_injectable=lambda x: True, + externally_provided={_A, _B, _C}, + ) + assert plan == [ + (_B, {}), + (Input, {'a': _B, 'b': _B}) + ] + assert plan.full_final_kwargs + + +def test_reuse_11(): + """Reuse[A] | Reuse[B] | C, A, B → A""" + class _A: + pass + + class _B: + pass + + class _C: + pass + + class Input: + + def __init__(self, a: Reuse[_A] | Reuse[_B] | _C, b: _A, c: _B): + pass + + plan = andi.plan( + Input, + is_injectable=lambda x: True, + externally_provided={_A, _B, _C}, + ) + assert plan == [ + (_A, {}), + (_B, {}), + (Input, {'a': _A, 'b': _A, 'c': _B}) + ] + assert plan.full_final_kwargs + + +def test_reuse_12(): + """Reuse[A] | Reuse[B] | C, B, A → A""" + class _A: + pass + + class _B: + pass + + class _C: + pass + + class Input: + + def __init__(self, a: Reuse[_A] | Reuse[_B] | _C, b: _B, c: _A): + pass + + plan = andi.plan( + Input, + is_injectable=lambda x: True, + externally_provided={_A, _B, _C}, + ) + assert plan == [ + (_A, {}), + (_B, {}), + (Input, {'a': _A, 'b': _B, 'c': _A}) + ] + assert plan.full_final_kwargs + + +def test_reuse_13(): + """A, B, Reuse[A] | Reuse[B] | C → A""" + class _A: + pass + + class _B: + pass + + class _C: + pass + + class Input: + + def __init__(self, a: _A, b: _B, c: Reuse[_A] | Reuse[_B] | _C): + pass + + plan = andi.plan( + Input, + is_injectable=lambda x: True, + externally_provided={_A, _B, _C}, + ) + assert plan == [ + (_A, {}), + (_B, {}), + (Input, {'a': _A, 'b': _B, 'c': _A}) + ] + assert plan.full_final_kwargs + + +def test_reuse_14(): + """B, A, Reuse[A] | Reuse[B] | C → A""" + class _A: + pass + + class _B: + pass + + class _C: + pass + + class Input: + + def __init__(self, a: _B, b: _A, c: Reuse[_A] | Reuse[_B] | _C): + pass + + plan = andi.plan( + Input, + is_injectable=lambda x: True, + externally_provided={_A, _B, _C}, + ) + assert plan == [ + (_B, {}), + (_A, {}), + (Input, {'a': _B, 'b': _A, 'c': _A}) + ] + assert plan.full_final_kwargs + + +def test_reuse_15(): + """Reuse[A] | Reuse[B] | C, Reuse[B] | Reuse[A] | C, A, B → A, B""" + class _A: + pass + + class _B: + pass + + class _C: + pass + + class Input: + + def __init__(self, a: Reuse[_A] | Reuse[_B] | _C, b: Reuse[_B] | Reuse[_A] | _C, c: _A, d: _B): + pass + + plan = andi.plan( + Input, + is_injectable=lambda x: True, + externally_provided={_A, _B, _C}, + ) + assert plan == [ + (_A, {}), + (_B, {}), + (Input, {'a': _A, 'b': _B, 'c': _A, 'd': _B}) + ] + assert plan.full_final_kwargs + + +# Scenarios to test: +# Reuse[A] | Reuse[B] | C, Reuse[B] | Reuse[A] | C, B, A → A, B +# A, B, Reuse[A] | Reuse[B] | C, Reuse[B] | Reuse[A] | C → A, B +# B, A, Reuse[A] | Reuse[B] | C, Reuse[B] | Reuse[A] | C → A, B +# Reuse[A] → Error? +# Reuse[A] | None → None +# Reuse[A], A → A +# A, Reuse[A] → A +# Nested scenarios +# Scenarios involving _may_override (do we need to remove the reuse annotation before that?) +# Do we treat extra annotation args as errors if the first arg is _REUSE_ANNOTATION? \ No newline at end of file