Coverage for greyhorse / river / component.py: 100%
67 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-06-22 19:38 +0300
« prev ^ index » next coverage.py v7.14.0, created at 2026-06-22 19:38 +0300
1"""Component — lifecycle container for operators and resource slots.
3Implements ``OperatorResolver`` protocol and adds lifecycle phases
4(IDLE → SETUP → STARTED → STOPPED → IDLE) with a slot registry.
6Component does **not own** fragments — it receives them already setup'd.
7Fragment lifecycle is managed by Module (future).
8"""
10from __future__ import annotations
12import enum
13from collections.abc import Iterable, Mapping
14from enum import IntEnum
15from types import MappingProxyType
16from typing import Any
18from greyhorse.app.private.functional.linker import FragmentLinker
19from greyhorse.app.resources.slot import OwnerSlot
22class ComponentState(enum.Enum):
23 """Lifecycle state of a Component."""
25 IDLE = enum.auto()
26 SETUP = enum.auto()
27 STARTED = enum.auto()
28 STOPPED = enum.auto()
31class Component:
32 """Lifecycle container for operators and resource slots.
34 Wraps fragments into a ``FragmentLinker``, manages lifecycle phases,
35 and provides a slot registry for controller-owned resources.
37 Component does not own fragments — they must be setup'd before
38 being passed in. Fragment lifecycle is Module's responsibility.
40 Args:
41 name: Component identifier.
42 fragments: Iterable of ``Fragment`` instances (must be setup'd).
43 scope: Optional phase scope for ``linker.link()``.
44 context_values: Pre-resolved runtime dependencies.
45 """
47 __slots__ = ('_context_values', '_linker', '_name', '_scope', '_slot_registry', '_state')
49 def __init__(
50 self,
51 name: str,
52 fragments: Iterable = (),
53 scope: IntEnum | None = None,
54 context_values: Mapping[str, Any] | None = None,
55 ) -> None:
56 self._name = name
57 self._linker = FragmentLinker(fragments)
58 self._scope = scope
59 self._context_values = MappingProxyType(dict(context_values or {}))
60 self._slot_registry: dict[str, OwnerSlot] = {}
61 self._state = ComponentState.IDLE
63 # ------------------------------------------------------------------
64 # OperatorResolver protocol
65 # ------------------------------------------------------------------
67 @property
68 def linker(self) -> FragmentLinker:
69 """FragmentLinker for multi-root type resolution across fragments."""
70 return self._linker
72 @property
73 def scope(self) -> IntEnum | None:
74 """Optional phase scope for ``linker.link()`` filtering."""
75 return self._scope
77 @property
78 def context_values(self) -> Mapping[str, Any]:
79 """Read-only mapping of pre-resolved runtime dependencies."""
80 return self._context_values
82 # ------------------------------------------------------------------
83 # Identity and state
84 # ------------------------------------------------------------------
86 @property
87 def name(self) -> str:
88 return self._name
90 @property
91 def state(self) -> ComponentState:
92 return self._state
94 # ------------------------------------------------------------------
95 # Lifecycle
96 # ------------------------------------------------------------------
98 def _require_state(self, expected: ComponentState, method: str) -> None:
99 if self._state != expected:
100 raise RuntimeError(
101 f'{self._name}: {method}() requires {expected.name}, got {self._state.name}'
102 )
104 def setup(self) -> None:
105 """Initialize component. IDLE → SETUP."""
106 self._require_state(ComponentState.IDLE, 'setup')
107 self._state = ComponentState.SETUP
109 def start(self) -> None:
110 """Start component. SETUP → STARTED."""
111 self._require_state(ComponentState.SETUP, 'start')
112 self._state = ComponentState.STARTED
114 def stop(self) -> None:
115 """Stop component. STARTED → STOPPED."""
116 self._require_state(ComponentState.STARTED, 'stop')
117 self._state = ComponentState.STOPPED
119 def teardown(self) -> None:
120 """Teardown component. STOPPED|SETUP → IDLE."""
121 if self._state not in (ComponentState.STOPPED, ComponentState.SETUP):
122 self._require_state(ComponentState.STOPPED, 'teardown')
123 for slot in reversed(list(self._slot_registry.values())):
124 slot.remove('teardown')
125 self._slot_registry.clear()
126 self._state = ComponentState.IDLE
128 # ------------------------------------------------------------------
129 # Slot registry
130 # ------------------------------------------------------------------
132 def create_slot[T](self, name: str) -> OwnerSlot[T]:
133 """Create and register a named resource slot."""
134 if name in self._slot_registry:
135 raise RuntimeError(f'Component {self._name}: slot "{name}" already exists')
136 slot: OwnerSlot[T] = OwnerSlot(name)
137 self._slot_registry[name] = slot
138 return slot
140 def get_slot(self, name: str) -> OwnerSlot | None:
141 """Get a registered slot by name, or None."""
142 return self._slot_registry.get(name)
144 @property
145 def slots(self) -> Mapping[str, OwnerSlot]:
146 """Read-only view of registered slots."""
147 return MappingProxyType(self._slot_registry)