pico-ioc 2.0.5__py3-none-any.whl → 2.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pico_ioc/container.py CHANGED
@@ -1,86 +1,77 @@
1
1
  # src/pico_ioc/container.py
2
+
2
3
  import inspect
3
4
  import contextvars
4
- from typing import Any, Dict, List, Optional, Tuple, overload, Union
5
+ import functools
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 CircularDependencyError, ComponentCreationError, ProviderNotFoundError, AsyncResolutionError
8
- from .factory import ComponentFactory
9
+ from .exceptions import ComponentCreationError, ProviderNotFoundError, AsyncResolutionError, ConfigurationError
10
+ from .factory import ComponentFactory, ProviderMetadata
9
11
  from .locator import ComponentLocator
10
12
  from .scope import ScopedCaches, ScopeManager
11
13
  from .aop import UnifiedComponentProxy, ContainerObserver
14
+ from .analysis import analyze_callable_dependencies, DependencyRequest
12
15
 
13
16
  KeyT = Union[str, type]
14
- _resolve_chain: contextvars.ContextVar[Tuple[KeyT, ...]] = contextvars.ContextVar("pico_resolve_chain", default=())
15
-
16
- class _TracerFrame:
17
- __slots__ = ("parent_key", "via")
18
- def __init__(self, parent_key: KeyT, via: str):
19
- self.parent_key = parent_key
20
- self.via = via
21
-
22
- class ResolutionTracer:
23
- def __init__(self, container: "PicoContainer") -> None:
24
- self._container = container
25
- self._stack_var: contextvars.ContextVar[List[_TracerFrame]] = contextvars.ContextVar("pico_tracer_stack", default=[])
26
- self._edges: Dict[Tuple[KeyT, KeyT], Tuple[str, str]] = {}
27
-
28
- def enter(self, parent_key: KeyT, via: str) -> contextvars.Token:
29
- stack = list(self._stack_var.get())
30
- stack.append(_TracerFrame(parent_key, via))
31
- return self._stack_var.set(stack)
32
-
33
- def leave(self, token: contextvars.Token) -> None:
34
- self._stack_var.reset(token)
35
-
36
- def override_via(self, new_via: str) -> Optional[str]:
37
- stack = self._stack_var.get()
38
- if not stack:
39
- return None
40
- prev = stack[-1].via
41
- stack[-1].via = new_via
42
- return prev
43
-
44
- def restore_via(self, previous: Optional[str]) -> None:
45
- if previous is None:
46
- return
47
- stack = self._stack_var.get()
48
- if not stack:
49
- return
50
- stack[-1].via = previous
51
-
52
- def note_param(self, child_key: KeyT, param_name: str) -> None:
53
- stack = self._stack_var.get()
54
- if not stack:
55
- return
56
- parent = stack[-1].parent_key
57
- via = stack[-1].via
58
- self._edges[(parent, child_key)] = (via, param_name)
59
-
60
- def describe_cycle(self, chain: Tuple[KeyT, ...], current: KeyT, locator: Optional[ComponentLocator]) -> str:
61
- def name_of(k: KeyT) -> str:
62
- return getattr(k, "__name__", str(k))
63
- def scope_of(k: KeyT) -> str:
64
- if not locator:
65
- return "singleton"
66
- md = locator._metadata.get(k)
67
- return md.scope if md else "singleton"
68
- lines: List[str] = []
69
- lines.append("Circular dependency detected.")
70
- lines.append("")
71
- lines.append("Resolution chain:")
72
- full = tuple(chain) + (current,)
73
- for idx, k in enumerate(full, 1):
74
- mark = " ❌" if idx == len(full) else ""
75
- lines.append(f" {idx}. {name_of(k)} [scope={scope_of(k)}]{mark}")
76
- if idx < len(full):
77
- parent = k
78
- child = full[idx]
79
- via, param = self._edges.get((parent, child), ("provider", "?"))
80
- lines.append(f" └─ via {via} param '{param}' → {name_of(child)}")
81
- lines.append("")
82
- lines.append("Hint: break the cycle with a @configure setter or use a factory/provider.")
83
- return "\n".join(lines)
17
+
18
+ def _normalize_callable(obj):
19
+ return getattr(obj, '__func__', obj)
20
+
21
+ def _get_signature_safe(callable_obj):
22
+ try:
23
+ return inspect.signature(callable_obj)
24
+ except (ValueError, TypeError):
25
+ wrapped = getattr(callable_obj, '__wrapped__', None)
26
+ if wrapped is not None:
27
+ return inspect.signature(wrapped)
28
+ raise
29
+
30
+ def _needs_async_configure(obj: Any) -> bool:
31
+ for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
32
+ meta = getattr(m, PICO_META, {})
33
+ if meta.get("configure", False) and inspect.iscoroutinefunction(m):
34
+ return True
35
+ return False
36
+
37
+ def _iter_configure_methods(obj: Any):
38
+ for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
39
+ meta = getattr(m, PICO_META, {})
40
+ if meta.get("configure", False):
41
+ yield m
42
+
43
+ def _build_resolution_graph(loc) -> Dict[KeyT, Tuple[KeyT, ...]]:
44
+ if not loc:
45
+ return {}
46
+
47
+ def _map_dep_to_bound_key(dep_key: KeyT) -> KeyT:
48
+ if dep_key in loc._metadata:
49
+ return dep_key
50
+
51
+ if isinstance(dep_key, str):
52
+ mapped = loc.find_key_by_name(dep_key)
53
+ if mapped is not None:
54
+ return mapped
55
+
56
+ if isinstance(dep_key, type):
57
+ for k, md in loc._metadata.items():
58
+ typ = md.provided_type or md.concrete_class
59
+ if isinstance(typ, type):
60
+ try:
61
+ if issubclass(typ, dep_key):
62
+ return k
63
+ except Exception:
64
+ continue
65
+ return dep_key
66
+
67
+ graph: Dict[KeyT, Tuple[KeyT, ...]] = {}
68
+ for key, md in list(loc._metadata.items()):
69
+ deps: List[KeyT] = []
70
+ for d in loc.dependency_keys_for_static(md):
71
+ mapped = _map_dep_to_bound_key(d)
72
+ deps.append(mapped)
73
+ graph[key] = tuple(deps)
74
+ return graph
84
75
 
