pico-ioc 2.1.0__py3-none-any.whl → 2.1.2__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.2'
pico_ioc/analysis.py CHANGED
@@ -1,6 +1,12 @@
1
1
  import inspect
2
2
  from dataclasses import dataclass
3
- from typing import Any, Callable, List, Optional, Tuple, Union, get_args, get_origin, Annotated
3
+ import collections
4
+ import collections.abc
5
+ from typing import (
6
+ Any, Callable, List, Optional, Tuple, Union, get_args, get_origin, Annotated,
7
+ Iterable, Set, Sequence, Collection, Deque, FrozenSet, MutableSequence, MutableSet,
8
+ Dict, Mapping
9
+ )
4
10
  from .decorators import Qualifier
5
11
 
6
12
  KeyT = Union[str, type]
@@ -12,6 +18,8 @@ class DependencyRequest:
12
18
  is_list: bool = False
13
19
  qualifier: Optional[str] = None
14
20
  is_optional: bool = False
21
+ is_dict: bool = False
22
+ dict_key_type: Any = None
15
23
 
16
24
  def _extract_annotated(ann: Any) -> Tuple[Any, Optional[str]]:
17
25
  qualifier = None
@@ -44,6 +52,24 @@ def analyze_callable_dependencies(callable_obj: Callable[..., Any]) -> Tuple[Dep
44
52
 
45
53
  plan: List[DependencyRequest] = []
46
54
 
55
+ SUPPORTED_COLLECTION_ORIGINS = (
56
+ # Runtime types
57
+ list,
58
+ set,
59
+ tuple,
60
+ frozenset,
61
+ collections.deque,
62
+
63
+ # Typing ABCs (from get_origin)
64
+ collections.abc.Iterable,
65
+ collections.abc.Collection,
66
+ collections.abc.Sequence,
67
+ collections.abc.MutableSequence,
68
+ collections.abc.MutableSet
69
+ )
70
+
71
+ SUPPORTED_DICT_ORIGINS = (dict, collections.abc.Mapping)
72
+
47
73
  for name, param in sig.parameters.items():
48
74
  if name in ("self", "cls"):
49
75
  continue
@@ -56,19 +82,35 @@ def analyze_callable_dependencies(callable_obj: Callable[..., Any]) -> Tuple[Dep
56
82
  base_type, qualifier = _extract_annotated(base_type)
57
83
 
58
84
  is_list = False
85
+ is_dict = False
59
86
  elem_t = None
87
+ dict_key_t = None
60
88
 
61
89
  origin = get_origin(base_type)
62
- if origin in (list, List):
90
+
91
+ if origin in SUPPORTED_COLLECTION_ORIGINS:
63
92
  is_list = True
64
93
  elem_t = get_args(base_type)[0] if get_args(base_type) else Any
65
94
  elem_t, list_qualifier = _extract_annotated(elem_t)
66
95
  if qualifier is None:
67
96
  qualifier = list_qualifier
97
+ elif origin in SUPPORTED_DICT_ORIGINS:
98
+ is_dict = True
99
+ args = get_args(base_type)
100
+ dict_key_t = args[0] if args else Any
101
+ elem_t = args[1] if len(args) > 1 else Any
102
+ elem_t, dict_qualifier = _extract_annotated(elem_t)
103
+ if qualifier is None:
104
+ qualifier = dict_qualifier
68
105
 
69
106
  final_key: KeyT
107
+ final_dict_key_type: Any = None
108
+
70
109
  if is_list:
71
110
  final_key = elem_t if isinstance(elem_t, type) else Any
111
+ elif is_dict:
112
+ final_key = elem_t if isinstance(elem_t, type) else Any
113
+ final_dict_key_type = dict_key_t
72
114
  elif isinstance(base_type, type):
73
115
  final_key = base_type
74
116
  elif isinstance(base_type, str):
@@ -84,9 +126,10 @@ def analyze_callable_dependencies(callable_obj: Callable[..., Any]) -> Tuple[Dep
84
126
  key=final_key,
85
127
  is_list=is_list,
86
128
  qualifier=qualifier,
87
- is_optional=is_optional or (param.default is not inspect._empty)
129
+ is_optional=is_optional or (param.default is not inspect._empty),
130
+ is_dict=is_dict,
131
+ dict_key_type=final_dict_key_type
88
132
  )
89
133
  )
90
134
 
91
135
  return tuple(plan)
92
-
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,13 @@
1
1
  import inspect
2
2
  import contextvars
3
3
  import functools
4
- from typing import Any, Dict, List, Optional, Tuple, overload, Union, Callable, Iterable, Set, get_args, get_origin, Annotated, Protocol, Mapping
4
+ from typing import (
5
+ Any, Dict, List, Optional, Tuple, overload, Union, Callable,
6
+ Iterable, Set, get_args, get_origin, Annotated, Protocol, Mapping, Type
7
+ )
5
8
  from contextlib import contextmanager
6
9
  from .constants import LOGGER, PICO_META
7
- from .exceptions import ComponentCreationError, ProviderNotFoundError, AsyncResolutionError
10
+ from .exceptions import ComponentCreationError, ProviderNotFoundError, AsyncResolutionError, ConfigurationError
8
11
  from .factory import ComponentFactory, ProviderMetadata
9
12
  from .locator import ComponentLocator
10
13
  from .scope import ScopedCaches, ScopeManager
@@ -195,6 +198,26 @@ class PicoContainer:
195
198
  finally:
196
199
  self.deactivate(token_container)
197
200
 
201
+ def _run_configure_methods(self, instance: Any) -> Any:
202
+ if not _needs_async_configure(instance):
203
+ for m in _iter_configure_methods(instance):
204
+ configure_deps = analyze_callable_dependencies(m)
205
+ args = self._resolve_args(configure_deps)
206
+ res = m(**args)
207
+ if inspect.isawaitable(res):
208
+ LOGGER.warning(f"Async configure method {m} called during sync get. Awaitable ignored.")
209
+ return instance
210
+
211
+ async def runner():
212
+ for m in _iter_configure_methods(instance):
213
+ configure_deps = analyze_callable_dependencies(m)
214
+ args = self._resolve_args(configure_deps)
215
+ r = m(**args)
216
+ if inspect.isawaitable(r):
217
+ await r
218
+ return instance
219
+ return runner()
220
+
198
221
  @overload
199
222
  def get(self, key: type) -> Any: ...
200
223
  @overload
@@ -210,6 +233,14 @@ class PicoContainer:
210
233
  key_name = getattr(key, '__name__', str(key))
211
234
  raise AsyncResolutionError(key)
212
235
 
236
+ md = self._locator._metadata.get(key) if self._locator else None
237
+ scope = (md.scope if md else "singleton")
238
+ if scope != "singleton":
239
+ instance_or_awaitable_configured = self._run_configure_methods(instance)
240
+ if inspect.isawaitable(instance_or_awaitable_configured):
241
+ raise AsyncResolutionError(key)
242
+ instance = instance_or_awaitable_configured
243
+
213
244
  final_instance = self._maybe_wrap_with_aspects(key, instance)
214
245
  cache = self._cache_for(key)
215
246
  cache.put(key, final_instance)
@@ -228,6 +259,15 @@ class PicoContainer:
228
259
  if inspect.isawaitable(instance_or_awaitable):
229
260
  instance = await instance_or_awaitable
230
261
 
262
+ md = self._locator._metadata.get(key) if self._locator else None
263
+ scope = (md.scope if md else "singleton")
264
+ if scope != "singleton":
265
+ instance_or_awaitable_configured = self._run_configure_methods(instance)
266
+ if inspect.isawaitable(instance_or_awaitable_configured):
267
+ instance = await instance_or_awaitable_configured
268
+ else:
269
+ instance = instance_or_awaitable_configured
270
+
231
271
  final_instance = self._maybe_wrap_with_aspects(key, instance)
232
272
  cache = self._cache_for(key)
233
273
  cache.put(key, final_instance)
@@ -394,19 +434,61 @@ class PicoContainer:
394
434
 
395
435
  def _resolve_args(self, dependencies: Tuple[DependencyRequest, ...]) -> Dict[str, Any]:
396
436
  kwargs: Dict[str, Any] = {}
397
- if not dependencies:
437
+ if not dependencies or self._locator is None:
398
438
  return kwargs
399
439
 
400
440
  for dep in dependencies:
401
441
  if dep.is_list:
402
442
  keys: Tuple[KeyT, ...] = ()
403
- if self._locator is not None and isinstance(dep.key, type):
443
+ if isinstance(dep.key, type):
404
444
  keys = tuple(self._locator.collect_by_type(dep.key, dep.qualifier))
405
445
  kwargs[dep.parameter_name] = [self.get(k) for k in keys]
406
446
  continue
447
+
448
+ if dep.is_dict:
449
+ value_type = dep.key
450
+ key_type = dep.dict_key_type
451
+ result_map: Dict[Any, Any] = {}
452
+
453
+ keys_to_resolve: Tuple[KeyT, ...] = ()
454
+ if isinstance(value_type, type):
455
+ keys_to_resolve = tuple(self._locator.collect_by_type(value_type, dep.qualifier))
456
+
457
+ for comp_key in keys_to_resolve:
458
+ instance = self.get(comp_key)
459
+ md = self._locator._metadata.get(comp_key)
460
+ if md is None:
461
+ continue
462
+
463
+ dict_key: Any = None
464
+ if key_type is str:
465
+ dict_key = md.pico_name
466
+ if dict_key is None:
467
+ if isinstance(comp_key, str):
468
+ dict_key = comp_key
469
+ else:
470
+ dict_key = getattr(comp_key, "__name__", str(comp_key))
471
+ elif key_type is type or key_type is Type:
472
+ dict_key = md.concrete_class or md.provided_type
473
+ elif key_type is Any:
474
+ dict_key = md.pico_name
475
+ if dict_key is None:
476
+ if isinstance(comp_key, str):
477
+ dict_key = comp_key
478
+ else:
479
+ dict_key = getattr(comp_key, "__name__", str(comp_key))
480
+
481
+ if dict_key is not None:
482
+ if (key_type is type or key_type is Type) and not isinstance(dict_key, type):
483
+ continue
484
+
485
+ result_map[dict_key] = instance
486
+
487
+ kwargs[dep.parameter_name] = result_map
488
+ continue
407
489
 
408
490
  primary_key = dep.key
409
- if isinstance(primary_key, str) and self._locator is not None:
491
+ if isinstance(primary_key, str):
410
492
  mapped = self._locator.find_key_by_name(primary_key)
411
493
  primary_key = mapped if mapped is not None else primary_key
412
494
 
@@ -432,7 +514,7 @@ class PicoContainer:
432
514
  inst = cls(**deps)
433
515
 
434
516
  ainit = getattr(inst, "__ainit__", None)
435
- has_async = (callable(ainit) and inspect.iscoroutinefunction(ainit)) or _needs_async_configure(inst)
517
+ has_async = (callable(ainit) and inspect.iscoroutinefunction(ainit))
436
518
 
437
519
  if has_async:
438
520
  async def runner():
@@ -446,39 +528,12 @@ class PicoContainer:
446
528
  res = ainit(**kwargs)
447
529
  if inspect.isawaitable(res):
448
530
  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
531
  return inst
456
532
  return runner()
457
533
 
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
534
  return inst
463
535
 
464
536
  def build_method(self, fn: Callable[..., Any], locator: ComponentLocator, dependencies: Tuple[DependencyRequest, ...]) -> Any:
465
537
  deps = self._resolve_args(dependencies)
466
538
  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
539
  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
@@ -80,9 +80,13 @@ class ComponentLocator:
80
80
  return isinstance(inst, proto)
81
81
  except Exception:
82
82
  pass
83
+
83
84
  for name, val in proto.__dict__.items():
84
- if name.startswith("_") or not callable(val):
85
+ if name.startswith("_") or not (callable(val) or name in getattr(proto, "__annotations__", {})):
85
86
  continue
87
+
88
+ if not hasattr(typ, name):
89
+ return False
86
90
  return True
87
91
 
88
92
  def collect_by_type(self, t: type, q: Optional[str]) -> List[KeyT]:
@@ -115,10 +119,6 @@ class ComponentLocator:
115
119
  return k
116
120
  return None
117
121
 
118
- def _compile_argplan_static(self, callable_obj):
119
- raise NotImplementedError("This method is obsolete and replaced by analysis module")
120
-
121
-
122
122
  def dependency_keys_for_static(self, md: ProviderMetadata):
123
123
  deps: List[KeyT] = []
124
124
  for dep in md.dependencies:
@@ -126,6 +126,10 @@ class ComponentLocator:
126
126
  if isinstance(dep.key, type):
127
127
  keys = self.collect_by_type(dep.key, dep.qualifier)
128
128
  deps.extend(keys)
129
+ elif dep.is_dict:
130
+ if isinstance(dep.key, type):
131
+ keys = self.collect_by_type(dep.key, dep.qualifier)
132
+ deps.extend(keys)
129
133
  else:
130
134
  deps.append(dep.key)
131
135
  return tuple(deps)
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.2
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
@@ -58,49 +58,53 @@ Dynamic: license-file
58
58
  [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=dperezcabrera_pico-ioc&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-ioc)
59
59
  [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=dperezcabrera_pico-ioc&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-ioc)
60
60
  [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=dperezcabrera_pico-ioc&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-ioc)
61
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/pico-ioc?period=monthly&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=Monthly+downloads)](https://pepy.tech/projects/pico-ioc)
62
+ [![Docs](https://img.shields.io/badge/Docs-pico--ioc-blue?style=flat&logo=readthedocs&logoColor=white)](https://dperezcabrera.github.io/pico-ioc/)
63
+ [![Interactive Lab](https://img.shields.io/badge/Learn-online-green?style=flat&logo=python&logoColor=white)](https://dperezcabrera.github.io/learn-pico-ioc/)
64
+
61
65
 
62
66
  **Pico-IoC** is a **lightweight, async-ready, decorator-driven IoC container** built for clarity, testability, and performance.
63
- It brings *Inversion of Control* and *dependency injection* to Python in a deterministic, modern, and framework-agnostic way.
67
+ It brings Inversion of Control and dependency injection to Python in a deterministic, modern, and framework-agnostic way.
64
68
 
65
- > 🐍 Requires **Python 3.10+**
69
+ > 🐍 Requires Python 3.10+
66
70
 
67
71
  ---
68
72
 
69
73
  ## ⚖️ Core Principles
70
74
 
71
- - **Single Purpose** – Do one thing: dependency management.
72
- - **Declarative** – Use simple decorators (`@component`, `@factory`, `@provides`, `@configured`) instead of complex config files.
73
- - **Deterministic** – No hidden scanning or side-effects; everything flows from an explicit `init()`.
74
- - **Async-Native** – Fully supports async providers, async lifecycle hooks (`__ainit__`), and async interceptors.
75
- - **Fail-Fast** – Detects missing bindings and circular dependencies at bootstrap (`init()`).
76
- - **Testable by Design** – Use `overrides` and `profiles` to swap components instantly.
77
- - **Zero Core Dependencies** – Built entirely on the Python standard library. Optional features may require external packages (see Installation).
75
+ - Single Purpose – Do one thing: dependency management.
76
+ - Declarative – Use simple decorators (`@component`, `@factory`, `@provides`, `@configured`) instead of complex config files.
77
+ - Deterministic – No hidden scanning or side-effects; everything flows from an explicit `init()`.
78
+ - Async-Native – Fully supports async providers, async lifecycle hooks (`__ainit__`), and async interceptors.
79
+ - Fail-Fast – Detects missing bindings and circular dependencies at bootstrap (`init()`).
80
+ - Testable by Design – Use `overrides` and `profiles` to swap components instantly.
81
+ - Zero Core Dependencies – Built entirely on the Python standard library. Optional features may require external packages (see Installation).
78
82
 
79
83
  ---
80
84
 
81
85
  ## 🚀 Why Pico-IoC?
82
86
 
83
87
  As Python systems evolve, wiring dependencies by hand becomes fragile and unmaintainable.
84
- **Pico-IoC** eliminates that friction by letting you declare how components relate — not how they’re created.
88
+ Pico-IoC eliminates that friction by letting you declare how components relate — not how they’re created.
85
89
 
86
- | Feature | Manual Wiring | With Pico-IoC |
87
- | :-------------- | :------------------------- | :-------------------------------- |
88
- | Object creation | `svc = Service(Repo(Config()))` | `svc = container.get(Service)` |
89
- | Replacing deps | Monkey-patch | `overrides={Repo: FakeRepo()}` |
90
- | Coupling | Tight | Loose |
91
- | Testing | Painful | Instant |
92
- | Async support | Manual | Built-in (`aget`, `__ainit__`, ...) |
90
+ | Feature | Manual Wiring | With Pico-IoC |
91
+ | :-------------- | :----------------------------- | :------------------------------ |
92
+ | Object creation | `svc = Service(Repo(Config()))` | `svc = container.get(Service)` |
93
+ | Replacing deps | Monkey-patch | `overrides={Repo: FakeRepo()}` |
94
+ | Coupling | Tight | Loose |
95
+ | Testing | Painful | Instant |
96
+ | Async support | Manual | Built-in (`aget`, `__ainit__`) |
93
97
 
94
98
  ---
95
99
 
96
100
  ## 🧩 Highlights (v2.0+)
97
101
 
98
- - **Unified Configuration:** Use `@configured` to bind both **flat** (ENV-like) and **tree** (YAML/JSON) sources via the `configuration(...)` builder (ADR-0010).
99
- - **Async-aware AOP system:** Method interceptors via `@intercepted_by`.
100
- - **Scoped resolution:** singleton, prototype, request, session, transaction, and custom scopes.
101
- - **`UnifiedComponentProxy`:** Transparent `lazy=True` and AOP proxy supporting serialization.
102
- - **Tree-based configuration runtime:** Advanced mapping with reusable adapters and discriminators (`Annotated[Union[...], Discriminator(...)]`).
103
- - **Observable container context:** Built-in stats, health checks (`@health`), observer hooks (`ContainerObserver`), dependency graph export (`export_graph`), and async cleanup.
102
+ - Unified Configuration: Use `@configured` to bind both flat (ENV-like) and tree (YAML/JSON) sources via the `configuration(...)` builder (ADR-0010).
103
+ - Async-aware AOP system: Method interceptors via `@intercepted_by`.
104
+ - Scoped resolution: singleton, prototype, request, session, transaction, and custom scopes.
105
+ - `UnifiedComponentProxy`: Transparent `lazy=True` and AOP proxy supporting serialization.
106
+ - Tree-based configuration runtime: Advanced mapping with reusable adapters and discriminators (`Annotated[Union[...], Discriminator(...)]`).
107
+ - Observable container context: Built-in stats, health checks (`@health`), observer hooks (`ContainerObserver`), dependency graph export (`export_graph`), and async cleanup.
104
108
 
105
109
  ---
106
110
 
@@ -108,26 +112,15 @@ As Python systems evolve, wiring dependencies by hand becomes fragile and unmain
108
112
 
109
113
  ```bash
110
114
  pip install pico-ioc
111
- ````
112
-
113
- For optional features, you can install extras:
114
-
115
- * **YAML Configuration:**
116
-
117
- ```bash
118
- pip install pico-ioc[yaml]
119
- ```
115
+ ```
120
116
 
121
- (Requires `PyYAML`)
117
+ Optional extras:
122
118
 
123
- * **Dependency Graph Export (Rendering):**
119
+ - YAML configuration support (requires PyYAML)
124
120
 
125
- ```bash
126
- # You still need Graphviz command-line tools installed separately
127
- # This extra is currently not required by the code,
128
- # as export_graph generates the .dot file content directly.
129
- # pip install pico-ioc[graphviz] # Consider removing if not used by code
130
- ```
121
+ ```bash
122
+ pip install pico-ioc[yaml]
123
+ ```
131
124
 
132
125
  -----
133
126
 
@@ -139,7 +132,7 @@ from dataclasses import dataclass
139
132
  from pico_ioc import component, configured, configuration, init, EnvSource
140
133
 
141
134
  # 1. Define configuration with @configured
142
- @configured(prefix="APP_", mapping="auto") # Auto-detects flat mapping
135
+ @configured(prefix="APP_", mapping="auto") # Auto-detects flat mapping
143
136
  @dataclass
144
137
  class Config:
145
138
  db_url: str = "sqlite:///demo.db"
@@ -147,14 +140,14 @@ class Config:
147
140
  # 2. Define components
148
141
  @component
149
142
  class Repo:
150
- def __init__(self, cfg: Config): # Inject config
143
+ def __init__(self, cfg: Config): # Inject config
151
144
  self.cfg = cfg
152
145
  def fetch(self):
153
146
  return f"fetching from {self.cfg.db_url}"
154
147
 
155
148
  @component
156
149
  class Service:
157
- def __init__(self, repo: Repo): # Inject Repo
150
+ def __init__(self, repo: Repo): # Inject Repo
158
151
  self.repo = repo
159
152
  def run(self):
160
153
  return self.repo.fetch()
@@ -164,11 +157,11 @@ os.environ['APP_DB_URL'] = 'postgresql://user:pass@host/db'
164
157
 
165
158
  # 3. Build configuration context
166
159
  config_ctx = configuration(
167
- EnvSource(prefix="") # Read APP_DB_URL from environment
160
+ EnvSource(prefix="") # Read APP_DB_URL from environment
168
161
  )
169
162
 
170
163
  # 4. Initialize container
171
- container = init(modules=[__name__], config=config_ctx) # Pass context via 'config'
164
+ container = init(modules=[__name__], config=config_ctx) # Pass context via 'config'
172
165
 
173
166
  # 5. Get and use the service
174
167
  svc = container.get(Service)
@@ -178,7 +171,7 @@ print(svc.run())
178
171
  del os.environ['APP_DB_URL']
179
172
  ```
180
173
 
181
- **Output:**
174
+ Output:
182
175
 
183
176
  ```
184
177
  fetching from postgresql://user:pass@host/db
@@ -199,7 +192,7 @@ test_config_ctx = configuration()
199
192
  container = init(
200
193
  modules=[__name__],
201
194
  config=test_config_ctx,
202
- overrides={Repo: FakeRepo()} # Replace Repo with FakeRepo
195
+ overrides={Repo: FakeRepo()} # Replace Repo with FakeRepo
203
196
  )
204
197
 
205
198
  svc = container.get(Service)
@@ -208,10 +201,56 @@ assert svc.run() == "fake-data"
208
201
 
209
202
  -----
210
203
 
204
+ ## 🧰 Profiles
205
+
206
+ Use profiles to enable/disable components or configuration branches conditionally.
207
+
208
+ ```python
209
+ # Enable "test" profile when bootstrapping the container
210
+ container = init(
211
+ modules=[__name__],
212
+ profiles=["test"]
213
+ )
214
+ ```
215
+
216
+ Profiles are typically referenced in decorators or configuration mappings to include/exclude components and bindings.
217
+
218
+ -----
219
+
220
+ ## ⚡ Async Components
221
+
222
+ Pico-IoC supports async lifecycle and resolution.
223
+
224
+ ```python
225
+ import asyncio
226
+ from pico_ioc import component, init
227
+
228
+ @component
229
+ class AsyncRepo:
230
+ async def __ainit__(self):
231
+ # e.g., open async connections
232
+ self.ready = True
233
+
234
+ async def fetch(self):
235
+ return "async-data"
236
+
237
+ async def main():
238
+ container = init(modules=[__name__])
239
+ repo = await container.aget(AsyncRepo) # Async resolution
240
+ print(await repo.fetch())
241
+
242
+ asyncio.run(main())
243
+ ```
244
+
245
+ - `__ainit__` runs after construction if defined.
246
+ - Use `container.aget(Type)` to resolve components that require async initialization or whose providers are async.
247
+
248
+ -----
249
+
211
250
  ## 🩺 Lifecycle & AOP
212
251
 
213
252
  ```python
214
- import time # For example
253
+ import time
215
254
  from pico_ioc import component, init, intercepted_by, MethodInterceptor, MethodCtx
216
255
 
217
256
  # Define an interceptor component
@@ -232,7 +271,7 @@ class LogInterceptor(MethodInterceptor):
232
271
 
233
272
  @component
234
273
  class Demo:
235
- @intercepted_by(LogInterceptor) # Apply the interceptor
274
+ @intercepted_by(LogInterceptor) # Apply the interceptor
236
275
  def work(self):
237
276
  print(" Working...")
238
277
  time.sleep(0.01)
@@ -244,7 +283,7 @@ result = c.get(Demo).work()
244
283
  print(f"Result: {result}")
245
284
  ```
246
285
 
247
- **Output:**
286
+ Output:
248
287
 
249
288
  ```
250
289
  → calling Demo.work
@@ -255,19 +294,41 @@ Result: ok
255
294
 
256
295
  -----
257
296
 
297
+ ## 👁️ Observability & Cleanup
298
+
299
+ - Export a dependency graph in DOT format:
300
+
301
+ ```python
302
+ c = init(modules=[...])
303
+ dot = c.export_graph() # Returns DOT graph as a string
304
+ with open("dependencies.dot", "w") as f:
305
+ f.write(dot)
306
+ ```
307
+
308
+ - Health checks:
309
+ - Annotate health probes inside components with `@health` for container-level reporting.
310
+ - The container exposes health information that can be queried in observability tooling.
311
+
312
+ - Container cleanup:
313
+ - For sync components: `container.close()`
314
+ - For async components/resources: `await container.aclose()`
315
+
316
+ Use cleanup in application shutdown hooks to release resources deterministically.
317
+
318
+ -----
319
+
258
320
  ## 📖 Documentation
259
321
 
260
322
  The full documentation is available within the `docs/` directory of the project repository. Start with `docs/README.md` for navigation.
261
323
 
262
- * **Getting Started:** `docs/getting-started.md`
263
- * **User Guide:** `docs/user-guide/README.md`
264
- * **Advanced Features:** `docs/advanced-features/README.md`
265
- * **Observability:** `docs/observability/README.md`
266
- * **Integrations:** `docs/integrations/README.md`
267
- * **Cookbook (Patterns):** `docs/cookbook/README.md`
268
- * **Architecture:** `docs/architecture/README.md`
269
- * **API Reference:** `docs/api-reference/README.md`
270
- * **ADR Index:** `docs/adr/README.md`
324
+ - Getting Started: `docs/getting-started.md`
325
+ - User Guide: `docs/user-guide/README.md`
326
+ - Advanced Features: `docs/advanced-features/README.md`
327
+ - Observability: `docs/observability/README.md`
328
+ - Cookbook (Patterns): `docs/cookbook/README.md`
329
+ - Architecture: `docs/architecture/README.md`
330
+ - API Reference: `docs/api-reference/README.md`
331
+ - ADR Index: `docs/adr/README.md`
271
332
 
272
333
  -----
273
334
 
@@ -282,11 +343,10 @@ tox
282
343
 
283
344
  ## 🧾 Changelog
284
345
 
285
- See [CHANGELOG.md](./CHANGELOG.md) — *Significant redesigns and features in v2.0+.*
346
+ See [CHANGELOG.md](./CHANGELOG.md) — Significant redesigns and features in v2.0+.
286
347
 
287
348
  -----
288
349
 
289
350
  ## 📜 License
290
351
 
291
352
  MIT — [LICENSE](https://opensource.org/licenses/MIT)
292
-
@@ -0,0 +1,25 @@
1
+ pico_ioc/__init__.py,sha256=i25Obx7aH_Oy5b6yjjnCswDgni7InIjrGEcG6vLAw6I,2414
2
+ pico_ioc/_version.py,sha256=m5qImnzcnIhayvILFVqEnXPYsN-vE0vxokygykKhRfw,22
3
+ pico_ioc/analysis.py,sha256=Iy3fuXCVLV8xtT-qp-uxsb1QptHBLLrLYbTSfDkQ-OA,4145
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=Ys1yLjiB3Qxxm_fvWCEYLSeaJ18LseWmXueAW8kHunk,20874
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=JD6psgdGGsBoCwov-G76BrmTfKUoJ22sdwa6wVdmQV8,5064
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.2.dist-info/licenses/LICENSE,sha256=N1_nOvHTM6BobYnOTNXiQkroDqCEi6EzfGBv8lWtyZ0,1077
22
+ pico_ioc-2.1.2.dist-info/METADATA,sha256=yerxK_c9JcZxnKqB-nWQL6bSovNLse9Qa67o2jD9R3I,12339
23
+ pico_ioc-2.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
+ pico_ioc-2.1.2.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
25
+ pico_ioc-2.1.2.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,,