pico-ioc 2.0.4__py3-none-any.whl → 2.1.0__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,75 @@
1
- # src/pico_ioc/container.py
2
1
  import inspect
3
2
  import contextvars
4
- from typing import Any, Dict, List, Optional, Tuple, overload, Union
3
+ import functools
4
+ from typing import Any, Dict, List, Optional, Tuple, overload, Union, Callable, Iterable, Set, get_args, get_origin, Annotated, Protocol, Mapping
5
5
  from contextlib import contextmanager
6
6
  from .constants import LOGGER, PICO_META
7
- from .exceptions import CircularDependencyError, ComponentCreationError, ProviderNotFoundError, AsyncResolutionError
8
- from .factory import ComponentFactory
7
+ from .exceptions import ComponentCreationError, ProviderNotFoundError, AsyncResolutionError
8
+ from .factory import ComponentFactory, ProviderMetadata
9
9
  from .locator import ComponentLocator
10
10
  from .scope import ScopedCaches, ScopeManager
11
11
  from .aop import UnifiedComponentProxy, ContainerObserver
12
+ from .analysis import analyze_callable_dependencies, DependencyRequest
12
13
 
13
14
  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)
15
+
16
+ def _normalize_callable(obj):
17
+ return getattr(obj, '__func__', obj)
18
+
19
+ def _get_signature_safe(callable_obj):
20
+ try:
21
+ return inspect.signature(callable_obj)
22
+ except (ValueError, TypeError):
23
+ wrapped = getattr(callable_obj, '__wrapped__', None)
24
+ if wrapped is not None:
25
+ return inspect.signature(wrapped)
26
+ raise
27
+
28
+ def _needs_async_configure(obj: Any) -> bool:
29
+ for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
30
+ meta = getattr(m, PICO_META, {})
31
+ if meta.get("configure", False) and inspect.iscoroutinefunction(m):
32
+ return True
33
+ return False
34
+
35
+ def _iter_configure_methods(obj: Any):
36
+ for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
37
+ meta = getattr(m, PICO_META, {})
38
+ if meta.get("configure", False):
39
+ yield m
40
+
41
+ def _build_resolution_graph(loc) -> Dict[KeyT, Tuple[KeyT, ...]]:
42
+ if not loc:
43
+ return {}
44
+
45
+ def _map_dep_to_bound_key(dep_key: KeyT) -> KeyT:
46
+ if dep_key in loc._metadata:
47
+ return dep_key
48
+
49
+ if isinstance(dep_key, str):
50
+ mapped = loc.find_key_by_name(dep_key)
51
+ if mapped is not None:
52
+ return mapped
53
+
54
+ if isinstance(dep_key, type):
55
+ for k, md in loc._metadata.items():
56
+ typ = md.provided_type or md.concrete_class
57
+ if isinstance(typ, type):
58
+ try:
59
+ if issubclass(typ, dep_key):
60
+ return k
61
+ except Exception:
62
+ continue
63
+ return dep_key
64
+
65
+ graph: Dict[KeyT, Tuple[KeyT, ...]] = {}
66
+ for key, md in list(loc._metadata.items()):
67
+ deps: List[KeyT] = []
68
+ for d in loc.dependency_keys_for_static(md):
69
+ mapped = _map_dep_to_bound_key(d)
70
+ deps.append(mapped)
71
+ graph[key] = tuple(deps)
72
+ return graph
84
73
 
85
74
  class PicoContainer:
86
75
  _container_id_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("pico_container_id", default=None)
@@ -104,7 +93,6 @@ class PicoContainer:
104
93
  import time as _t
105
94
  self.context = PicoContainer._Ctx(container_id=self.container_id, profiles=profiles, created_at=_t.time())
106
95
  PicoContainer._container_registry[self.container_id] = self
107
- self._tracer = ResolutionTracer(self)
108
96
 
109
97
  @staticmethod
110
98
  def _generate_container_id() -> str:
@@ -153,6 +141,7 @@ class PicoContainer:
153
141
  def _canonical_key(self, key: KeyT) -> KeyT:
154
142
  if self._factory.has(key):
155
143
  return key
144
+
156
145
  if isinstance(key, type) and self._locator:
157
146
  cands: List[Tuple[bool, Any]] = []
