pico-ioc 2.1.0__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 CHANGED
@@ -29,7 +29,7 @@ from .factory import ComponentFactory, ProviderMetadata, DeferredProvider
29
29
  from .aop import MethodCtx, MethodInterceptor, intercepted_by, UnifiedComponentProxy, health, ContainerObserver
30
30
  from .container import PicoContainer
31
31
  from .event_bus import EventBus, ExecPolicy, ErrorPolicy, Event, subscribe, AutoSubscriberMixin
32
- from .config_runtime import JsonTreeSource, YamlTreeSource, DictSource, Discriminator
32
+ from .config_runtime import JsonTreeSource, YamlTreeSource, DictSource, Discriminator, Value
33
33
  from .analysis import DependencyRequest, analyze_callable_dependencies
34
34
 
35
35
  __all__ = [
@@ -88,6 +88,7 @@ __all__ = [
88
88
  "YamlTreeSource",
89
89
  "DictSource",
90
90
  "Discriminator",
91
+ "Value",
91
92
  "DependencyRequest",
92
93
  "analyze_callable_dependencies",
93
94
  ]
pico_ioc/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '2.1.0'
1
+ __version__ = '2.1.1'
pico_ioc/aop.py CHANGED
@@ -1,8 +1,10 @@
1
+ # src/pico_ioc/aop.py
2
+
1
3
  import inspect
2
4
  import pickle
3
5
  import threading
4
6
  from typing import Any, Callable, Dict, List, Tuple, Protocol, Union
5
- from .exceptions import SerializationError
7
+ from .exceptions import SerializationError, AsyncResolutionError
6
8
 
7
9
  KeyT = Union[str, type]
8
10
 
@@ -119,9 +121,19 @@ class UnifiedComponentProxy:
119
121
  tgt = creator()
120
122
  if tgt is None:
121
123
  raise RuntimeError("UnifiedComponentProxy object_creator returned None")
124
+
125
+ container = object.__getattribute__(self, "_container")
126
+ if container and hasattr(container, "_run_configure_methods"):
127
+ res = container._run_configure_methods(tgt)
128
+ if inspect.isawaitable(res):
129
+ raise AsyncResolutionError(
130
+ f"Lazy component {type(tgt).__name__} requires async "
131
+ "@configure but was resolved via sync get()"
132
+ )
133
+
122
134
  object.__setattr__(self, "_target", tgt)
123
135
  return tgt
124
-
136
+
125
137
  def _scope_signature(self) -> Tuple[Any, ...]:
126
138
  container = object.__getattribute__(self, "_container")
127
139
  target = object.__getattribute__(self, "_target")
@@ -138,7 +150,7 @@ class UnifiedComponentProxy:
138
150
  return ()
139
151
  return (container.scopes.get_id(sc),)
140
152
  return ()
141
-
153
+
142
154
  def _build_wrapped(self, name: str, bound: Callable[..., Any], interceptors_cls: Tuple[type, ...]):
143
155
  container = object.__getattribute__(self, "_container")
144
156
  interceptors = [container.get(cls) for cls in interceptors_cls]
@@ -182,11 +194,11 @@ class UnifiedComponentProxy:
182
194
  raise RuntimeError(f"Async interceptor returned awaitable on sync method: {name}")
183
195
  return res
184
196
  return sig, sw, interceptors_cls
185
-
197
+
186
198
  @property
187
199
  def __class__(self):
188
200
  return self._get_real_object().__class__
189
-
201
+
190
202
  def __getattr__(self, name: str) -> Any:
191
203
  target = self._get_real_object()
192
204
  attr = getattr(target, name)
@@ -211,7 +223,7 @@ class UnifiedComponentProxy:
211
223
  sig, wrapped, cls_tuple = self._build_wrapped(name, attr, interceptors_cls)
212
224
  cache[name] = (sig, wrapped, cls_tuple)
213
225
  return wrapped
214
-
226
+
215
227
  def __setattr__(self, name, value): setattr(self._get_real_object(), name, value)
216
228
  def __delattr__(self, name): delattr(self._get_real_object(), name)
217
229
  def __str__(self): return str(self._get_real_object())
@@ -275,4 +287,3 @@ class UnifiedComponentProxy:
275
287
  return (pickle.loads, (data,))
276
288
  except Exception as e:
277
289
  raise SerializationError(f"Proxy target is not serializable: {e}")
278
-
pico_ioc/api.py CHANGED
@@ -1,8 +1,10 @@
1
+ # src/pico_ioc/api.py
2
+
1
3
  import importlib
2
4
  import pkgutil
3
5
  import logging
4
6
  import inspect
5
- from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union, Protocol
7
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
6
8
  from .exceptions import ConfigurationError, InvalidBindingError
7
9
  from .factory import ComponentFactory, ProviderMetadata
8
10
  from .locator import ComponentLocator
@@ -10,6 +12,7 @@ from .scope import ScopeManager, ScopedCaches
10
12
  from .container import PicoContainer
11
13
  from .decorators import component, factory, provides, Qualifier, configure, cleanup, configured
12
14
  from .config_builder import ContextConfig, configuration
15
+ from .registrar import Registrar
13
16
 
14
17
  KeyT = Union[str, type]
15
18
  Provider = Callable[[], Any]
@@ -26,7 +29,6 @@ def _iter_input_modules(inputs: Union[Any, Iterable[Any]]) -> Iterable[Any]:
26
29
  mod = importlib.import_module(it)
27
30
  else:
28
31
  mod = it
29
-
30
32
  if hasattr(mod, "__path__"):
31
33
  for sub in _scan_package(mod):
32
34
  name = getattr(sub, "__name__", None)
@@ -49,28 +51,37 @@ def _normalize_override_provider(v: Any):
49
51
  return (lambda f=v: f()), False
50
52
  return (lambda inst=v: inst), False
51
53
 
52
- def init(modules: Union[Any, Iterable[Any]], *, profiles: Tuple[str, ...] = (), allowed_profiles: Optional[Iterable[str]] = None, environ: Optional[Dict[str, str]] = None, overrides: Optional[Dict[KeyT, Any]] = None, logger: Optional[logging.Logger] = None, config: Optional[ContextConfig] = None, custom_scopes: Optional[Dict[str, "ScopeProtocol"]] = None, validate_only: bool = False, container_id: Optional[str] = None, observers: Optional[List["ContainerObserver"]] = None,) -> PicoContainer:
53
- from .registrar import Registrar
54
-
54
+ def init(
55
+ modules: Union[Any, Iterable[Any]],
56
+ *,
57
+ profiles: Tuple[str, ...] = (),
58
+ allowed_profiles: Optional[Iterable[str]] = None,
59
+ environ: Optional[Dict[str, str]] = None,
60
+ overrides: Optional[Dict[KeyT, Any]] = None,
61
+ logger: Optional[logging.Logger] = None,
62
+ config: Optional[ContextConfig] = None,
63
+ custom_scopes: Optional[Iterable[str]] = None,
64
+ validate_only: bool = False,
65
+ container_id: Optional[str] = None,
66
+ observers: Optional[List["ContainerObserver"]] = None,
67
+ ) -> PicoContainer:
55
68
  active = tuple(p.strip() for p in profiles if p)
56
- allowed_set = set(a.strip() for a in allowed_profiles) if allowed_profiles is not None else None
57
69
 
70
+ allowed_set = set(a.strip() for a in allowed_profiles) if allowed_profiles is not None else None
58
71
  if allowed_set is not None:
59
72
  unknown = set(active) - allowed_set
60
73
  if unknown:
61
74
  raise ConfigurationError(f"Unknown profiles: {sorted(unknown)}; allowed: {sorted(allowed_set)}")
62
-
75
+
63
76
  factory = ComponentFactory()
64
77
  caches = ScopedCaches()
65
78
  scopes = ScopeManager()
66
-
67
79
  if custom_scopes:
68
- for n, impl in custom_scopes.items():
69
- scopes.register_scope(n, impl)
80
+ for name in custom_scopes:
81
+ scopes.register_scope(name)
70
82
 
71
83
  pico = PicoContainer(factory, caches, scopes, container_id=container_id, profiles=active, observers=observers or [])
72
84
  registrar = Registrar(factory, profiles=active, environ=environ, logger=logger, config=config)
73
-
74
85
  for m in _iter_input_modules(modules):
75
86
  registrar.register_module(m)
76
87
 
@@ -79,8 +90,7 @@ def init(modules: Union[Any, Iterable[Any]], *, profiles: Tuple[str, ...] = (),
79
90
  prov, _ = _normalize_override_provider(v)
80
91
  factory.bind(k, prov)
81
92
 
82
- registrar.finalize(overrides)
83
-
93
+ registrar.finalize(overrides, pico_instance=pico)
84
94
  if validate_only:
85
95
  locator = registrar.locator()
86
96
  pico.attach_locator(locator)
@@ -91,13 +101,39 @@ def init(modules: Union[Any, Iterable[Any]], *, profiles: Tuple[str, ...] = (),
91
101
  registrar.attach_runtime(pico, locator)
92
102
  pico.attach_locator(locator)
93
103
  _fail_fast_cycle_check(pico)
104
+
105
+ if not validate_only:
106
+ eager_singletons = []
107
+ for key, md in locator._metadata.items():
108
+ if md.scope == "singleton" and not md.lazy:
109
+ cache = pico._cache_for(key)
110
+ instance = cache.get(key)
111
+ if instance is None:
112
+ instance = pico.get(key)
113
+ eager_singletons.append(instance)
114
+ else:
115
+ eager_singletons.append(instance)
116
+
117
+ configure_awaitables = []
118
+ for instance in eager_singletons:
119
+ res = pico._run_configure_methods(instance)
120
+ if inspect.isawaitable(res):
121
+ configure_awaitables.append(res)
122
+
123
+ if configure_awaitables:
124
+ raise ConfigurationError(
125
+ "Sync init() found eagerly loaded singletons with async @configure methods. "
126
+ "This can be caused by an async __ainit__ or async @configure. "
127
+ "Use an async main function and await pico.aget() for those components, "
128
+ "or mark them as lazy=True."
129
+ )
130
+
94
131
  return pico
95
132
 
96
133
  def _find_cycle(graph: Dict[KeyT, Tuple[KeyT, ...]]) -> Optional[Tuple[KeyT, ...]]:
97
134
  temp: Set[KeyT] = set()
98
135
  perm: Set[KeyT] = set()
99
136
  stack: List[KeyT] = []
100
-
101
137
  def visit(n: KeyT) -> Optional[Tuple[KeyT, ...]]:
102
138
  if n in perm:
103
139
  return None
@@ -107,20 +143,16 @@ def _find_cycle(graph: Dict[KeyT, Tuple[KeyT, ...]]) -> Optional[Tuple[KeyT, ...
107
143
  return tuple(stack[idx:] + [n])
108
144
  except ValueError:
109
145
  return tuple([n, n])
110
-
111
146
  temp.add(n)
112
147
  stack.append(n)
113
-
114
148
  for m in graph.get(n, ()):
115
149
  c = visit(m)
116
150
  if c:
117
151
  return c
118
-
119
152
  stack.pop()
120
153
  temp.remove(n)
121
154
  perm.add(n)
122
155
  return None
123
-
124
156
  for node in graph.keys():
125
157
  c = visit(node)
126
158
  if c:
@@ -3,13 +3,9 @@ import json
3
3
  from dataclasses import dataclass
4
4
  from typing import Any, Optional, Protocol, Mapping, List, Tuple, Dict, Union
5
5
 
6
- from .config_runtime import TreeSource, DictSource, JsonTreeSource, YamlTreeSource
6
+ from .config_runtime import TreeSource, DictSource, JsonTreeSource, YamlTreeSource, Value
7
7
  from .exceptions import ConfigurationError
8
8
 
9
- class Value:
10
- def __init__(self, value: Any):
11
- self.value = value
12
-
13
9
  class ConfigSource(Protocol):
14
10
  pass
15
11
 
@@ -4,7 +4,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union, get_args,
4
4
  from .constants import PICO_INFRA, PICO_NAME, PICO_META
5
5
  from .exceptions import ConfigurationError
6
6
  from .factory import ProviderMetadata, DeferredProvider
7
- from .config_builder import ContextConfig, ConfigSource, FlatDictSource
7
+ from .config_builder import ContextConfig, ConfigSource, FlatDictSource, Value
8
8
  from .config_runtime import ConfigResolver, TypeAdapterRegistry, ObjectGraphBuilder, TreeSource
9
9
  from .analysis import analyze_callable_dependencies, DependencyRequest
10
10
 
@@ -68,6 +68,22 @@ class ConfigurationManager:
68
68
  raise ConfigurationError(f"Configuration class {getattr(cls, '__name__', str(cls))} must be a dataclass")
69
69
  values: Dict[str, Any] = {}
70
70
  for f in fields(cls):
71
+ field_type = f.type
72
+ value_override = None
73
+
74
+ if get_origin(field_type) is Annotated:
75
+ args = get_args(field_type)
76
+ field_type = args[0] if args else Any
77
+ metas = args[1:] if len(args) > 1 else ()
78
+ for m in metas:
79
+ if isinstance(m, Value):
80
+ value_override = m.value
81
+ break
82
+
83
+ if value_override is not None:
84
+ values[f.name] = value_override
85
+ continue
86
+
71
87
  base_key = _upper_key(f.name)
72
88
  keys_to_try = []
73
89
  if prefix:
@@ -84,7 +100,7 @@ class ConfigurationManager:
84
100
  if f.default is not MISSING or f.default_factory is not MISSING:
85
101
  continue
86
102
  raise ConfigurationError(f"Missing configuration key: {(prefix or '') + base_key}")
87
- values[f.name] = _coerce(raw, f.type if isinstance(f.type, type) or get_origin(f.type) else str)
103
+ values[f.name] = _coerce(raw, field_type if isinstance(field_type, type) or get_origin(field_type) else str)
88
104
  return cls(**values)
89
105
 
90
106
  def _auto_detect_mapping(self, target_type: type) -> str:
@@ -159,7 +175,7 @@ class ConfigurationManager:
159
175
  factory_method=None,
160
176
  qualifiers=qset,
161
177
  primary=True,
162
- lazy=False,
178
+ lazy=bool(meta.get("lazy", False)),
163
179
  infra="configured",
164
180
  pico_name=prefix,
165
181
  scope=sc,
@@ -180,7 +196,7 @@ class ConfigurationManager:
180
196
  factory_method=None,
181
197
  qualifiers=qset,
182
198
  primary=True,
183
- lazy=False,
199
+ lazy=bool(meta.get("lazy", False)),
184
200
  infra="configured",
185
201
  pico_name=prefix,
186
202
  scope=sc,
@@ -217,3 +233,4 @@ class ConfigurationManager:
217
233
  prefix = md.pico_name or ""
218
234
  keys = [_upper_key(f.name) for f in fields(target_type)]
219
235
  return any(self._lookup_flat(prefix + k) is not None for k in keys)
236
+
@@ -8,6 +8,10 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple, Union, g
8
8
  from .exceptions import ConfigurationError
9
9
  from .constants import PICO_META
10
10
 
11
+ class Value:
12
+ def __init__(self, value: Any):
13
+ self.value = value
14
+
11
15
  class Discriminator:
12
16
  def __init__(self, name: str):
13
17
  self.name = name
@@ -160,8 +164,8 @@ class ObjectGraphBuilder:
160
164
  org = get_origin(t)
161
165
 
162
166
  if org is Annotated:
163
- base, meta = self._split_annotated(t)
164
- return self._build_discriminated(node, base, meta, path)
167
+ base, metas = self._split_annotated(t)
168
+ return self._build_discriminated(node, base, metas, path)
165
169
 
166
170
  if org in (list, List):
167
171
  elem_t = get_args(t)[0] if get_args(t) else Any
@@ -255,20 +259,42 @@ class ObjectGraphBuilder:
255
259
 
256
260
  def _build_discriminated(self, node: Any, base: Any, metas: Tuple[Any, ...], path: Tuple[str, ...]) -> Any:
257
261
  disc_name = None
262
+ disc_value = None
263
+ has_value = False
264
+
258
265
  for m in metas:
259
266
  if isinstance(m, Discriminator):
260
267
  disc_name = m.name
261
- break
262
-
263
- if disc_name and isinstance(node, dict) and disc_name in node:
264
- if get_origin(base) is Union:
265
- tn = str(node[disc_name])
266
- for cand in get_args(base):
267
- if isinstance(cand, type) and getattr(cand, "__name__", "") == tn:
268
- cleaned = {k: v for k, v in node.items() if k != disc_name}
269
- return self._build(cleaned, cand, path)
270
- raise ConfigurationError(f"Discriminator {disc_name} did not match at {'.'.join(path)}")
271
-
268
+ if isinstance(m, Value):
269
+ disc_value = m.value
270
+ has_value = True
271
+
272
+ tn: Optional[str] = None
273
+
274
+ if disc_name and has_value:
275
+ tn = str(disc_value)
276
+ elif disc_name and isinstance(node, dict) and disc_name in node:
277
+ tn = str(node[disc_name])
278
+
279
+ if tn is not None and get_origin(base) is Union:
280
+ for cand in get_args(base):
281
+ if isinstance(cand, type) and getattr(cand, "__name__", "") == tn:
282
+
283
+ cleaned_node = {k: v for k, v in node.items() if k != disc_name}
284
+
285
+ if has_value:
286
+ cleaned_node[disc_name] = tn
287
+
288
+ return self._build(cleaned_node, cand, path)
289
+
290
+ raise ConfigurationError(
291
+ f"Discriminator value '{tn}' for field '{disc_name}' "
292
+ f"did not match any type in Union {base} at {'.'.join(path)}"
293
+ )
294
+
295
+ if has_value and not disc_name:
296
+ return disc_value
297
+
272
298
  return self._build(node, base, path)
273
299
 
274
300
  def _coerce_prim(self, node: Any, t: type, path: Tuple[str, ...]) -> Any:
pico_ioc/container.py CHANGED
@@ -1,10 +1,12 @@
1
+ # src/pico_ioc/container.py
2
+
1
3
  import inspect
2
4
  import contextvars
3
5
  import functools
4
6
  from typing import Any, Dict, List, Optional, Tuple, overload, Union, Callable, Iterable, Set, get_args, get_origin, Annotated, Protocol, Mapping
5
7
  from contextlib import contextmanager
6
8
  from .constants import LOGGER, PICO_META
7
- from .exceptions import ComponentCreationError, ProviderNotFoundError, AsyncResolutionError
9
+ from .exceptions import ComponentCreationError, ProviderNotFoundError, AsyncResolutionError, ConfigurationError
8
10
  from .factory import ComponentFactory, ProviderMetadata
9
11
  from .locator import ComponentLocator
10
12
  from .scope import ScopedCaches, ScopeManager
@@ -195,6 +197,26 @@ class PicoContainer:
195
197
  finally:
196
198
  self.deactivate(token_container)
197
199
 
200
+ def _run_configure_methods(self, instance: Any) -> Any:
201
+ if not _needs_async_configure(instance):
202
+ for m in _iter_configure_methods(instance):
203
+ configure_deps = analyze_callable_dependencies(m)
204
+ args = self._resolve_args(configure_deps)
205
+ res = m(**args)
206
+ if inspect.isawaitable(res):
207
+ LOGGER.warning(f"Async configure method {m} called during sync get. Awaitable ignored.")
208
+ return instance
209
+
210
+ async def runner():
211
+ for m in _iter_configure_methods(instance):
212
+ configure_deps = analyze_callable_dependencies(m)
213
+ args = self._resolve_args(configure_deps)
214
+ r = m(**args)
215
+ if inspect.isawaitable(r):
216
+ await r
217
+ return instance
218
+ return runner()
219
+
198
220
  @overload
199
221
  def get(self, key: type) -> Any: ...
200
222
  @overload
@@ -210,6 +232,14 @@ class PicoContainer:
210
232
  key_name = getattr(key, '__name__', str(key))
211
233
  raise AsyncResolutionError(key)
212
234
 
235
+ md = self._locator._metadata.get(key) if self._locator else None
236
+ scope = (md.scope if md else "singleton")
237
+ if scope != "singleton":
238
+ instance_or_awaitable_configured = self._run_configure_methods(instance)
239
+ if inspect.isawaitable(instance_or_awaitable_configured):
240
+ raise AsyncResolutionError(key)
241
+ instance = instance_or_awaitable_configured
242
+
213
243
  final_instance = self._maybe_wrap_with_aspects(key, instance)
214
244
  cache = self._cache_for(key)
215
245
  cache.put(key, final_instance)
@@ -228,6 +258,15 @@ class PicoContainer:
228
258
  if inspect.isawaitable(instance_or_awaitable):
229
259
  instance = await instance_or_awaitable
230
260
 
261
+ md = self._locator._metadata.get(key) if self._locator else None
262
+ scope = (md.scope if md else "singleton")
263
+ if scope != "singleton":
264
+ instance_or_awaitable_configured = self._run_configure_methods(instance)
265
+ if inspect.isawaitable(instance_or_awaitable_configured):
266
+ instance = await instance_or_awaitable_configured
267
+ else:
268
+ instance = instance_or_awaitable_configured
269
+
231
270
  final_instance = self._maybe_wrap_with_aspects(key, instance)
232
271
  cache = self._cache_for(key)
233
272
  cache.put(key, final_instance)
@@ -432,7 +471,7 @@ class PicoContainer:
432
471
  inst = cls(**deps)
433
472
 
434
473
  ainit = getattr(inst, "__ainit__", None)
435
- has_async = (callable(ainit) and inspect.iscoroutinefunction(ainit)) or _needs_async_configure(inst)
474
+ has_async = (callable(ainit) and inspect.iscoroutinefunction(ainit))
436
475
 
437
476
  if has_async:
438
477
  async def runner():
@@ -446,39 +485,12 @@ class PicoContainer:
446
485
  res = ainit(**kwargs)
447
486
  if inspect.isawaitable(res):
448
487
  await res
449
- for m in _iter_configure_methods(inst):
450
- configure_deps = analyze_callable_dependencies(m)
451
- args = self._resolve_args(configure_deps)
452
- r = m(**args)
453
- if inspect.isawaitable(r):
454
- await r
455
488
  return inst
456
489
  return runner()
457
490
 
458
- for m in _iter_configure_methods(inst):
459
- configure_deps = analyze_callable_dependencies(m)
460
- args = self._resolve_args(configure_deps)
461
- m(**args)
462
491
  return inst
463
492
 
464
493
  def build_method(self, fn: Callable[..., Any], locator: ComponentLocator, dependencies: Tuple[DependencyRequest, ...]) -> Any:
465
494
  deps = self._resolve_args(dependencies)
466
495
  obj = fn(**deps)
467
-
468
- has_async = _needs_async_configure(obj)
469
- if has_async:
470
- async def runner():
471
- for m in _iter_configure_methods(obj):
472
- configure_deps = analyze_callable_dependencies(m)
473
- args = self._resolve_args(configure_deps)
474
- r = m(**args)
475
- if inspect.isawaitable(r):
476
- await r
477
- return obj
478
- return runner()
479
-
480
- for m in _iter_configure_methods(obj):
481
- configure_deps = analyze_callable_dependencies(m)
482
- args = self._resolve_args(configure_deps)
483
- m(**args)
484
496
  return obj
pico_ioc/decorators.py CHANGED
@@ -172,13 +172,14 @@ def cleanup(fn):
172
172
  m["cleanup"] = True
173
173
  return fn
174
174
 
175
- def configured(target: Any, *, prefix: str = "", mapping: str = "auto"):
175
+ def configured(target: Any = "self", *, prefix: str = "", mapping: str = "auto", **kwargs):
176
176
  if mapping not in ("auto", "flat", "tree"):
177
177
  raise ValueError("mapping must be one of 'auto', 'flat', or 'tree'")
178
178
  def dec(cls):
179
179
  setattr(cls, PICO_INFRA, "configured")
180
180
  m = _meta_get(cls)
181
181
  m["configured"] = {"target": target, "prefix": prefix, "mapping": mapping}
182
+ _apply_common_metadata(cls, **kwargs)
182
183
  return cls
183
184
  return dec
184
185
 
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/locator.py CHANGED
@@ -115,10 +115,6 @@ class ComponentLocator:
115
115
  return k
116
116
  return None
117
117
 
118
- def _compile_argplan_static(self, callable_obj):
119
- raise NotImplementedError("This method is obsolete and replaced by analysis module")
120
-
121
-
122
118
  def dependency_keys_for_static(self, md: ProviderMetadata):
123
119
  deps: List[KeyT] = []
124
120
  for dep in md.dependencies:
pico_ioc/registrar.py CHANGED
@@ -17,6 +17,7 @@ from .provider_selector import ProviderSelector
17
17
  from .dependency_validator import DependencyValidator
18
18
  from .component_scanner import ComponentScanner
19
19
  from .analysis import analyze_callable_dependencies, DependencyRequest
20
+ from .container import PicoContainer
20
21
 
21
22
  KeyT = Union[str, type]
22
23
  Provider = Callable[[], Any]
@@ -133,7 +134,7 @@ class Registrar:
133
134
  add("pico_name", md.pico_name, k)
134
135
 
135
136
 
136
- def finalize(self, overrides: Optional[Dict[KeyT, Any]]) -> None:
137
+ def finalize(self, overrides: Optional[Dict[KeyT, Any]], *, pico_instance: PicoContainer) -> None:
137
138
  candidates, on_missing, deferred_providers, provides_functions = self._scanner.get_scan_results()
138
139
  self._deferred = deferred_providers
139
140
  self._provides_functions = provides_functions
@@ -142,6 +143,24 @@ class Registrar:
142
143
  for key, (provider, md) in winners.items():
143
144
  self._bind_if_absent(key, provider)
144
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
+ )
145
164
 
146
165
  self._promote_scopes()
147
166
  self._rebuild_indexes()
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pico-ioc
3
- Version: 2.1.0
3
+ Version: 2.1.1
4
4
  Summary: A minimalist, zero-dependency Inversion of Control (IoC) container for Python.
5
5
  Author-email: David Perez Cabrera <dperezcabrera@gmail.com>
6
6
  License: MIT License
@@ -39,7 +39,7 @@ Classifier: Programming Language :: Python :: 3.13
39
39
  Classifier: Programming Language :: Python :: 3.14
40
40
  Classifier: License :: OSI Approved :: MIT License
41
41
  Classifier: Operating System :: OS Independent
42
- Requires-Python: >=3.8
42
+ Requires-Python: >=3.10
43
43
  Description-Content-Type: text/markdown
44
44
  License-File: LICENSE
45
45
  Provides-Extra: yaml
@@ -0,0 +1,25 @@
1
+ pico_ioc/__init__.py,sha256=i25Obx7aH_Oy5b6yjjnCswDgni7InIjrGEcG6vLAw6I,2414
2
+ pico_ioc/_version.py,sha256=Aht2295j8FswZ-nPYofCYr3fBZ6Uyf0thTfl5Oc2mWA,22
3
+ pico_ioc/analysis.py,sha256=k49R-HcDyvpSNid8mxv7Fc6fPHnDu1C_b4HxrGLNF2g,2780
4
+ pico_ioc/aop.py,sha256=XcyzsuKPrVPk1_Jad7Mn-qwoL1y0ZuVWwRZBA-CslJk,13301
5
+ pico_ioc/api.py,sha256=0pcRFHzhDcX8ijd67xAsVrTejwXuJKz7kTKRUrIuX2s,6161
6
+ pico_ioc/component_scanner.py,sha256=S-9XNxrgyq_JFdc4Uqn2bEb-HxafSgIWylIurxyN_UA,7955
7
+ pico_ioc/config_builder.py,sha256=7kcYIq1Yrb46Tic7uLeaCDvLA-Sa_p1PIoGF00mivso,2848
8
+ pico_ioc/config_registrar.py,sha256=34iNQY1TUEPTXbb-QV1T-c5VKAn18hBcNt5MLhzDSfY,8456
9
+ pico_ioc/config_runtime.py,sha256=hiL1kCxhpjbfOdUaH71jMGNESDpWsaJkQXh7q1T71bg,12781
10
+ pico_ioc/constants.py,sha256=AhIt0ieDZ9Turxb_YbNzj11wUbBbzjKfWh1BDlSx2Nw,183
11
+ pico_ioc/container.py,sha256=gXgQT12ChpexbZALUfb0YYohlcRbUUeJ8-ltdR7xitc,18956
12
+ pico_ioc/decorators.py,sha256=ru_YeqyJ3gbfb6M8WeJZlBxfcBBEuGDvxpHJGzU6FIs,6412
13
+ pico_ioc/dependency_validator.py,sha256=BIR6pKntACiabF6CjNZ3m00RMnet9BPK1_9y1iCJ5KQ,4144
14
+ pico_ioc/event_bus.py,sha256=nOL91JLYxap9kbb-HBGEhOVwtXN_bfI4q0mtSRZFlHk,8434
15
+ pico_ioc/exceptions.py,sha256=FBuajj5g29hAGODt2tAWuy2sG5mQojdSddaqFzim-aY,2383
16
+ pico_ioc/factory.py,sha256=oJXx_BYJuvV8oxYzs5I3gx9WM6uLYZ8GCc43gukNanc,1671
17
+ pico_ioc/locator.py,sha256=4WN1qvXXW_LRInB2XJR8pTgIuJ8RyWBSpVo28HwtlL0,4737
18
+ pico_ioc/provider_selector.py,sha256=pU7NbI5vifvUlJEjlRJmvveQUZVD47T24QmiP0CHRw0,1213
19
+ pico_ioc/registrar.py,sha256=hIk48nXghTdA3WBljCbw2q8J_6F_hCk1ljSi4Pb8P3A,8368
20
+ pico_ioc/scope.py,sha256=hOdTmjjfrRt8APXoS3lbTbSPxILi7flBXz_qpIkpoKw,6137
21
+ pico_ioc-2.1.1.dist-info/licenses/LICENSE,sha256=N1_nOvHTM6BobYnOTNXiQkroDqCEi6EzfGBv8lWtyZ0,1077
22
+ pico_ioc-2.1.1.dist-info/METADATA,sha256=deaBUX6MOzClrMVDDhk5siAh2W0QrtQI-w4n2ZI-gGI,10673
23
+ pico_ioc-2.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
+ pico_ioc-2.1.1.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
25
+ pico_ioc-2.1.1.dist-info/RECORD,,
@@ -1,25 +0,0 @@
1
- pico_ioc/__init__.py,sha256=pxYvrunT9cFAXxGbow5TRlzRXDVvm4E7L6PT1UkPeUw,2394
2
- pico_ioc/_version.py,sha256=LbU43-7hsLmdXWI0wTAl3y6D3Tr7CbJiDOLXwl7clj8,22
3
- pico_ioc/analysis.py,sha256=k49R-HcDyvpSNid8mxv7Fc6fPHnDu1C_b4HxrGLNF2g,2780
4
- pico_ioc/aop.py,sha256=r8JCPsTTF1hmrQMtnj-_Lj99fIy5J97RAJjiR2LjVPQ,12769
5
- pico_ioc/api.py,sha256=tR0pm6YEnDTA62EIT_Cpw1EaPfdnBFY9Dvpc38ypP-o,5061
6
- pico_ioc/component_scanner.py,sha256=S-9XNxrgyq_JFdc4Uqn2bEb-HxafSgIWylIurxyN_UA,7955
7
- pico_ioc/config_builder.py,sha256=ROBvbnm2Zv8apRdtZJHtebp-2cOMiuENFyiqaX1T2Ik,2918
8
- pico_ioc/config_registrar.py,sha256=8Xpl-CNRFny3EIRlcNx6oMJQldXkj_jdaPbC17qk2Ec,7803
9
- pico_ioc/config_runtime.py,sha256=Q8jVFQ1b6h1ZOkdacm70u0Q7TlEiUGENFkG-YWRkBxA,12133
10
- pico_ioc/constants.py,sha256=AhIt0ieDZ9Turxb_YbNzj11wUbBbzjKfWh1BDlSx2Nw,183
11
- pico_ioc/container.py,sha256=1gRjHH5kq_lOlnGUh5QYA4vUHF3ScQpwXoT9PWiyJ2g,18352
12
- pico_ioc/decorators.py,sha256=qrx4oMjRmmgwqT4cdp6FxBoqex3L1yNGlCUxO865z14,6347
13
- pico_ioc/dependency_validator.py,sha256=BIR6pKntACiabF6CjNZ3m00RMnet9BPK1_9y1iCJ5KQ,4144
14
- pico_ioc/event_bus.py,sha256=E8Qb8KZ6K1CuXSbMlG0MNPHkGoWlssLLPzHq1QYdADQ,8346
15
- pico_ioc/exceptions.py,sha256=FBuajj5g29hAGODt2tAWuy2sG5mQojdSddaqFzim-aY,2383
16
- pico_ioc/factory.py,sha256=oJXx_BYJuvV8oxYzs5I3gx9WM6uLYZ8GCc43gukNanc,1671
17
- pico_ioc/locator.py,sha256=UDyXl3oSK-sZ8ouOlhsDM91shMP0atMxyMoQlaSYvfE,4885
18
- pico_ioc/provider_selector.py,sha256=pU7NbI5vifvUlJEjlRJmvveQUZVD47T24QmiP0CHRw0,1213
19
- pico_ioc/registrar.py,sha256=cexXFDsuxBwM8FvW7mwmuyqcTcOkSP6ELecwklcs0F4,7625
20
- pico_ioc/scope.py,sha256=GDsDJWw7e5Vpiys-M4vQfKMJWSCiorRsT5cPo6z34Mk,5924
21
- pico_ioc-2.1.0.dist-info/licenses/LICENSE,sha256=N1_nOvHTM6BobYnOTNXiQkroDqCEi6EzfGBv8lWtyZ0,1077
22
- pico_ioc-2.1.0.dist-info/METADATA,sha256=lYR54Lhdwmn5tDXol2M5ReJxBsXpdZ9bH0KrcYOlq2s,10672
23
- pico_ioc-2.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
- pico_ioc-2.1.0.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
25
- pico_ioc-2.1.0.dist-info/RECORD,,