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
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-18 11:33 +0300
1"""Dependency injection helpers for operator execution.
3Two injection strategies:
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"""
10from __future__ import annotations
12from enum import IntEnum
13from functools import partial
14from typing import Any
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
23def inject_functor[T](functor: Factory[T], selector: Selector[type, Any]) -> None:
24 """Inject dependencies into a factory via per-param Selector lookup.
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 )
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.
46 Processing order: external overrides first (excluded from linking),
47 then remaining types via ``linker.link()`` (multi-root, cross-fragment).
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.
56 Returns:
57 List of opened ``ValueResolver`` instances — caller must close them
58 via ``__exit__`` when resolved instances are no longer needed.
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
69 external = external or {}
70 skip_slots = skip_slots or set()
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)
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]
85 if not remaining:
86 return []
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()
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}')
104 instances, resolvers = _resolve_instances(linkage, external)
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])
111 return list(resolvers.values())