Coverage for greyhorse / river / private / injection.py: 92%

48 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-18 11:33 +0300

1"""Dependency injection helpers for operator execution. 

2 

3Two injection strategies: 

4 

5- ``inject_functor`` — per-param ``Selector.get()`` (from prototype, O(N)). 

6- ``inject_functor_via_linker`` — multi-root ``FragmentLinker.link()`` with 

7 overrides-first semantics and cross-fragment resolution. 

8""" 

9 

10from __future__ import annotations 

11 

12from enum import IntEnum 

13from functools import partial 

14from typing import Any 

15 

16from greyhorse.app.abc.collections.selectors import Selector 

17from greyhorse.app.private.functional.linker import FragmentLinker 

18from greyhorse.app.private.resolving import ValueResolver, _type_to_param_name 

19from greyhorse.factory import Factory 

20from greyhorse.utils.types import unwrap_maybe, unwrap_optional 

21 

22 

23def inject_functor[T](functor: Factory[T], selector: Selector[type, Any]) -> None: 

24 """Inject dependencies into a factory via per-param Selector lookup. 

25 

26 Args: 

27 functor: Factory to inject into (mutated in place). 

28 selector: Provides ``get(type) -> Maybe[value]``. 

29 """ 

30 for param_type in functor.actual_params.values(): 

31 stripped_param_type: type = unwrap_maybe(unwrap_optional(param_type)) 

32 selector.get(stripped_param_type).map( 

33 partial(functor.add_typed_arg, stripped_param_type) 

34 ) 

35 

36 

37def inject_functor_via_linker( 

38 functor: Factory, 

39 linker: FragmentLinker, 

40 scope: IntEnum | None, 

41 external: dict[str, Any] | None = None, 

42 skip_slots: set[type] | None = None, 

43) -> list[ValueResolver]: 

44 """Inject dependencies via FragmentLinker with overrides-first semantics. 

45 

46 Processing order: external overrides first (excluded from linking), 

47 then remaining types via ``linker.link()`` (multi-root, cross-fragment). 

48 

49 Args: 

50 functor: Factory to inject into (mutated in place). 

51 linker: FragmentLinker for multi-root type resolution. 

52 scope: Optional phase scope for ``linker.link()``. 

53 external: Pre-resolved values keyed by snake_case type name. 

54 skip_slots: Types to exclude from both override and linking. 

55 

56 Returns: 

57 List of opened ``ValueResolver`` instances — caller must close them 

58 via ``__exit__`` when resolved instances are no longer needed. 

59 

60 Raises: 

61 RuntimeError: If a required type cannot be resolved. 

62 """ 

63 from greyhorse.app.private.functional.operator_ctx import ( 

64 _build_param_layout, 

65 _resolve_instances, 

66 ) 

67 from greyhorse.utils.types import unwrap 

68 

69 external = external or {} 

70 skip_slots = skip_slots or set() 

71 

72 override_types: set[type] = set() 

73 for _name, raw_type in functor.actual_params.items(): 

74 plain: type = unwrap(raw_type) 

75 pname = _type_to_param_name(plain) 

76 if pname in external and functor.add_typed_arg(plain, external[pname]): 

77 override_types.add(plain) 

78 

79 remaining: dict[str, type] = {} 

80 for _name, raw_type in functor.actual_params.items(): 

81 plain = unwrap(raw_type) 

82 if plain not in override_types and plain not in skip_slots: 

83 remaining[_name] = raw_type # type: ignore[assignment] 

84 

85 if not remaining: 

86 return [] 

87 

88 layout = _build_param_layout(remaining) 

89 match linker.link(layout.required_types, layout.optional_types, scope): 

90 case result if result.is_err(): 

91 raise RuntimeError(str(result.unwrap_err())) 

92 case result: 

93 linkage = result.unwrap() 

94 

95 if linkage.pending: 

96 unsatisfied = [ 

97 p for p in linkage.pending 

98 if _type_to_param_name(p.target_type) not in external # type: ignore[attr-defined] 

99 ] 

100 if unsatisfied: 

101 names = ', '.join(p.target_type.__qualname__ for p in unsatisfied) # type: ignore[attr-defined] 

102 raise RuntimeError(f'Unresolved dependencies: {names}') 

103 

104 instances, resolvers = _resolve_instances(linkage, external) 

105 

106 for raw_type in remaining.values(): 

107 plain = unwrap(raw_type) 

108 if plain in instances: 

109 functor.add_typed_arg(plain, instances[plain]) 

110 

111 return list(resolvers.values())