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/__init__.py CHANGED
@@ -3,7 +3,6 @@ from .constants import LOGGER_NAME, LOGGER, PICO_INFRA, PICO_NAME, PICO_KEY, PIC
3
3
  from .exceptions import (
4
4
  PicoError,
5
5
  ProviderNotFoundError,
6
- CircularDependencyError,
7
6
  ComponentCreationError,
8
7
  ScopeError,
9
8
  ConfigurationError,
@@ -18,23 +17,20 @@ from .api import (
18
17
  factory,
19
18
  provides,
20
19
  Qualifier,
21
- configuration,
22
20
  configure,
23
21
  cleanup,
24
- ConfigSource,
25
- EnvSource,
26
- FileSource,
27
- FlatDictSource,
28
22
  init,
29
23
  configured,
30
24
  )
25
+ from .config_builder import configuration, ContextConfig, EnvSource, FileSource, FlatDictSource, Value
31
26
  from .scope import ScopeManager, ContextVarScope, ScopeProtocol, ScopedCaches
32
27
  from .locator import ComponentLocator
33
28
  from .factory import ComponentFactory, ProviderMetadata, DeferredProvider
34
29
  from .aop import MethodCtx, MethodInterceptor, intercepted_by, UnifiedComponentProxy, health, ContainerObserver
35
30
  from .container import PicoContainer
36
31
  from .event_bus import EventBus, ExecPolicy, ErrorPolicy, Event, subscribe, AutoSubscriberMixin
37
- from .config_runtime import JsonTreeSource, YamlTreeSource, DictSource, Discriminator
32
+ from .config_runtime import JsonTreeSource, YamlTreeSource, DictSource, Discriminator, Value
33
+ from .analysis import DependencyRequest, analyze_callable_dependencies
38
34
 
39
35
  __all__ = [
40
36
  "LOGGER_NAME",
@@ -45,7 +41,6 @@ __all__ = [
45
41
  "PICO_META",
46
42
  "PicoError",
47
43
  "ProviderNotFoundError",
48
- "CircularDependencyError",
49
44
  "ComponentCreationError",
50
45
  "ScopeError",
51
46
  "ConfigurationError",
@@ -58,7 +53,6 @@ __all__ = [
58
53
  "factory",
59
54
  "provides",
60
55
  "Qualifier",
61
- "configuration",
62
56
  "configure",
63
57
  "cleanup",
64
58
  "ScopeProtocol",
@@ -78,10 +72,12 @@ __all__ = [
78
72
  "PicoContainer",
79
73
  "EnvSource",
80
74
  "FileSource",
81
- "ConfigSource",
82
75
  "FlatDictSource",
83
76
  "init",
84
77
  "configured",
78
+ "configuration",
79
+ "ContextConfig",
80
+ "Value",
85
81
  "EventBus",
86
82
  "ExecPolicy",
87
83
  "ErrorPolicy",
@@ -92,6 +88,7 @@ __all__ = [
92
88
  "YamlTreeSource",
93
89
  "DictSource",
94
90
  "Discriminator",
91
+ "Value",
92
+ "DependencyRequest",
93
+ "analyze_callable_dependencies",
95
94
  ]
96
-
97
-
pico_ioc/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '2.0.5'
1
+ __version__ = '2.1.1'
pico_ioc/analysis.py ADDED
@@ -0,0 +1,92 @@
1
+ import inspect
2
+ from dataclasses import dataclass
3
+ from typing import Any, Callable, List, Optional, Tuple, Union, get_args, get_origin, Annotated
4
+ from .decorators import Qualifier
5
+
6
+ KeyT = Union[str, type]
7
+
8
+ @dataclass(frozen=True)
9
+ class DependencyRequest:
10
+ parameter_name: str
11
+ key: KeyT
12
+ is_list: bool = False
13
+ qualifier: Optional[str] = None
14
+ is_optional: bool = False
15
+
16
+ def _extract_annotated(ann: Any) -> Tuple[Any, Optional[str]]:
17
+ qualifier = None
18
+ base = ann
19
+ origin = get_origin(ann)
20
+
21
+ if origin is Annotated:
22
+ args = get_args(ann)
23
+ base = args[0] if args else Any
24
+ metas = args[1:] if len(args) > 1 else ()
25
+ for m in metas:
26
+ if isinstance(m, Qualifier):
27
+ qualifier = str(m)
28
+ break
29
+ return base, qualifier
30
+
31
+ def _check_optional(ann: Any) -> Tuple[Any, bool]:
32
+ origin = get_origin(ann)
33
+ if origin is Union:
34
+ args = [a for a in get_args(ann) if a is not type(None)]
35
+ if len(args) == 1:
36
+ return args[0], True
37
+ return ann, False
38
+
39
+ def analyze_callable_dependencies(callable_obj: Callable[..., Any]) -> Tuple[DependencyRequest, ...]:
40
+ try:
41
+ sig = inspect.signature(callable_obj)
42
+ except (ValueError, TypeError):
43
+ return ()
44
+
45
+ plan: List[DependencyRequest] = []
46
+
47
+ for name, param in sig.parameters.items():
48
+ if name in ("self", "cls"):
49
+ continue
50
+ if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
51
+ continue
52
+
53
+ ann = param.annotation
54
+
55
+ base_type, is_optional = _check_optional(ann)
56
+ base_type, qualifier = _extract_annotated(base_type)
57
+
58
+ is_list = False
59
+ elem_t = None
60
+
61
+ origin = get_origin(base_type)
62
+ if origin in (list, List):
63
+ is_list = True
64
+ elem_t = get_args(base_type)[0] if get_args(base_type) else Any
65
+ elem_t, list_qualifier = _extract_annotated(elem_t)
66
+ if qualifier is None:
67
+ qualifier = list_qualifier
68
+
69
+ final_key: KeyT
70
+ if is_list:
71
+ final_key = elem_t if isinstance(elem_t, type) else Any
72
+ elif isinstance(base_type, type):
73
+ final_key = base_type
74
+ elif isinstance(base_type, str):
75
+ final_key = base_type
76
+ elif ann is inspect._empty:
77
+ final_key = name
78
+ else:
79
+ final_key = base_type
80
+
81
+ plan.append(
82
+ DependencyRequest(
83
+ parameter_name=name,
84
+ key=final_key,
85
+ is_list=is_list,
86
+ qualifier=qualifier,
87
+ is_optional=is_optional or (param.default is not inspect._empty)
88
+ )
89
+ )
90
+
91
+ return tuple(plan)
92
+
pico_ioc/aop.py CHANGED
@@ -1,9 +1,10 @@
1
1
  # src/pico_ioc/aop.py
2
+
2
3
  import inspect
3
4
  import pickle
4
5
  import threading
5
6
  from typing import Any, Callable, Dict, List, Tuple, Protocol, Union
6
- from .exceptions import SerializationError
7
+ from .exceptions import SerializationError, AsyncResolutionError
7
8
 
8
9
  KeyT = Union[str, type]
9
10
 
@@ -120,9 +121,19 @@ class UnifiedComponentProxy:
120
121
  tgt = creator()
121
122
  if tgt is None:
122
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
+
123
134
  object.__setattr__(self, "_target", tgt)
124
135
  return tgt
125
-
136
+
126
137
  def _scope_signature(self) -> Tuple[Any, ...]:
127
138
  container = object.__getattribute__(self, "_container")
128
139
  target = object.__getattribute__(self, "_target")
@@ -139,7 +150,7 @@ class UnifiedComponentProxy:
139
150
  return ()
140
151
  return (container.scopes.get_id(sc),)
141
152
  return ()
142
-
153
+
143
154
  def _build_wrapped(self, name: str, bound: Callable[..., Any], interceptors_cls: Tuple[type, ...]):
144
155
  container = object.__getattribute__(self, "_container")
145
156
  interceptors = [container.get(cls) for cls in interceptors_cls]
@@ -148,6 +159,7 @@ class UnifiedComponentProxy:
148
159
  original_func = bound
149
160
  if hasattr(bound, '__func__'):
150
161
  original_func = bound.__func__
162
+
151
163
  if inspect.iscoroutinefunction(original_func):
152
164
  async def aw(*args, **kwargs):
153
165
  ctx = MethodCtx(
@@ -182,16 +194,17 @@ 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)
193
205
  if not callable(attr):
194
206
  return attr
207
+
195
208
  interceptors_cls = _gather_interceptors_for_method(type(target), name)
196
209
  if not interceptors_cls:
197
210
  return attr
@@ -201,20 +214,16 @@ class UnifiedComponentProxy:
201
214
  cache: Dict[str, Tuple[Tuple[Any, ...], Callable[..., Any], Tuple[type, ...]]] = object.__getattribute__(self, "_cache")
202
215
  cur_sig = self._scope_signature()
203
216
  cached = cache.get(name)
217
+
204
218
  if cached is not None:
205
219
  sig, wrapped, cls_tuple = cached
206
220
  if sig == cur_sig and cls_tuple == interceptors_cls:
207
221
  return wrapped
208
222
 
209
- cached = cache.get(name)
210
- if cached is not None:
211
- sig, wrapped, cls_tuple = cached
212
- if sig == cur_sig and cls_tuple == interceptors_cls:
213
- return wrapped
214
223
  sig, wrapped, cls_tuple = self._build_wrapped(name, attr, interceptors_cls)
215
224
  cache[name] = (sig, wrapped, cls_tuple)
216
225
  return wrapped
217
-
226
+
218
227
  def __setattr__(self, name, value): setattr(self._get_real_object(), name, value)
219
228
  def __delattr__(self, name): delattr(self._get_real_object(), name)
220
229
  def __str__(self): return str(self._get_real_object())
@@ -278,4 +287,3 @@ class UnifiedComponentProxy:
278
287
  return (pickle.loads, (data,))
279
288
  except Exception as e:
280
289
  raise SerializationError(f"Proxy target is not serializable: {e}")
281
-