158
147
  for k, md in self._locator._metadata.items():
@@ -167,16 +156,19 @@ class PicoContainer:
167
156
  if cands:
168
157
  prim = [k for is_p, k in cands if is_p]
169
158
  return prim[0] if prim else cands[0][1]
159
+
170
160
  if isinstance(key, str) and self._locator:
171
161
  for k, md in self._locator._metadata.items():
172
162
  if md.pico_name == key:
173
163
  return k
164
+
174
165
  return key
175
166
 
176
167
  def _resolve_or_create_internal(self, key: KeyT) -> Tuple[Any, float, bool]:
177
168
  key = self._canonical_key(key)
178
169
  cache = self._cache_for(key)
179
170
  cached = cache.get(key)
171
+
180
172
  if cached is not None:
181
173
  self.context.cache_hit_count += 1
182
174
  for o in self._observers: o.on_cache_hit(key)
@@ -184,19 +176,9 @@ class PicoContainer:
184
176
 
185
177
  import time as _tm
186
178
  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
179
 
194
- token_chain = _resolve_chain.set(tuple(chain + [key]))
195
180
  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
181
+ requester = None
200
182
 
201
183
  try:
202
184
  provider = self._factory.get(key, origin=requester)
@@ -211,8 +193,6 @@ class PicoContainer:
211
193
  return instance_or_awaitable, took_ms, False
212
194
 
213
195
  finally:
214
- self._tracer.leave(token_tracer)
215
- _resolve_chain.reset(token_chain)
216
196
  self.deactivate(token_container)
217
197
 
218
198
  @overload
@@ -256,24 +236,6 @@ class PicoContainer:
256
236
 
257
237
  return final_instance
258
238
 
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
239
  def _maybe_wrap_with_aspects(self, key, instance: Any) -> Any:
278
240
  if isinstance(instance, UnifiedComponentProxy):
279
241
  return instance
@@ -283,14 +245,9 @@ class PicoContainer:
283
245
  return UnifiedComponentProxy(container=self, target=instance)
284
246
  return instance
285
247
 
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)
248
+ def _iterate_cleanup_targets(self) -> Iterable[Any]:
249
+ yield from (obj for _, obj in self._caches.all_items())
250
+
294
251
  if self._locator:
295
252
  seen = set()
296
253
  for md in self._locator._metadata.values():
@@ -298,12 +255,37 @@ class PicoContainer:
298
255
  if fc and fc not in seen:
299
256
  seen.add(fc)
300
257
  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)
258
+ yield inst
259
+
260
+ def _call_cleanup_method(self, method: Callable[..., Any]) -> Any:
261
+ deps_requests = analyze_callable_dependencies(method)
262
+ return method(**self._resolve_args(deps_requests))
263
+
264
+ def cleanup_all(self) -> None:
265
+ for obj in self._iterate_cleanup_targets():
266
+ for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
267
+ meta = getattr(m, PICO_META, {})
268
+ if meta.get("cleanup", False):
269
+ res = self._call_cleanup_method(m)
270
+ if inspect.isawaitable(res):
271
+ LOGGER.warning(f"Async cleanup method {m} called during sync shutdown. Awaitable ignored.")
272
+
273
+ async def cleanup_all_async(self) -> None:
274
+ for obj in self._iterate_cleanup_targets():
275
+ for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
276
+ meta = getattr(m, PICO_META, {})
277
+ if meta.get("cleanup", False):
278
+ res = self._call_cleanup_method(m)
279
+ if inspect.isawaitable(res):
280
+ await res
281
+
282
+ try:
283
+ from .event_bus import EventBus
284
+ for _, obj in self._caches.all_items():
285
+ if isinstance(obj, EventBus):
286
+ await obj.aclose()
287
+ except Exception:
288
+ pass
307
289
 
308
290
  def activate_scope(self, name: str, scope_id: Any):
309
291
  return self.scopes.activate(name, scope_id)
@@ -333,39 +315,6 @@ class PicoContainer:
333
315
  out[f"{getattr(k,'__name__',k)}.{name}"] = False
334
316
  return out
335
317
 
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
318
  def stats(self) -> Dict[str, Any]:
370
319
  import time as _t
