pico-ioc 1.5.0__py3-none-any.whl → 2.0.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
@@ -1,68 +1,93 @@
1
- try:
2
- from ._version import __version__
3
- except Exception:
4
- __version__ = "0.0.0"
5
-
6
- from .container import PicoContainer, Binder
7
- from .scope import ScopedContainer
8
- from .decorators import (
9
- component, factory_component, provides, plugin,
10
- Qualifier, qualifier,
11
- on_missing, primary, conditional, infrastructure,
12
- )
13
- from .plugins import PicoPlugin
14
- from .resolver import Resolver
15
- from .api import init, reset, scope, container_fingerprint
16
- from .proxy import ComponentProxy, IoCProxy
17
- from .interceptors import (
18
- MethodInterceptor,
19
- ContainerInterceptor,
20
- MethodCtx,
21
- ResolveCtx,
22
- CreateCtx,
1
+ # src/pico_ioc/__init__.py
2
+ from .constants import LOGGER_NAME, LOGGER, PICO_INFRA, PICO_NAME, PICO_KEY, PICO_META
3
+ from .exceptions import (
4
+ PicoError,
5
+ ProviderNotFoundError,
6
+ CircularDependencyError,
7
+ ComponentCreationError,
8
+ ScopeError,
9
+ ConfigurationError,
10
+ SerializationError,
11
+ ValidationError,
12
+ InvalidBindingError,
13
+ EventBusClosedError,
23
14
  )
24
- from .config import (
25
- config_component, EnvSource, FileSource,
26
- Env, File, Path, Value,
15
+ from .api import (
16
+ component,
17
+ factory,
18
+ provides,
19
+ Qualifier,
20
+ configuration,
21
+ configure,
22
+ cleanup,
23
+ ConfigSource,
24
+ EnvSource,
25
+ FileSource,
26
+ init,
27
+ configured,
27
28
  )
28
- from .infra import Infra, Select
29
+ from .scope import ScopeManager, ContextVarScope, ScopeProtocol, ScopedCaches
30
+ from .locator import ComponentLocator
31
+ from .factory import ComponentFactory, ProviderMetadata, DeferredProvider
32
+ from .aop import MethodCtx, MethodInterceptor, intercepted_by, UnifiedComponentProxy, health, ContainerObserver
33
+ from .container import PicoContainer
34
+ from .event_bus import EventBus, ExecPolicy, ErrorPolicy, Event, subscribe, AutoSubscriberMixin
35
+ from .config_runtime import JsonTreeSource, YamlTreeSource, DictSource, Discriminator
29
36
 
30
37
  __all__ = [
31
- "__version__",
32
- "PicoContainer",
33
- "Binder",
34
- "PicoPlugin",
35
- "ComponentProxy",
36
- "IoCProxy",
37
- "MethodInterceptor",
38
- "ContainerInterceptor",
39
- "MethodCtx",
40
- "ResolveCtx",
41
- "CreateCtx",
42
- "init",
43
- "scope",
44
- "reset",
45
- "container_fingerprint",
38
+ "LOGGER_NAME",
39
+ "LOGGER",
40
+ "PICO_INFRA",
41
+ "PICO_NAME",
42
+ "PICO_KEY",
43
+ "PICO_META",
44
+ "PicoError",
45
+ "ProviderNotFoundError",
46
+ "CircularDependencyError",
47
+ "ComponentCreationError",
48
+ "ScopeError",
49
+ "ConfigurationError",
50
+ "SerializationError",
51
+ "ValidationError",
52
+ "InvalidBindingError",
53
+ "EventBusClosedError",
46
54
  "component",
47
- "factory_component",
55
+ "factory",
48
56
  "provides",
49
- "plugin",
50
57
  "Qualifier",
51
- "qualifier",
52
- "on_missing",
53
- "primary",
54
- "conditional",
55
- "infrastructure",
56
- "Resolver",
57
- "ScopedContainer",
58
- "config_component",
58
+ "configuration",
59
+ "configure",
60
+ "cleanup",
61
+ "ScopeProtocol",
62
+ "ContextVarScope",
63
+ "ScopeManager",
64
+ "ComponentLocator",
65
+ "ScopedCaches",
66
+ "ProviderMetadata",
67
+ "ComponentFactory",
68
+ "DeferredProvider",
69
+ "MethodCtx",
70
+ "MethodInterceptor",
71
+ "intercepted_by",
72
+ "UnifiedComponentProxy",
73
+ "health",
74
+ "ContainerObserver",
75
+ "PicoContainer",
59
76
  "EnvSource",
60
77
  "FileSource",
61
- "Env",
62
- "File",
63
- "Path",
64
- "Value",
65
- "Infra",
66
- "Select",
78
+ "ConfigSource",
79
+ "init",
80
+ "configured",
81
+ "EventBus",
82
+ "ExecPolicy",
83
+ "ErrorPolicy",
84
+ "Event",
85
+ "subscribe",
86
+ "AutoSubscriberMixin",
87
+ "JsonTreeSource",
88
+ "YamlTreeSource",
89
+ "DictSource",
90
+ "Discriminator",
67
91
  ]
68
92
 
93
+
pico_ioc/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '1.5.0'
1
+ __version__ = '2.0.1'
pico_ioc/aop.py ADDED
@@ -0,0 +1,281 @@
1
+ # src/pico_ioc/aop.py
2
+ import inspect
3
+ import pickle
4
+ import threading
5
+ from typing import Any, Callable, Dict, List, Tuple, Protocol, Union
6
+ from .exceptions import SerializationError
7
+
8
+ KeyT = Union[str, type]
9
+
10
+ class MethodCtx:
11
+ __slots__ = ("instance", "cls", "method", "name", "args", "kwargs", "container", "local", "request_key")
12
+ def __init__(self, *, instance: object, cls: type, method: Callable[..., Any], name: str, args: tuple, kwargs: dict, container: Any, request_key: Any = None):
13
+ self.instance = instance
14
+ self.cls = cls
15
+ self.method = method
16
+ self.name = name
17
+ self.args = args
18
+ self.kwargs = kwargs
19
+ self.container = container
20
+ self.local: Dict[str, Any] = {}
21
+ self.request_key = request_key
22
+
23
+ class MethodInterceptor(Protocol):
24
+ def invoke(self, ctx: MethodCtx, call_next: Callable[[MethodCtx], Any]) -> Any: ...
25
+
26
+ class ContainerObserver(Protocol):
27
+ def on_resolve(self, key: KeyT, took_ms: float): ...
28
+ def on_cache_hit(self, key: KeyT): ...
29
+
30
+ def dispatch_method(interceptors: List["MethodInterceptor"], ctx: MethodCtx) -> Any:
31
+ idx = 0
32
+ def call_next(next_ctx: MethodCtx) -> Any:
33
+ nonlocal idx
34
+ if idx >= len(interceptors):
35
+ return next_ctx.method(*next_ctx.args, **next_ctx.kwargs)
36
+ interceptor = interceptors[idx]
37
+ idx += 1
38
+ return interceptor.invoke(next_ctx, call_next)
39
+ return call_next(ctx)
40
+
41
+ def intercepted_by(*interceptor_classes: type["MethodInterceptor"]):
42
+ if not interceptor_classes:
43
+ raise TypeError("intercepted_by requires at least one interceptor class")
44
+ for ic in interceptor_classes:
45
+ if not inspect.isclass(ic):
46
+ raise TypeError("intercepted_by expects interceptor classes")
47
+ def dec(fn):
48
+ if not (inspect.isfunction(fn) or inspect.ismethod(fn) or inspect.iscoroutinefunction(fn)):
49
+ raise TypeError("intercepted_by can only decorate callables")
50
+ existing = list(getattr(fn, "_pico_interceptors_", []))
51
+ for cls in interceptor_classes:
52
+ if cls not in existing:
53
+ existing.append(cls)
54
+ setattr(fn, "_pico_interceptors_", tuple(existing))
55
+ return fn
56
+ return dec
57
+
58
+ def _gather_interceptors_for_method(target_cls: type, name: str) -> Tuple[type, ...]:
59
+ try:
60
+ original = getattr(target_cls, name)
61
+ except AttributeError:
62
+ return ()
63
+ if inspect.ismethoddescriptor(original) or inspect.isbuiltin(original):
64
+ return ()
65
+ return tuple(getattr(original, "_pico_interceptors_", ()))
66
+
67
+ def health(fn):
68
+ from .constants import PICO_META
69
+ meta = getattr(fn, PICO_META, None)
70
+ if meta is None:
71
+ meta = {}
72
+ setattr(fn, PICO_META, meta)
73
+ meta["health_check"] = True
74
+ return fn
75
+
76
+ class UnifiedComponentProxy:
77
+ __slots__ = ("_target", "_creator", "_container", "_cache", "_lock")
78
+ def __init__(self, *, container: Any, target: Any = None, object_creator: Callable[[], Any] | None = None):
79
+ if container is None:
80
+ raise ValueError("UnifiedComponentProxy requires a non-null container")
81
+ if target is None and object_creator is None:
82
+ raise ValueError("UnifiedComponentProxy requires either a target or an object_creator")
83
+ object.__setattr__(self, "_container", container)
84
+ object.__setattr__(self, "_target", target)
85
+ object.__setattr__(self, "_creator", object_creator)
86
+ object.__setattr__(self, "_cache", {})
87
+ object.__setattr__(self, "_lock", threading.RLock())
88
+
89
+ def __getstate__(self):
90
+ o = self._get_real_object()
91
+ try:
92
+ data = pickle.dumps(o)
93
+ except Exception as e:
94
+ raise SerializationError(f"Proxy target is not serializable: {e}")
95
+ return {"data": data}
96
+
97
+ def __setstate__(self, state):
98
+ object.__setattr__(self, "_container", None)
99
+ object.__setattr__(self, "_creator", None)
100
+ object.__setattr__(self, "_cache", {})
101
+ object.__setattr__(self, "_lock", threading.RLock())
102
+ try:
103
+ obj = pickle.loads(state["data"])
104
+ except Exception as e:
105
+ raise SerializationError(f"Failed to restore proxy: {e}")
106
+ object.__setattr__(self, "_target", obj)
107
+
108
+ def _get_real_object(self) -> Any:
109
+ tgt = object.__getattribute__(self, "_target")
110
+ if tgt is not None:
111
+ return tgt
112
+ lock = object.__getattribute__(self, "_lock")
113
+ with lock:
114
+ tgt = object.__getattribute__(self, "_target")
115
+ if tgt is not None:
116
+ return tgt
117
+ creator = object.__getattribute__(self, "_creator")
118
+ if not callable(creator):
119
+ raise TypeError("UnifiedComponentProxy object_creator must be callable")
120
+ tgt = creator()
121
+ if tgt is None:
122
+ raise RuntimeError("UnifiedComponentProxy object_creator returned None")
123
+ object.__setattr__(self, "_target", tgt)
124
+ return tgt
125
+
126
+ def _scope_signature(self) -> Tuple[Any, ...]:
127
+ container = object.__getattribute__(self, "_container")
128
+ target = object.__getattribute__(self, "_target")
129
+ loc = getattr(container, "_locator", None)
130
+ if not loc:
131
+ return ()
132
+ if target is not None:
133
+ t = type(target)
134
+ for k, md in loc._metadata.items():
135
+ typ = md.provided_type or md.concrete_class
136
+ if isinstance(typ, type) and t is typ:
137
+ sc = md.scope
138
+ if sc == "singleton":
139
+ return ()
140
+ return (container.scopes.get_id(sc),)
141
+ return ()
142
+
143
+ def _build_wrapped(self, name: str, bound: Callable[..., Any], interceptors_cls: Tuple[type, ...]):
144
+ container = object.__getattribute__(self, "_container")
145
+ interceptors = [container.get(cls) for cls in interceptors_cls]
146
+ sig = self._scope_signature()
147
+ target = self._get_real_object()
148
+ original_func = bound
149
+ if hasattr(bound, '__func__'):
150
+ original_func = bound.__func__
151
+ if inspect.iscoroutinefunction(original_func):
152
+ async def aw(*args, **kwargs):
153
+ ctx = MethodCtx(
154
+ instance=target,
155
+ cls=type(target),
156
+ method=bound,
157
+ name=name,
158
+ args=args,
159
+ kwargs=kwargs,
160
+ container=container,
161
+ request_key=sig[0] if sig else None
162
+ )
163
+ res = dispatch_method(interceptors, ctx)
164
+ if inspect.isawaitable(res):
165
+ return await res
166
+ return res
167
+ return sig, aw, interceptors_cls
168
+ else:
169
+ def sw(*args, **kwargs):
170
+ ctx = MethodCtx(
171
+ instance=target,
172
+ cls=type(target),
173
+ method=bound,
174
+ name=name,
175
+ args=args,
176
+ kwargs=kwargs,
177
+ container=container,
178
+ request_key=sig[0] if sig else None
179
+ )
180
+ res = dispatch_method(interceptors, ctx)
181
+ if inspect.isawaitable(res):
182
+ raise RuntimeError(f"Async interceptor returned awaitable on sync method: {name}")
183
+ return res
184
+ return sig, sw, interceptors_cls
185
+
186
+ @property
187
+ def __class__(self):
188
+ return self._get_real_object().__class__
189
+
190
+ def __getattr__(self, name: str) -> Any:
191
+ target = self._get_real_object()
192
+ attr = getattr(target, name)
193
+ if not callable(attr):
194
+ return attr
195
+ interceptors_cls = _gather_interceptors_for_method(type(target), name)
196
+ if not interceptors_cls:
197
+ return attr
198
+
199
+ lock = object.__getattribute__(self, "_lock")
200
+ with lock:
201
+ cache: Dict[str, Tuple[Tuple[Any, ...], Callable[..., Any], Tuple[type, ...]]] = object.__getattribute__(self, "_cache")
202
+ cur_sig = self._scope_signature()
203
+ cached = cache.get(name)
204
+ if cached is not None:
205
+ sig, wrapped, cls_tuple = cached
206
+ if sig == cur_sig and cls_tuple == interceptors_cls:
207
+ return wrapped
208
+
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
+ sig, wrapped, cls_tuple = self._build_wrapped(name, attr, interceptors_cls)
215
+ cache[name] = (sig, wrapped, cls_tuple)
216
+ return wrapped
217
+
218
+ def __setattr__(self, name, value): setattr(self._get_real_object(), name, value)
219
+ def __delattr__(self, name): delattr(self._get_real_object(), name)
220
+ def __str__(self): return str(self._get_real_object())
221
+ def __repr__(self): return repr(self._get_real_object())
222
+ def __dir__(self): return dir(self._get_real_object())
223
+ def __len__(self): return len(self._get_real_object())
224
+ def __getitem__(self, key): return self._get_real_object()[key]
225
+ def __setitem__(self, key, value): self._get_real_object()[key] = value
226
+ def __delitem__(self, key): del self._get_real_object()[key]
227
+ def __iter__(self): return iter(self._get_real_object())
228
+ def __reversed__(self): return reversed(self._get_real_object())
229
+ def __contains__(self, item): return item in self._get_real_object()
230
+ def __add__(self, other): return self._get_real_object() + other
231
+ def __sub__(self, other): return self._get_real_object() - other
232
+ def __mul__(self, other): return self._get_real_object() * other
233
+ def __matmul__(self, other): return self._get_real_object() @ other
234
+ def __truediv__(self, other): return self._get_real_object() / other
235
+ def __floordiv__(self, other): return self._get_real_object() // other
236
+ def __mod__(self, other): return self._get_real_object() % other
237
+ def __divmod__(self, other): return divmod(self._get_real_object(), other)
238
+ def __pow__(self, other, modulo=None): return pow(self._get_real_object(), other, modulo)
239
+ def __lshift__(self, other): return self._get_real_object() << other
240
+ def __rshift__(self, other): return self._get_real_object() >> other
241
+ def __and__(self, other): return self._get_real_object() & other
242
+ def __xor__(self, other): return self._get_real_object() ^ other
243
+ def __or__(self, other): return self._get_real_object() | other
244
+ def __radd__(self, other): return other + self._get_real_object()
245
+ def __rsub__(self, other): return other - self._get_real_object()
246
+ def __rmul__(self, other): return other * self._get_real_object()
247
+ def __rmatmul__(self, other): return other @ self._get_real_object()
248
+ def __rtruediv__(self, other): return other / self._get_real_object()
249
+ def __rfloordiv__(self, other): return other // self._get_real_object()
250
+ def __rmod__(self, other): return other % self._get_real_object()
251
+ def __rdivmod__(self, other): return divmod(other, self._get_real_object())
252
+ def __rpow__(self, other): return pow(other, self._get_real_object())
253
+ def __rlshift__(self, other): return other << self._get_real_object()
254
+ def __rrshift__(self, other): return other >> self._get_real_object()
255
+ def __rand__(self, other): return other & self._get_real_object()
256
+ def __rxor__(self, other): return other ^ self._get_real_object()
257
+ def __ror__(self, other): return other | self._get_real_object()
258
+ def __neg__(self): return -self._get_real_object()
259
+ def __pos__(self): return +self._get_real_object()
260
+ def __abs__(self): return abs(self._get_real_object())
261
+ def __invert__(self): return ~self._get_real_object()
262
+ def __eq__(self, other): return self._get_real_object() == other
263
+ def __ne__(self, other): return self._get_real_object() != other
264
+ def __lt__(self, other): return self._get_real_object() < other
265
+ def __le__(self, other): return self._get_real_object() <= other
266
+ def __gt__(self, other): return self._get_real_object() > other
267
+ def __ge__(self, other): return self._get_real_object() >= other
268
+ def __hash__(self): return hash(self._get_real_object())
269
+ def __bool__(self): return bool(self._get_real_object())
270
+ def __call__(self, *args, **kwargs): return self._get_real_object()(*args, **kwargs)
271
+ def __enter__(self): return self._get_real_object().__enter__()
272
+ def __exit__(self, exc_type, exc_val, exc_tb): return self._get_real_object().__exit__(exc_type, exc_val, exc_tb)
273
+
274
+ def __reduce_ex__(self, protocol):
275
+ o = self._get_real_object()
276
+ try:
277
+ data = pickle.dumps(o, protocol=protocol)
278
+ return (pickle.loads, (data,))
279
+ except Exception as e:
280
+ raise SerializationError(f"Proxy target is not serializable: {e}")
281
+