85
76
  class PicoContainer:
86
77
  _container_id_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("pico_container_id", default=None)
@@ -104,7 +95,6 @@ class PicoContainer:
104
95
  import time as _t
105
96
  self.context = PicoContainer._Ctx(container_id=self.container_id, profiles=profiles, created_at=_t.time())
106
97
  PicoContainer._container_registry[self.container_id] = self
107
- self._tracer = ResolutionTracer(self)
108
98
 
109
99
  @staticmethod
110
100
  def _generate_container_id() -> str:
@@ -153,6 +143,7 @@ class PicoContainer:
153
143
  def _canonical_key(self, key: KeyT) -> KeyT:
154
144
  if self._factory.has(key):
155
145
  return key
146
+
156
147
  if isinstance(key, type) and self._locator:
157
148
  cands: List[Tuple[bool, Any]] = []
158
149
  for k, md in self._locator._metadata.items():
@@ -167,16 +158,19 @@ class PicoContainer:
167
158
  if cands:
168
159
  prim = [k for is_p, k in cands if is_p]
169
160
  return prim[0] if prim else cands[0][1]
161
+
170
162
  if isinstance(key, str) and self._locator:
171
163
  for k, md in self._locator._metadata.items():
172
164
  if md.pico_name == key:
173
165
  return k
166
+
174
167
  return key
175
168
 
176
169
  def _resolve_or_create_internal(self, key: KeyT) -> Tuple[Any, float, bool]:
177
170
  key = self._canonical_key(key)
178
171
  cache = self._cache_for(key)
179
172
  cached = cache.get(key)
173
+
180
174
  if cached is not None:
181
175
  self.context.cache_hit_count += 1
182
176
  for o in self._observers: o.on_cache_hit(key)
@@ -184,19 +178,9 @@ class PicoContainer:
184
178
 
185
179
  import time as _tm
186
180
  t0 = _tm.perf_counter()
187
- chain = list(_resolve_chain.get())
188
-
189
- for k_in_chain in chain:
190
- if k_in_chain == key:
191
- details = self._tracer.describe_cycle(tuple(chain), key, self._locator)
192
- raise ComponentCreationError(key, CircularDependencyError(chain, key, details=details))
193
181
 
194
- token_chain = _resolve_chain.set(tuple(chain + [key]))
195
182
  token_container = self.activate()
196
- token_tracer = self._tracer.enter(key, via="provider")
197
-
198
- requester = chain[-1] if chain else None
199
- instance_or_awaitable = None
183
+ requester = None
200
184
 
201
185
  try:
202
186
  provider = self._factory.get(key, origin=requester)
