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.
@@ -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
- 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().")
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
- def register_scope(self, name: str, implementation: ScopeProtocol) -> None:
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
- from .exceptions import ScopeError
53
- raise ScopeError("Cannot register or override reserved scopes: 'singleton' or 'prototype'")
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