pico-ioc 2.0.5__py3-none-any.whl → 2.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pico_ioc/__init__.py +9 -12
- pico_ioc/_version.py +1 -1
- pico_ioc/analysis.py +92 -0
- pico_ioc/aop.py +20 -12
- pico_ioc/api.py +63 -1088
- pico_ioc/component_scanner.py +166 -0
- pico_ioc/config_builder.py +87 -0
- pico_ioc/config_registrar.py +236 -0
- pico_ioc/config_runtime.py +54 -13
- pico_ioc/container.py +208 -155
- pico_ioc/decorators.py +193 -0
- pico_ioc/dependency_validator.py +103 -0
- pico_ioc/event_bus.py +24 -25
- pico_ioc/exceptions.py +0 -16
- pico_ioc/factory.py +2 -1
- pico_ioc/locator.py +76 -2
- pico_ioc/provider_selector.py +35 -0
- pico_ioc/registrar.py +188 -0
- pico_ioc/scope.py +13 -4
- {pico_ioc-2.0.5.dist-info → pico_ioc-2.1.1.dist-info}/METADATA +94 -45
- pico_ioc-2.1.1.dist-info/RECORD +25 -0
- pico_ioc-2.0.5.dist-info/RECORD +0 -17
- {pico_ioc-2.0.5.dist-info → pico_ioc-2.1.1.dist-info}/WHEEL +0 -0
- {pico_ioc-2.0.5.dist-info → pico_ioc-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-2.0.5.dist-info → pico_ioc-2.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union, Set
|
|
2
|
+
from .factory import ComponentFactory, ProviderMetadata
|
|
3
|
+
from .locator import ComponentLocator
|
|
4
|
+
from .exceptions import InvalidBindingError
|
|
5
|
+
from .analysis import DependencyRequest
|
|
6
|
+
|
|
7
|
+
KeyT = Union[str, type]
|
|
8
|
+
|
|
9
|
+
def _fmt(k: KeyT) -> str:
|
|
10
|
+
return getattr(k, '__name__', str(k))
|
|
11
|
+
|
|
12
|
+
def _skip_type(t: type) -> bool:
|
|
13
|
+
if t in (str, int, float, bool, bytes):
|
|
14
|
+
return True
|
|
15
|
+
if t is Any:
|
|
16
|
+
return True
|
|
17
|
+
if getattr(t, "_is_protocol", False):
|
|
18
|
+
return True
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
class DependencyValidator:
|
|
22
|
+
def __init__(self, metadata: Dict[KeyT, ProviderMetadata], factory: ComponentFactory, locator: ComponentLocator):
|
|
23
|
+
self._metadata = metadata
|
|
24
|
+
self._factory = factory
|
|
25
|
+
self._locator = locator
|
|
26
|
+
|
|
27
|
+
def _find_md_for_type(self, t: type) -> Optional[ProviderMetadata]:
|
|
28
|
+
cands: List[ProviderMetadata] = []
|
|
29
|
+
for md in self._metadata.values():
|
|
30
|
+
typ = md.provided_type or md.concrete_class
|
|
31
|
+
if not isinstance(typ, type):
|
|
32
|
+
continue
|
|
33
|
+
try:
|
|
34
|
+
if issubclass(typ, t):
|
|
35
|
+
cands.append(md)
|
|
36
|
+
except TypeError:
|
|
37
|
+
pass
|
|
38
|
+
except Exception:
|
|
39
|
+
continue
|
|
40
|
+
if not cands:
|
|
41
|
+
if getattr(t, "_is_protocol", False):
|
|
42
|
+
for md in self._metadata.values():
|
|
43
|
+
typ = md.provided_type or md.concrete_class
|
|
44
|
+
if isinstance(typ, type) and ComponentLocator._implements_protocol(typ, t):
|
|
45
|
+
cands.append(md)
|
|
46
|
+
|
|
47
|
+
if not cands:
|
|
48
|
+
return None
|
|
49
|
+
prim = [m for m in cands if m.primary]
|
|
50
|
+
return prim[0] if prim else cands[0]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _find_md_for_name(self, name: str) -> Optional[KeyT]:
|
|
54
|
+
return self._locator.find_key_by_name(name)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def validate_bindings(self) -> None:
|
|
58
|
+
errors: List[str] = []
|
|
59
|
+
|
|
60
|
+
for k, md in self._metadata.items():
|
|
61
|
+
if md.infra == "configuration":
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
if not md.dependencies and md.infra not in ("configured", "component") and not md.override:
|
|
65
|
+
continue
|
|
66
|
+
if md.infra == "component" and md.concrete_class and md.concrete_class.__init__ is object.__init__:
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
loc_name = f"component {_fmt(k)}"
|
|
71
|
+
if md.factory_method:
|
|
72
|
+
loc_name = f"factory method {md.factory_method}"
|
|
73
|
+
|
|
74
|
+
for dep in md.dependencies:
|
|
75
|
+
if dep.is_optional:
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
if dep.is_list:
|
|
79
|
+
if dep.qualifier:
|
|
80
|
+
if not self._locator.collect_by_type(dep.key, dep.qualifier) and isinstance(dep.key, type) and not _skip_type(dep.key):
|
|
81
|
+
errors.append(f"{_fmt(k)} ({loc_name}) expects List[{_fmt(dep.key)}] with qualifier '{dep.qualifier}' but no matching components exist")
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
dep_key = dep.key
|
|
85
|
+
if isinstance(dep_key, str):
|
|
86
|
+
key_found_by_name = self._find_md_for_name(dep_key)
|
|
87
|
+
directly_bound = dep_key in self._metadata or self._factory.has(dep_key)
|
|
88
|
+
if not key_found_by_name and not directly_bound:
|
|
89
|
+
errors.append(f"{_fmt(k)} ({loc_name}) depends on string key '{dep_key}' which is not bound")
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
if isinstance(dep_key, type) and not _skip_type(dep_key):
|
|
93
|
+
dep_key_found = self._factory.has(dep_key) or dep_key in self._metadata
|
|
94
|
+
if not dep_key_found:
|
|
95
|
+
assignable_md = self._find_md_for_type(dep_key)
|
|
96
|
+
if assignable_md is None:
|
|
97
|
+
by_name_key = self._find_md_for_name(getattr(dep_key, "__name__", ""))
|
|
98
|
+
if by_name_key is None:
|
|
99
|
+
errors.append(f"{_fmt(k)} ({loc_name}) depends on {_fmt(dep_key)} which is not bound")
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
if errors:
|
|
103
|
+
raise InvalidBindingError(errors)
|
pico_ioc/event_bus.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# src/pico_ioc/event_bus.py
|
|
2
1
|
import asyncio
|
|
3
2
|
import inspect
|
|
4
3
|
import logging
|
|
@@ -151,29 +150,30 @@ class EventBus:
|
|
|
151
150
|
self._worker_loop = None
|
|
152
151
|
|
|
153
152
|
def post(self, event: Event) -> None:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
153
|
+
with self._lock:
|
|
154
|
+
if self._closed:
|
|
155
|
+
raise EventBusClosedError()
|
|
156
|
+
if self._queue is None:
|
|
157
|
+
raise EventBusError("Worker queue not initialized. Call start_worker().")
|
|
158
|
+
loop = self._worker_loop
|
|
159
|
+
if loop and loop.is_running():
|
|
160
|
+
try:
|
|
161
|
+
current_loop = asyncio.get_running_loop()
|
|
162
|
+
if current_loop is loop:
|
|
163
|
+
try:
|
|
164
|
+
self._queue.put_nowait(event)
|
|
165
|
+
return
|
|
166
|
+
except asyncio.QueueFull:
|
|
167
|
+
raise EventBusQueueFullError()
|
|
168
|
+
except RuntimeError:
|
|
169
|
+
pass
|
|
170
|
+
try:
|
|
171
|
+
loop.call_soon_threadsafe(self._queue.put_nowait, event)
|
|
172
|
+
return
|
|
173
|
+
except asyncio.QueueFull:
|
|
174
|
+
raise EventBusQueueFullError()
|
|
175
|
+
else:
|
|
176
|
+
raise EventBusError("Worker queue not initialized or loop not running. Call start_worker().")
|
|
177
177
|
|
|
178
178
|
async def aclose(self) -> None:
|
|
179
179
|
await self.stop_worker()
|
|
@@ -220,4 +220,3 @@ class PicoEventBusProvider:
|
|
|
220
220
|
loop.create_task(event_bus.aclose())
|
|
221
221
|
else:
|
|
222
222
|
asyncio.run(event_bus.aclose())
|
|
223
|
-
|
pico_ioc/exceptions.py
CHANGED
|
@@ -14,21 +14,6 @@ class ProviderNotFoundError(PicoError):
|
|
|
14
14
|
self.key = key
|
|
15
15
|
self.origin = origin
|
|
16
16
|
|
|
17
|
-
class CircularDependencyError(PicoError):
|
|
18
|
-
def __init__(self, chain: Iterable[Any], current: Any, details: str | None = None, hint: str | None = None):
|
|
19
|
-
chain_str = " -> ".join(getattr(k, "__name__", str(k)) for k in chain)
|
|
20
|
-
cur_str = getattr(current, "__name__", str(current))
|
|
21
|
-
base = f"Circular dependency detected: {chain_str} -> {cur_str}"
|
|
22
|
-
if details:
|
|
23
|
-
base += f"\n\n{details}"
|
|
24
|
-
if hint:
|
|
25
|
-
base += f"\n\nHint: {hint}"
|
|
26
|
-
super().__init__(base)
|
|
27
|
-
self.chain = tuple(chain)
|
|
28
|
-
self.current = current
|
|
29
|
-
self.details = details
|
|
30
|
-
self.hint = hint
|
|
31
|
-
|
|
32
17
|
class ComponentCreationError(PicoError):
|
|
33
18
|
def __init__(self, key: Any, cause: Exception):
|
|
34
19
|
k = getattr(key, "__name__", key)
|
|
@@ -84,4 +69,3 @@ class EventBusHandlerError(EventBusError):
|
|
|
84
69
|
self.event_name = event_name
|
|
85
70
|
self.handler_name = handler_name
|
|
86
71
|
self.cause = cause
|
|
87
|
-
|
pico_ioc/factory.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from typing import Any, Callable, Dict, Optional, Set, Tuple, Union
|
|
4
4
|
from .exceptions import ProviderNotFoundError
|
|
5
|
+
from .analysis import DependencyRequest
|
|
5
6
|
|
|
6
7
|
KeyT = Union[str, type]
|
|
7
8
|
Provider = Callable[[], Any]
|
|
@@ -18,6 +19,7 @@ class ProviderMetadata:
|
|
|
18
19
|
lazy: bool
|
|
19
20
|
infra: Optional[str]
|
|
20
21
|
pico_name: Optional[Any]
|
|
22
|
+
dependencies: Tuple[DependencyRequest, ...] = ()
|
|
21
23
|
override: bool = False
|
|
22
24
|
scope: str = "singleton"
|
|
23
25
|
|
|
@@ -45,4 +47,3 @@ class DeferredProvider:
|
|
|
45
47
|
if self._pico is None or self._locator is None:
|
|
46
48
|
raise RuntimeError("DeferredProvider must be attached before use")
|
|
47
49
|
return self._builder(self._pico, self._locator)
|
|
48
|
-
|
pico_ioc/locator.py
CHANGED
|
@@ -1,45 +1,62 @@
|
|
|
1
|
-
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union
|
|
1
|
+
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union, get_origin, Annotated, get_args
|
|
2
2
|
from .factory import ProviderMetadata
|
|
3
|
+
from .decorators import Qualifier
|
|
4
|
+
import inspect
|
|
5
|
+
from .analysis import DependencyRequest
|
|
3
6
|
|
|
4
7
|
KeyT = Union[str, type]
|
|
5
8
|
|
|
9
|
+
def _get_signature_static(fn):
|
|
10
|
+
return inspect.signature(fn)
|
|
11
|
+
|
|
6
12
|
class ComponentLocator:
|
|
7
13
|
def __init__(self, metadata: Dict[KeyT, ProviderMetadata], indexes: Dict[str, Dict[Any, List[KeyT]]]) -> None:
|
|
8
14
|
self._metadata = metadata
|
|
9
15
|
self._indexes = indexes
|
|
10
16
|
self._candidates: Optional[Set[KeyT]] = None
|
|
17
|
+
|
|
11
18
|
def _ensure(self) -> Set[KeyT]:
|
|
12
19
|
return set(self._metadata.keys()) if self._candidates is None else set(self._candidates)
|
|
20
|
+
|
|
13
21
|
def _select_index(self, name: str, values: Iterable[Any]) -> Set[KeyT]:
|
|
14
22
|
out: Set[KeyT] = set()
|
|
15
23
|
idx = self._indexes.get(name, {})
|
|
16
24
|
for v in values:
|
|
17
25
|
out.update(idx.get(v, []))
|
|
18
26
|
return out
|
|
27
|
+
|
|
19
28
|
def _new(self, candidates: Set[KeyT]) -> "ComponentLocator":
|
|
20
29
|
nl = ComponentLocator(self._metadata, self._indexes)
|
|
21
30
|
nl._candidates = candidates
|
|
22
31
|
return nl
|
|
32
|
+
|
|
23
33
|
def with_index_any(self, name: str, *values: Any) -> "ComponentLocator":
|
|
24
34
|
base = self._ensure()
|
|
25
35
|
sel = self._select_index(name, values)
|
|
26
36
|
return self._new(base & sel)
|
|
37
|
+
|
|
27
38
|
def with_index_all(self, name: str, *values: Any) -> "ComponentLocator":
|
|
28
39
|
base = self._ensure()
|
|
29
40
|
cur = base
|
|
30
41
|
for v in values:
|
|
31
42
|
cur = cur & set(self._indexes.get(name, {}).get(v, []))
|
|
32
43
|
return self._new(cur)
|
|
44
|
+
|
|
33
45
|
def with_qualifier_any(self, *qs: Any) -> "ComponentLocator":
|
|
34
46
|
return self.with_index_any("qualifier", *qs)
|
|
47
|
+
|
|
35
48
|
def primary_only(self) -> "ComponentLocator":
|
|
36
49
|
return self.with_index_any("primary", True)
|
|
50
|
+
|
|
37
51
|
def lazy(self, is_lazy: bool = True) -> "ComponentLocator":
|
|
38
52
|
return self.with_index_any("lazy", True) if is_lazy else self.with_index_any("lazy", False)
|
|
53
|
+
|
|
39
54
|
def infra(self, *names: Any) -> "ComponentLocator":
|
|
40
55
|
return self.with_index_any("infra", *names)
|
|
56
|
+
|
|
41
57
|
def pico_name(self, *names: Any) -> "ComponentLocator":
|
|
42
58
|
return self.with_index_any("pico_name", *names)
|
|
59
|
+
|
|
43
60
|
def by_key_type(self, t: type) -> "ComponentLocator":
|
|
44
61
|
base = self._ensure()
|
|
45
62
|
if t is str:
|
|
@@ -49,5 +66,62 @@ class ComponentLocator:
|
|
|
49
66
|
else:
|
|
50
67
|
c = {k for k in base if isinstance(k, t)}
|
|
51
68
|
return self._new(c)
|
|
69
|
+
|
|
52
70
|
def keys(self) -> List[KeyT]:
|
|
53
|
-
return list(self._ensure())
|
|
71
|
+
return list(self._ensure())
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def _implements_protocol(typ: type, proto: type) -> bool:
|
|
75
|
+
if not getattr(proto, "_is_protocol", False):
|
|
76
|
+
return False
|
|
77
|
+
try:
|
|
78
|
+
if getattr(proto, "__runtime_protocol__", False) or getattr(proto, "__annotations__", None) is not None:
|
|
79
|
+
inst = object.__new__(typ)
|
|
80
|
+
return isinstance(inst, proto)
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
for name, val in proto.__dict__.items():
|
|
84
|
+
if name.startswith("_") or not callable(val):
|
|
85
|
+
continue
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
def collect_by_type(self, t: type, q: Optional[str]) -> List[KeyT]:
|
|
89
|
+
keys = list(self._metadata.keys())
|
|
90
|
+
out: List[KeyT] = []
|
|
91
|
+
for k in keys:
|
|
92
|
+
md = self._metadata.get(k)
|
|
93
|
+
if md is None:
|
|
94
|
+
continue
|
|
95
|
+
typ = md.provided_type or md.concrete_class
|
|
96
|
+
if not isinstance(typ, type):
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
ok = False
|
|
100
|
+
try:
|
|
101
|
+
ok = issubclass(typ, t)
|
|
102
|
+
except Exception:
|
|
103
|
+
ok = ComponentLocator._implements_protocol(typ, t)
|
|
104
|
+
|
|
105
|
+
if ok and (q is None or q in md.qualifiers):
|
|
106
|
+
out.append(k)
|
|
107
|
+
return out
|
|
108
|
+
|
|
109
|
+
def find_key_by_name(self, name: str) -> Optional[KeyT]:
|
|
110
|
+
for k, md in self._metadata.items():
|
|
111
|
+
if md.pico_name == name:
|
|
112
|
+
return k
|
|
113
|
+
typ = md.provided_type or md.concrete_class
|
|
114
|
+
if isinstance(typ, type) and getattr(typ, "__name__", "") == name:
|
|
115
|
+
return k
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
def dependency_keys_for_static(self, md: ProviderMetadata):
|
|
119
|
+
deps: List[KeyT] = []
|
|
120
|
+
for dep in md.dependencies:
|
|
121
|
+
if dep.is_list:
|
|
122
|
+
if isinstance(dep.key, type):
|
|
123
|
+
keys = self.collect_by_type(dep.key, dep.qualifier)
|
|
124
|
+
deps.extend(keys)
|
|
125
|
+
else:
|
|
126
|
+
deps.append(dep.key)
|
|
127
|
+
return tuple(deps)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Dict, List, Tuple, Union
|
|
2
|
+
from .factory import Provider, ProviderMetadata
|
|
3
|
+
from .config_registrar import ConfigurationManager
|
|
4
|
+
|
|
5
|
+
KeyT = Union[str, type]
|
|
6
|
+
|
|
7
|
+
class ProviderSelector:
|
|
8
|
+
def __init__(self, config_manager: ConfigurationManager):
|
|
9
|
+
self._config_manager = config_manager
|
|
10
|
+
|
|
11
|
+
def _rank_provider(self, item: Tuple[bool, Provider, ProviderMetadata]) -> Tuple[int, int, int]:
|
|
12
|
+
provider, md = item[1], item[2]
|
|
13
|
+
|
|
14
|
+
is_present = 1 if self._config_manager.prefix_exists(md) else 0
|
|
15
|
+
|
|
16
|
+
pref = str(md.pico_name or "")
|
|
17
|
+
pref_len = len(pref)
|
|
18
|
+
|
|
19
|
+
is_primary = 1 if item[0] else 0
|
|
20
|
+
|
|
21
|
+
return (is_present, pref_len, is_primary)
|
|
22
|
+
|
|
23
|
+
def select_providers(
|
|
24
|
+
self,
|
|
25
|
+
candidates: Dict[KeyT, List[Tuple[bool, Provider, ProviderMetadata]]],
|
|
26
|
+
) -> Dict[KeyT, Tuple[Provider, ProviderMetadata]]:
|
|
27
|
+
|
|
28
|
+
winners: Dict[KeyT, Tuple[Provider, ProviderMetadata]] = {}
|
|
29
|
+
|
|
30
|
+
for key, lst in candidates.items():
|
|
31
|
+
lst_sorted = sorted(lst, key=self._rank_provider, reverse=True)
|
|
32
|
+
chosen = lst_sorted[0]
|
|
33
|
+
winners[key] = (chosen[1], chosen[2])
|
|
34
|
+
|
|
35
|
+
return winners
|
pico_ioc/registrar.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
import functools
|
|
5
|
+
from dataclasses import is_dataclass, fields, MISSING
|
|
6
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union, get_args, get_origin, Iterable
|
|
7
|
+
from .constants import LOGGER, PICO_INFRA, PICO_NAME, PICO_KEY, PICO_META
|
|
8
|
+
from .exceptions import ConfigurationError, InvalidBindingError
|
|
9
|
+
from .factory import ComponentFactory, ProviderMetadata, DeferredProvider
|
|
10
|
+
from .locator import ComponentLocator
|
|
11
|
+
from .aop import UnifiedComponentProxy
|
|
12
|
+
from .decorators import Qualifier, get_return_type
|
|
13
|
+
from .config_builder import ContextConfig
|
|
14
|
+
from .config_runtime import TreeSource
|
|
15
|
+
from .config_registrar import ConfigurationManager
|
|
16
|
+
from .provider_selector import ProviderSelector
|
|
17
|
+
from .dependency_validator import DependencyValidator
|
|
18
|
+
from .component_scanner import ComponentScanner
|
|
19
|
+
from .analysis import analyze_callable_dependencies, DependencyRequest
|
|
20
|
+
from .container import PicoContainer
|
|
21
|
+
|
|
22
|
+
KeyT = Union[str, type]
|
|
23
|
+
Provider = Callable[[], Any]
|
|
24
|
+
|
|
25
|
+
def _can_be_selected_for(reg_md: Dict[KeyT, ProviderMetadata], selector: Any) -> bool:
|
|
26
|
+
if not isinstance(selector, type):
|
|
27
|
+
return False
|
|
28
|
+
for md in reg_md.values():
|
|
29
|
+
typ = md.provided_type or md.concrete_class
|
|
30
|
+
if isinstance(typ, type):
|
|
31
|
+
try:
|
|
32
|
+
if issubclass(typ, selector):
|
|
33
|
+
return True
|
|
34
|
+
except Exception:
|
|
35
|
+
continue
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
class Registrar:
|
|
39
|
+
def __init__(self, factory: ComponentFactory, *, profiles: Tuple[str, ...] = (), environ: Optional[Dict[str, str]] = None, logger: Optional[logging.Logger] = None, config: Optional[ContextConfig] = None) -> None:
|
|
40
|
+
self._factory = factory
|
|
41
|
+
self._profiles = set(p.strip() for p in profiles if p)
|
|
42
|
+
self._environ = environ if environ is not None else os.environ
|
|
43
|
+
self._metadata: Dict[KeyT, ProviderMetadata] = {}
|
|
44
|
+
self._indexes: Dict[str, Dict[Any, List[KeyT]]] = {}
|
|
45
|
+
self._log = logger or LOGGER
|
|
46
|
+
self._config_manager = ConfigurationManager(config)
|
|
47
|
+
self._provider_selector = ProviderSelector(self._config_manager)
|
|
48
|
+
self._scanner = ComponentScanner(self._profiles, self._environ, self._config_manager)
|
|
49
|
+
self._deferred: List[DeferredProvider] = []
|
|
50
|
+
self._provides_functions: Dict[KeyT, Callable[..., Any]] = {}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def locator(self) -> ComponentLocator:
|
|
54
|
+
loc = ComponentLocator(dict(self._metadata), dict(self._indexes))
|
|
55
|
+
setattr(loc, "_provides_functions", dict(self._provides_functions))
|
|
56
|
+
return loc
|
|
57
|
+
|
|
58
|
+
def attach_runtime(self, pico, locator: ComponentLocator) -> None:
|
|
59
|
+
for deferred in self._deferred:
|
|
60
|
+
deferred.attach(pico, locator)
|
|
61
|
+
for key, md in list(self._metadata.items()):
|
|
62
|
+
if md.lazy:
|
|
63
|
+
original = self._factory.get(key, origin='lazy')
|
|
64
|
+
def lazy_proxy_provider(_orig=original, _p=pico):
|
|
65
|
+
return UnifiedComponentProxy(container=_p, object_creator=_orig)
|
|
66
|
+
self._factory.bind(key, lazy_proxy_provider)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _bind_if_absent(self, key: KeyT, provider: Provider) -> None:
|
|
70
|
+
if not self._factory.has(key):
|
|
71
|
+
self._factory.bind(key, provider)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def register_module(self, module: Any) -> None:
|
|
75
|
+
self._scanner.scan_module(module)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _find_md_for_type(self, t: type) -> Optional[ProviderMetadata]:
|
|
79
|
+
cands: List[ProviderMetadata] = []
|
|
80
|
+
for md in self._metadata.values():
|
|
81
|
+
typ = md.provided_type or md.concrete_class
|
|
82
|
+
if not isinstance(typ, type):
|
|
83
|
+
continue
|
|
84
|
+
try:
|
|
85
|
+
if issubclass(typ, t):
|
|
86
|
+
cands.append(md)
|
|
87
|
+
except Exception:
|
|
88
|
+
continue
|
|
89
|
+
if not cands:
|
|
90
|
+
return None
|
|
91
|
+
prim = [m for m in cands if m.primary]
|
|
92
|
+
return prim[0] if prim else cands[0]
|
|
93
|
+
|
|
94
|
+
def _find_narrower_scope_from_deps(self, deps: Tuple[DependencyRequest, ...]) -> Optional[str]:
|
|
95
|
+
if not deps:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
for dep_req in deps:
|
|
99
|
+
if dep_req.is_list:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
dep_md = self._metadata.get(dep_req.key)
|
|
103
|
+
if dep_md is None:
|
|
104
|
+
if isinstance(dep_req.key, type):
|
|
105
|
+
dep_md = self._find_md_for_type(dep_req.key)
|
|
106
|
+
|
|
107
|
+
if dep_md and dep_md.scope != "singleton":
|
|
108
|
+
return dep_md.scope
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def _promote_scopes(self) -> None:
|
|
112
|
+
for k, md in list(self._metadata.items()):
|
|
113
|
+
if md.scope == "singleton":
|
|
114
|
+
ns = self._find_narrower_scope_from_deps(md.dependencies)
|
|
115
|
+
if ns and ns != "singleton":
|
|
116
|
+
self._metadata[k] = ProviderMetadata(key=md.key, provided_type=md.provided_type, concrete_class=md.concrete_class, factory_class=md.factory_class, factory_method=md.factory_method, qualifiers=md.qualifiers, primary=md.primary, lazy=md.lazy, infra=md.infra, pico_name=md.pico_name, override=md.override, scope=ns, dependencies=md.dependencies)
|
|
117
|
+
|
|
118
|
+
def _rebuild_indexes(self) -> None:
|
|
119
|
+
self._indexes.clear()
|
|
120
|
+
def add(idx: str, val: Any, key: KeyT):
|
|
121
|
+
b = self._indexes.setdefault(idx, {}).setdefault(val, [])
|
|
122
|
+
if key not in b:
|
|
123
|
+
b.append(key)
|
|
124
|
+
|
|
125
|
+
for k, md in self._metadata.items():
|
|
126
|
+
for q in md.qualifiers:
|
|
127
|
+
add("qualifier", q, k)
|
|
128
|
+
if md.primary:
|
|
129
|
+
add("primary", True, k)
|
|
130
|
+
add("lazy", bool(md.lazy), k)
|
|
131
|
+
if md.infra is not None:
|
|
132
|
+
add("infra", md.infra, k)
|
|
133
|
+
if md.pico_name is not None:
|
|
134
|
+
add("pico_name", md.pico_name, k)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def finalize(self, overrides: Optional[Dict[KeyT, Any]], *, pico_instance: PicoContainer) -> None:
|
|
138
|
+
candidates, on_missing, deferred_providers, provides_functions = self._scanner.get_scan_results()
|
|
139
|
+
self._deferred = deferred_providers
|
|
140
|
+
self._provides_functions = provides_functions
|
|
141
|
+
|
|
142
|
+
winners = self._provider_selector.select_providers(candidates)
|
|
143
|
+
for key, (provider, md) in winners.items():
|
|
144
|
+
self._bind_if_absent(key, provider)
|
|
145
|
+
self._metadata[key] = md
|
|
146
|
+
|
|
147
|
+
if PicoContainer not in self._metadata:
|
|
148
|
+
self._factory.bind(PicoContainer, lambda: pico_instance)
|
|
149
|
+
self._metadata[PicoContainer] = ProviderMetadata(
|
|
150
|
+
key=PicoContainer,
|
|
151
|
+
provided_type=PicoContainer,
|
|
152
|
+
concrete_class=PicoContainer,
|
|
153
|
+
factory_class=None,
|
|
154
|
+
factory_method=None,
|
|
155
|
+
qualifiers=set(),
|
|
156
|
+
primary=True,
|
|
157
|
+
lazy=False,
|
|
158
|
+
infra="component",
|
|
159
|
+
pico_name="PicoContainer",
|
|
160
|
+
override=True,
|
|
161
|
+
scope="singleton",
|
|
162
|
+
dependencies=()
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
self._promote_scopes()
|
|
166
|
+
self._rebuild_indexes()
|
|
167
|
+
|
|
168
|
+
for _, selector, default_cls in sorted(on_missing, key=lambda x: -x[0]):
|
|
169
|
+
key = selector
|
|
170
|
+
if key in self._metadata or self._factory.has(key) or _can_be_selected_for(self._metadata, selector):
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
deps = analyze_callable_dependencies(default_cls.__init__)
|
|
174
|
+
provider = DeferredProvider(lambda pico, loc, c=default_cls, d=deps: pico.build_class(c, loc, d))
|
|
175
|
+
qset = set(str(q) for q in getattr(default_cls, PICO_META, {}).get("qualifier", ()))
|
|
176
|
+
sc = getattr(default_cls, PICO_META, {}).get("scope", "singleton")
|
|
177
|
+
md = ProviderMetadata(key=key, provided_type=key if isinstance(key, type) else None, concrete_class=default_cls, factory_class=None, factory_method=None, qualifiers=qset, primary=True, lazy=bool(getattr(default_cls, PICO_META, {}).get("lazy", False)), infra=getattr(default_cls, PICO_INFRA, None), pico_name=getattr(default_cls, PICO_NAME, None), override=True, scope=sc, dependencies=deps)
|
|
178
|
+
|
|
179
|
+
self._bind_if_absent(key, provider)
|
|
180
|
+
self._metadata[key] = md
|
|
181
|
+
if isinstance(provider, DeferredProvider):
|
|
182
|
+
self._deferred.append(provider)
|
|
183
|
+
|
|
184
|
+
self._rebuild_indexes()
|
|
185
|
+
|
|
186
|
+
final_locator = ComponentLocator(self._metadata, self._indexes)
|
|
187
|
+
validator = DependencyValidator(self._metadata, self._factory, final_locator)
|
|
188
|
+
validator.validate_bindings()
|
pico_ioc/scope.py
CHANGED
|
@@ -3,6 +3,7 @@ import contextvars
|
|
|
3
3
|
import inspect
|
|
4
4
|
from typing import Any, Dict, Optional, Tuple
|
|
5
5
|
from collections import OrderedDict
|
|
6
|
+
from .exceptions import ScopeError
|
|
6
7
|
|
|
7
8
|
class ScopeProtocol:
|
|
8
9
|
def get_id(self) -> Any | None: ...
|
|
@@ -42,16 +43,24 @@ class ScopeManager:
|
|
|
42
43
|
self._scopes: Dict[str, ScopeProtocol] = {
|
|
43
44
|
"request": ContextVarScope(contextvars.ContextVar("pico_request_id", default=None)),
|
|
44
45
|
"session": ContextVarScope(contextvars.ContextVar("pico_session_id", default=None)),
|
|
46
|
+
"websocket": ContextVarScope(contextvars.ContextVar("pico_websocket_id", default=None)),
|
|
45
47
|
"transaction": ContextVarScope(contextvars.ContextVar("pico_tx_id", default=None)),
|
|
46
48
|
}
|
|
47
|
-
|
|
49
|
+
|
|
50
|
+
def register_scope(self, name: str) -> None:
|
|
48
51
|
if not isinstance(name, str) or not name:
|
|
49
|
-
from .exceptions import ScopeError
|
|
50
52
|
raise ScopeError("Scope name must be a non-empty string")
|
|
51
53
|
if name in ("singleton", "prototype"):
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
raise ScopeError(f"Cannot register reserved scope: '{name}'")
|
|
55
|
+
if name in self._scopes:
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
var_name = f"pico_{name}_id"
|
|
59
|
+
context_var = contextvars.ContextVar(var_name, default=None)
|
|
60
|
+
implementation = ContextVarScope(context_var)
|
|
54
61
|
self._scopes[name] = implementation
|
|
62
|
+
|
|
63
|
+
|
|
55
64
|
def get_id(self, name: str) -> Any | None:
|
|
56
65
|
if name in ("singleton", "prototype"):
|
|
57
66
|
return None
|