371
320
  resolves = self.context.resolve_count
@@ -385,6 +334,9 @@ class PicoContainer:
385
334
  self.cleanup_all()
386
335
  PicoContainer._container_registry.pop(self.container_id, None)
387
336
 
337
+ def build_resolution_graph(self) -> None:
338
+ return _build_resolution_graph(self._locator)
339
+
388
340
  def export_graph(
389
341
  self,
390
342
  path: str,
@@ -398,10 +350,8 @@ class PicoContainer:
398
350
  if not self._locator:
399
351
  raise RuntimeError("No locator attached; cannot export dependency graph.")
400
352
 
401
- from .api import _build_resolution_graph
402
-
403
353
  md_by_key = self._locator._metadata
404
- graph = _build_resolution_graph(self)
354
+ graph = _build_resolution_graph(self._locator)
405
355
 
406
356
  lines: List[str] = []
407
357
  lines.append("digraph Pico {")
@@ -441,3 +391,94 @@ class PicoContainer:
441
391
  with open(path, "w", encoding="utf-8") as f:
442
392
  f.write("\n".join(lines))
443
393
 
394
+
395
+ def _resolve_args(self, dependencies: Tuple[DependencyRequest, ...]) -> Dict[str, Any]:
396
+ kwargs: Dict[str, Any] = {}
397
+ if not dependencies:
398
+ return kwargs
399
+
400
+ for dep in dependencies:
401
+ if dep.is_list:
402
+ keys: Tuple[KeyT, ...] = ()
403
+ if self._locator is not None and isinstance(dep.key, type):
404
+ keys = tuple(self._locator.collect_by_type(dep.key, dep.qualifier))
405
+ kwargs[dep.parameter_name] = [self.get(k) for k in keys]
406
+ continue
407
+
408
+ primary_key = dep.key
409
+ if isinstance(primary_key, str) and self._locator is not None:
410
+ mapped = self._locator.find_key_by_name(primary_key)
411
+ primary_key = mapped if mapped is not None else primary_key
412
+
413
+ try:
414
+ kwargs[dep.parameter_name] = self.get(primary_key)
415
+ except Exception as first_error:
416
+ if primary_key != dep.parameter_name:
417
+ try:
418
+ kwargs[dep.parameter_name] = self.get(dep.parameter_name)
419
+ except Exception:
420
+ raise first_error from None
421
+ else:
422
+ raise first_error from None
423
+ return kwargs
424
+
425
+
426
+ def build_class(self, cls: type, locator: ComponentLocator, dependencies: Tuple[DependencyRequest, ...]) -> Any:
427
+ init = cls.__init__
428
+ if init is object.__init__:
429
+ inst = cls()
430
+ else:
431
+ deps = self._resolve_args(dependencies)
432
+ inst = cls(**deps)
433
+
434
+ ainit = getattr(inst, "__ainit__", None)
435
+ has_async = (callable(ainit) and inspect.iscoroutinefunction(ainit)) or _needs_async_configure(inst)
436
+
437
+ if has_async:
438
+ async def runner():
439
+ if callable(ainit):
440
+ kwargs = {}
441
+ try:
442
+ ainit_deps = analyze_callable_dependencies(ainit)
443
+ kwargs = self._resolve_args(ainit_deps)
444
+ except Exception:
445
+ kwargs = {}
446
+ res = ainit(**kwargs)
447
+ if inspect.isawaitable(res):
448
+ 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
+ return inst
456
+ return runner()
457
+
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
+ return inst
463
+
464
+ def build_method(self, fn: Callable[..., Any], locator: ComponentLocator, dependencies: Tuple[DependencyRequest, ...]) -> Any:
465
+ deps = self._resolve_args(dependencies)
466
+ 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
+ return obj
pico_ioc/decorators.py ADDED
@@ -0,0 +1,192 @@
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, *, prefix: str = "", mapping: str = "auto"):
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
+ return cls
183
+ return dec
184
+
185
+ def get_return_type(fn: Callable[..., Any]) -> Optional[type]:
186
+ try:
187
+ ra = inspect.signature(fn).return_annotation
188
+ except Exception:
189
+ return None
190
+ if ra is inspect._empty:
191
+ return None
192
+ return ra if isinstance(ra, type) else None