@@ -211,10 +195,28 @@ class PicoContainer:
211
195
  return instance_or_awaitable, took_ms, False
212
196
 
213
197
  finally:
214
- self._tracer.leave(token_tracer)
215
- _resolve_chain.reset(token_chain)
216
198
  self.deactivate(token_container)
217
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
+
218
220
  @overload
219
221
  def get(self, key: type) -> Any: ...
220
222
  @overload
@@ -230,6 +232,14 @@ class PicoContainer:
230
232
  key_name = getattr(key, '__name__', str(key))
231
233
  raise AsyncResolutionError(key)
232
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
+
233
243
  final_instance = self._maybe_wrap_with_aspects(key, instance)
234
244
  cache = self._cache_for(key)
235
245
  cache.put(key, final_instance)
@@ -248,6 +258,15 @@ class PicoContainer:
248
258
  if inspect.isawaitable(instance_or_awaitable):
249
259
  instance = await instance_or_awaitable
250
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
+
251
270
  final_instance = self._maybe_wrap_with_aspects(key, instance)
252
271
  cache = self._cache_for(key)
253
272
  cache.put(key, final_instance)
@@ -256,24 +275,6 @@ class PicoContainer:
256
275
 
257
276
  return final_instance
258
277
 
259
- def _resolve_type_key(self, key: type):
260
- if not self._locator:
261
- return None
262
- cands: List[Tuple[bool, Any]] = []
263
- for k, md in self._locator._metadata.items():
264
- typ = md.provided_type or md.concrete_class
265
- if not isinstance(typ, type):
266
- continue
267
- try:
268
- if typ is not key and issubclass(typ, key):
269
- cands.append((md.primary, k))
270
- except Exception:
271
- continue
272
- if not cands:
273
- return None
274
- prim = [k for is_p, k in cands if is_p]
275
- return prim[0] if prim else cands[0][1]
276
-
277
278
  def _maybe_wrap_with_aspects(self, key, instance: Any) -> Any:
278
279
  if isinstance(instance, UnifiedComponentProxy):
279
280
  return instance
@@ -283,14 +284,9 @@ class PicoContainer:
283
284
  return UnifiedComponentProxy(container=self, target=instance)
284
285
  return instance
285
286
 
286
- def cleanup_all(self) -> None:
287
- for _, obj in self._caches.all_items():
288
- for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
289
- meta = getattr(m, PICO_META, {})
290
- if meta.get("cleanup", False):
291
- from .api import _resolve_args
292
- kwargs = _resolve_args(m, self)
293
- m(**kwargs)
287
+ def _iterate_cleanup_targets(self) -> Iterable[Any]:
288
+ yield from (obj for _, obj in self._caches.all_items())
289
+
294
290
  if self._locator:
295
291
  seen = set()
296
292
  for md in self._locator._metadata.values():
@@ -298,12 +294,37 @@ class PicoContainer:
298
294
  if fc and fc not in seen:
299
295
  seen.add(fc)
300
296
  inst = self.get(fc) if self._factory.has(fc) else fc()
301
- for _, m in inspect.getmembers(inst, predicate=inspect.ismethod):
302
- meta = getattr(m, PICO_META, {})
303
- if meta.get("cleanup", False):
304
- from .api import _resolve_args
305
- kwargs = _resolve_args(m, self)
306
- m(**kwargs)
297
+ yield inst
298
+
299
+ def _call_cleanup_method(self, method: Callable[..., Any]) -> Any:
300
+ deps_requests = analyze_callable_dependencies(method)
301
+ return method(**self._resolve_args(deps_requests))
302
+
303
+ def cleanup_all(self) -> None:
304
+ for obj in self._iterate_cleanup_targets():
305
+ for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
306
+ meta = getattr(m, PICO_META, {})
307
+ if meta.get("cleanup", False):
308
+ res = self._call_cleanup_method(m)
309
+ if inspect.isawaitable(res):
310
+ LOGGER.warning(f"Async cleanup method {m} called during sync shutdown. Awaitable ignored.")
311
+
312
+ async def cleanup_all_async(self) -> None:
313
+ for obj in self._iterate_cleanup_targets():
314
+ for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
315
+ meta = getattr(m, PICO_META, {})
316
+ if meta.get("cleanup", False):
317
+ res = self._call_cleanup_method(m)
318
+ if inspect.isawaitable(res):
319
+ await res
320
+
321
+ try:
322
+ from .event_bus import EventBus
323
+ for _, obj in self._caches.all_items():
324
+ if isinstance(obj, EventBus):
325
+ await obj.aclose()
326
+ except Exception:
327
+ pass
307
328
 
308
329
  def activate_scope(self, name: str, scope_id: Any):
309
330
  return self.scopes.activate(name, scope_id)
@@ -333,39 +354,6 @@ class PicoContainer:
333
354
  out[f"{getattr(k,'__name__',k)}.{name}"] = False
334
355
  return out
335
356
 
336
- async def cleanup_all_async(self) -> None:
337
- for _, obj in self._caches.all_items():
338
- for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
339
- meta = getattr(m, PICO_META, {})
340
- if meta.get("cleanup", False):
341
- from .api import _resolve_args
342
- res = m(**_resolve_args(m, self))
343
- import inspect as _i
344
- if _i.isawaitable(res):
345
- await res
346
- if self._locator:
347
- seen = set()
348
- for md in self._locator._metadata.values():
349
- fc = md.factory_class
350
- if fc and fc not in seen:
351
- seen.add(fc)
352
- inst = self.get(fc) if self._factory.has(fc) else fc()
353
- for _, m in inspect.getmembers(inst, predicate=inspect.ismethod):
354
- meta = getattr(m, PICO_META, {})
355
- if meta.get("cleanup", False):
356
- from .api import _resolve_args
357
- res = m(**_resolve_args(m, self))
358
- import inspect as _i
359
- if _i.isawaitable(res):
360
- await res
361
- try:
362
- from .event_bus import EventBus
363
- for _, obj in self._caches.all_items():
364
- if isinstance(obj, EventBus):
365
- await obj.aclose()
366
- except Exception:
367
- pass
368
-
369
357
  def stats(self) -> Dict[str, Any]:
370
358
  import time as _t
371
359
  resolves = self.context.resolve_count
@@ -385,6 +373,9 @@ class PicoContainer:
385
373
  self.cleanup_all()
386
374
  PicoContainer._container_registry.pop(self.container_id, None)
387
375
 
376
+ def build_resolution_graph(self) -> None:
377
+ return _build_resolution_graph(self._locator)
378
+
388
379
  def export_graph(
389
380
  self,
390
381
  path: str,
@@ -398,10 +389,8 @@ class PicoContainer:
398
389
  if not self._locator:
399
390
  raise RuntimeError("No locator attached; cannot export dependency graph.")
400
391
 
401
- from .api import _build_resolution_graph
402
-
403
392
  md_by_key = self._locator._metadata
404
- graph = _build_resolution_graph(self)
393
+ graph = _build_resolution_graph(self._locator)
405
394
 
406
395
  lines: List[str] = []
407
396
  lines.append("digraph Pico {")
@@ -441,3 +430,67 @@ class PicoContainer:
441
430
  with open(path, "w", encoding="utf-8") as f:
442
431
  f.write("\n".join(lines))
443
432
 
433
+
434
+ def _resolve_args(self, dependencies: Tuple[DependencyRequest, ...]) -> Dict[str, Any]:
435
+ kwargs: Dict[str, Any] = {}
436
+ if not dependencies:
437
+ return kwargs
438
+
439
+ for dep in dependencies:
440
+ if dep.is_list:
441
+ keys: Tuple[KeyT, ...] = ()
442
+ if self._locator is not None and isinstance(dep.key, type):
443
+ keys = tuple(self._locator.collect_by_type(dep.key, dep.qualifier))
444
+ kwargs[dep.parameter_name] = [self.get(k) for k in keys]
445
+ continue
446
+
447
+ primary_key = dep.key
448
+ if isinstance(primary_key, str) and self._locator is not None:
449
+ mapped = self._locator.find_key_by_name(primary_key)
450
+ primary_key = mapped if mapped is not None else primary_key
451
+
452
+ try:
453
+ kwargs[dep.parameter_name] = self.get(primary_key)
454
+ except Exception as first_error:
455
+ if primary_key != dep.parameter_name:
456
+ try:
457
+ kwargs[dep.parameter_name] = self.get(dep.parameter_name)
458
+ except Exception:
459
+ raise first_error from None
460
+ else:
461
+ raise first_error from None
462
+ return kwargs
463
+
464
+
465
+ def build_class(self, cls: type, locator: ComponentLocator, dependencies: Tuple[DependencyRequest, ...]) -> Any:
466
+ init = cls.__init__
467
+ if init is object.__init__:
468
+ inst = cls()
469
+ else:
470
+ deps = self._resolve_args(dependencies)
471
+ inst = cls(**deps)
472
+
473
+ ainit = getattr(inst, "__ainit__", None)
474
+ has_async = (callable(ainit) and inspect.iscoroutinefunction(ainit))
475
+
476
+ if has_async:
477
+ async def runner():
478
+ if callable(ainit):
479
+ kwargs = {}
480
+ try:
481
+ ainit_deps = analyze_callable_dependencies(ainit)
482
+ kwargs = self._resolve_args(ainit_deps)
483
+ except Exception:
484
+ kwargs = {}
485
+ res = ainit(**kwargs)
486
+ if inspect.isawaitable(res):
487
+ await res
488
+ return inst
489
+ return runner()
490
+
491
+ return inst
492
+
493
+ def build_method(self, fn: Callable[..., Any], locator: ComponentLocator, dependencies: Tuple[DependencyRequest, ...]) -> Any:
494
+ deps = self._resolve_args(dependencies)
495
+ obj = fn(**deps)
496
+ return obj
pico_ioc/decorators.py ADDED
@@ -0,0 +1,193 @@
1
+ from typing import Any, Callable, Dict, Iterable, Optional
2
+ import inspect
3
+ from dataclasses import MISSING
4
+ from .constants import PICO_INFRA, PICO_NAME, PICO_KEY, PICO_META
5
+
6
+ def _meta_get(obj: Any) -> Dict[str, Any]:
7
+ m = getattr(obj, PICO_META, None)
8
+ if m is None:
9
+ m = {}
10
+ setattr(obj, PICO_META, m)
11
+ return m
12
+
13
+ def _apply_common_metadata(
14
+ obj: Any,
15
+ *,
16
+ qualifiers: Iterable[str] = (),
17
+ scope: str = "singleton",
18
+ primary: bool = False,
19
+ lazy: bool = False,
20
+ conditional_profiles: Iterable[str] = (),
21
+ conditional_require_env: Iterable[str] = (),
22
+ conditional_predicate: Optional[Callable[[], bool]] = None,
23
+ on_missing_selector: Optional[object] = None,
24
+ on_missing_priority: int = 0,
25
+ ):
26
+ m = _meta_get(obj)
27
+ m["qualifier"] = tuple(str(q) for q in qualifiers or ())
28
+ m["scope"] = scope
29
+
30
+ if primary:
31
+ m["primary"] = True
32
+ if lazy:
33
+ m["lazy"] = True
34
+
35
+ has_conditional = (
36
+ conditional_profiles or
37
+ conditional_require_env or
38
+ conditional_predicate is not None
39
+ )
40
+
41
+ if has_conditional:
42
+ m["conditional"] = {
43
+ "profiles": tuple(p for p in conditional_profiles or ()),
44
+ "require_env": tuple(e for e in conditional_require_env or ()),
45
+ "predicate": conditional_predicate,
46
+ }
47
+
48
+ if on_missing_selector is not None:
49
+ m["on_missing"] = {
50
+ "selector": on_missing_selector,
51
+ "priority": int(on_missing_priority)
52
+ }
53
+ return obj
54
+
55
+ def component(
56
+ cls=None,
57
+ *,
58
+ name: Any = None,
59
+ qualifiers: Iterable[str] = (),
60
+ scope: str = "singleton",
61
+ primary: bool = False,
62
+ lazy: bool = False,
63
+ conditional_profiles: Iterable[str] = (),
64
+ conditional_require_env: Iterable[str] = (),
65
+ conditional_predicate: Optional[Callable[[], bool]] = None,
66
+ on_missing_selector: Optional[object] = None,
67
+ on_missing_priority: int = 0,
68
+ ):
69
+ def dec(c):
70
+ setattr(c, PICO_INFRA, "component")
71
+ setattr(c, PICO_NAME, name if name is not None else getattr(c, "__name__", str(c)))
72
+ setattr(c, PICO_KEY, name if name is not None else c)
73
+
74
+ _apply_common_metadata(
75
+ c,
76
+ qualifiers=qualifiers,
77
+ scope=scope,
78
+ primary=primary,
79
+ lazy=lazy,
80
+ conditional_profiles=conditional_profiles,
81
+ conditional_require_env=conditional_require_env,
82
+ conditional_predicate=conditional_predicate,
83
+ on_missing_selector=on_missing_selector,
84
+ on_missing_priority=on_missing_priority,
85
+ )
86
+ return c
87
+ return dec(cls) if cls else dec
88
+
89
+ def factory(
90
+ cls=None,
91
+ *,
92
+ name: Any = None,
93
+ qualifiers: Iterable[str] = (),
94
+ scope: str = "singleton",
95
+ primary: bool = False,
96
+ lazy: bool = False,
97
+ conditional_profiles: Iterable[str] = (),
98
+ conditional_require_env: Iterable[str] = (),
99
+ conditional_predicate: Optional[Callable[[], bool]] = None,
100
+ on_missing_selector: Optional[object] = None,
101
+ on_missing_priority: int = 0,
102
+ ):
103
+ def dec(c):
104
+ setattr(c, PICO_INFRA, "factory")
105
+ setattr(c, PICO_NAME, name if name is not None else getattr(c, "__name__", str(c)))
106
+
107
+ _apply_common_metadata(
108
+ c,
109
+ qualifiers=qualifiers,
110
+ scope=scope,
111
+ primary=primary,
112
+ lazy=lazy,
113
+ conditional_profiles=conditional_profiles,
114
+ conditional_require_env=conditional_require_env,
115
+ conditional_predicate=conditional_predicate,
116
+ on_missing_selector=on_missing_selector,
117
+ on_missing_priority=on_missing_priority,
118
+ )
119
+ return c
120
+ return dec(cls) if cls else dec
121
+
122
+ def provides(*dargs, **dkwargs):
123
+ def _apply(fn, key_hint, *, name=None, qualifiers=(), scope="singleton", primary=False, lazy=False, conditional_profiles=(), conditional_require_env=(), conditional_predicate=None, on_missing_selector=None, on_missing_priority=0):
124
+ target = fn.__func__ if isinstance(fn, (staticmethod, classmethod)) else fn
125
+
126
+ inferred_key = key_hint
127
+ if inferred_key is MISSING:
128
+ rt = get_return_type(target)
129
+ if isinstance(rt, type):
130
+ inferred_key = rt
131
+ else:
132
+ inferred_key = getattr(target, "__name__", str(target))
133
+
134
+ setattr(target, PICO_INFRA, "provides")
135
+ pico_name = name if name is not None else (inferred_key if isinstance(inferred_key, str) else getattr(target, "__name__", str(target)))
136
+ setattr(target, PICO_NAME, pico_name)
137
+ setattr(target, PICO_KEY, inferred_key)
138
+
139
+ _apply_common_metadata(
140
+ target,
141
+ qualifiers=qualifiers,
142
+ scope=scope,
143
+ primary=primary,
144
+ lazy=lazy,
145
+ conditional_profiles=conditional_profiles,
146
+ conditional_require_env=conditional_require_env,
147
+ conditional_predicate=conditional_predicate,
148
+ on_missing_selector=on_missing_selector,
149
+ on_missing_priority=on_missing_priority,
150
+ )
151
+ return fn
152
+
153
+ if dargs and len(dargs) == 1 and inspect.isfunction(dargs[0]) and not dkwargs:
154
+ fn = dargs[0]
155
+ return _apply(fn, MISSING)
156
+ else:
157
+ key = dargs[0] if dargs else MISSING
158
+ def _decorator(fn):
159
+ return _apply(fn, key, **dkwargs)
160
+ return _decorator
161
+
162
+ class Qualifier(str):
163
+ __slots__ = ()
164
+
165
+ def configure(fn):
166
+ m = _meta_get(fn)
167
+ m["configure"] = True
168
+ return fn
169
+
170
+ def cleanup(fn):
171
+ m = _meta_get(fn)
172
+ m["cleanup"] = True
173
+ return fn
174
+
175
+ def configured(target: Any = "self", *, prefix: str = "", mapping: str = "auto", **kwargs):
176
+ if mapping not in ("auto", "flat", "tree"):
177
+ raise ValueError("mapping must be one of 'auto', 'flat', or 'tree'")
178
+ def dec(cls):
179
+ setattr(cls, PICO_INFRA, "configured")
180
+ m = _meta_get(cls)
181
+ m["configured"] = {"target": target, "prefix": prefix, "mapping": mapping}
182
+ _apply_common_metadata(cls, **kwargs)
183
+ return cls
184
+ return dec
185
+
186
+ def get_return_type(fn: Callable[..., Any]) -> Optional[type]:
187
+ try:
188
+ ra = inspect.signature(fn).return_annotation
189
+ except Exception:
190
+ return None
191
+ if ra is inspect._empty:
192
+ return None
193
+ return ra if isinstance(ra, type) else None