pico-ioc 0.2.0__py3-none-any.whl → 0.2.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 +68 -20
- pico_ioc/_version.py +1 -1
- {pico_ioc-0.2.0.dist-info → pico_ioc-0.2.1.dist-info}/METADATA +1 -1
- pico_ioc-0.2.1.dist-info/RECORD +6 -0
- pico_ioc-0.2.0.dist-info/RECORD +0 -6
- {pico_ioc-0.2.0.dist-info → pico_ioc-0.2.1.dist-info}/WHEEL +0 -0
- {pico_ioc-0.2.0.dist-info → pico_ioc-0.2.1.dist-info}/top_level.txt +0 -0
pico_ioc/__init__.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from typing import Callable, Any, Iterator, Optional, AsyncIterator
|
|
1
|
+
import functools, inspect, pkgutil, importlib, logging
|
|
2
|
+
from typing import Callable, Any, Optional
|
|
3
|
+
from contextvars import ContextVar
|
|
5
4
|
|
|
6
5
|
try:
|
|
7
6
|
# written at build time by setuptools-scm
|
|
@@ -11,6 +10,15 @@ except Exception: # pragma: no cover
|
|
|
11
10
|
|
|
12
11
|
__all__ = ["__version__"]
|
|
13
12
|
|
|
13
|
+
# ------------------------------------------------------------------------------
|
|
14
|
+
# Re-entrancy guards
|
|
15
|
+
# ------------------------------------------------------------------------------
|
|
16
|
+
# True while init/scan is running. Blocks userland container access during scan.
|
|
17
|
+
_scanning: ContextVar[bool] = ContextVar("pico_scanning", default=False)
|
|
18
|
+
|
|
19
|
+
# True while the container is resolving deps for a component (internal use allowed).
|
|
20
|
+
_resolving: ContextVar[bool] = ContextVar("pico_resolving", default=False)
|
|
21
|
+
|
|
14
22
|
# ==============================================================================
|
|
15
23
|
# --- 1. Container and Chameleon Proxy (Framework-Agnostic) ---
|
|
16
24
|
# ==============================================================================
|
|
@@ -26,6 +34,16 @@ class PicoContainer:
|
|
|
26
34
|
return key in self._providers or key in self._singletons
|
|
27
35
|
|
|
28
36
|
def get(self, key: Any) -> Any:
|
|
37
|
+
# Forbid user code calling container.get() while the scanner is importing modules.
|
|
38
|
+
# Allow only internal calls performed during dependency resolution.
|
|
39
|
+
if _scanning.get() and not _resolving.get():
|
|
40
|
+
raise RuntimeError(
|
|
41
|
+
"pico-ioc: re-entrant container access during scan. "
|
|
42
|
+
"Avoid calling init()/get() at import time (e.g., in a module body). "
|
|
43
|
+
"Move resolution to runtime (e.g., under if __name__ == '__main__':) "
|
|
44
|
+
"or delay it until pico-ioc init completes."
|
|
45
|
+
)
|
|
46
|
+
|
|
29
47
|
if key in self._singletons:
|
|
30
48
|
return self._singletons[key]
|
|
31
49
|
if key in self._providers:
|
|
@@ -143,13 +161,17 @@ def _resolve_param(container: PicoContainer, p: inspect.Parameter) -> Any:
|
|
|
143
161
|
if p.name == 'self' or p.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
144
162
|
raise RuntimeError("Invalid param for resolution")
|
|
145
163
|
|
|
164
|
+
# 1) NAME
|
|
146
165
|
if container.has(p.name):
|
|
147
166
|
return container.get(p.name)
|
|
148
167
|
|
|
149
168
|
ann = p.annotation
|
|
169
|
+
|
|
170
|
+
# 2) TYPE
|
|
150
171
|
if ann is not inspect._empty and container.has(ann):
|
|
151
172
|
return container.get(ann)
|
|
152
173
|
|
|
174
|
+
# 3) TYPE MRO
|
|
153
175
|
if ann is not inspect._empty:
|
|
154
176
|
try:
|
|
155
177
|
for base in getattr(ann, "__mro__", ())[1:]:
|
|
@@ -160,10 +182,11 @@ def _resolve_param(container: PicoContainer, p: inspect.Parameter) -> Any:
|
|
|
160
182
|
except Exception:
|
|
161
183
|
pass
|
|
162
184
|
|
|
185
|
+
# 4) str(NAME)
|
|
163
186
|
if container.has(str(p.name)):
|
|
164
187
|
return container.get(str(p.name))
|
|
165
188
|
|
|
166
|
-
key = p.name if
|
|
189
|
+
key = p.name if ann is inspect._empty else ann
|
|
167
190
|
return container.get(key)
|
|
168
191
|
|
|
169
192
|
|
|
@@ -175,21 +198,22 @@ def _scan_and_configure(
|
|
|
175
198
|
package = importlib.import_module(package_or_name) if isinstance(package_or_name, str) else package_or_name
|
|
176
199
|
logging.info(f"🚀 Scanning in '{package.__name__}'...")
|
|
177
200
|
component_classes, factory_classes = [], []
|
|
201
|
+
|
|
178
202
|
for _, name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + '.'):
|
|
179
|
-
# Skip excluded modules (used by auto_exclude_caller and custom excludes)
|
|
180
203
|
if exclude and exclude(name):
|
|
181
204
|
logging.info(f" ⏭️ Skipping module {name} (excluded)")
|
|
182
205
|
continue
|
|
183
206
|
try:
|
|
184
207
|
module = importlib.import_module(name)
|
|
185
208
|
for _, obj in inspect.getmembers(module, inspect.isclass):
|
|
186
|
-
if
|
|
209
|
+
if getattr(obj, '_is_component', False):
|
|
187
210
|
component_classes.append(obj)
|
|
188
|
-
elif
|
|
211
|
+
elif getattr(obj, '_is_factory_component', False):
|
|
189
212
|
factory_classes.append(obj)
|
|
190
213
|
except Exception as e:
|
|
191
214
|
logging.warning(f" ⚠️ Module {name} not processed: {e}")
|
|
192
215
|
|
|
216
|
+
# Register factories
|
|
193
217
|
for factory_cls in factory_classes:
|
|
194
218
|
try:
|
|
195
219
|
sig = inspect.signature(factory_cls.__init__)
|
|
@@ -200,28 +224,39 @@ def _scan_and_configure(
|
|
|
200
224
|
except Exception as e:
|
|
201
225
|
logging.error(f" ❌ Error in factory {factory_cls.__name__}: {e}", exc_info=True)
|
|
202
226
|
|
|
227
|
+
# Register components
|
|
203
228
|
for component_cls in component_classes:
|
|
204
229
|
key = getattr(component_cls, '_component_key', component_cls)
|
|
230
|
+
|
|
205
231
|
def create_component(cls=component_cls):
|
|
206
232
|
sig = inspect.signature(cls.__init__)
|
|
207
233
|
deps = {}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
234
|
+
tok = _resolving.set(True)
|
|
235
|
+
try:
|
|
236
|
+
for p in sig.parameters.values():
|
|
237
|
+
if p.name == 'self' or p.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
238
|
+
continue
|
|
239
|
+
deps[p.name] = _resolve_param(container, p)
|
|
240
|
+
finally:
|
|
241
|
+
_resolving.reset(tok)
|
|
212
242
|
return cls(**deps)
|
|
243
|
+
|
|
213
244
|
container.bind(key, create_component)
|
|
214
245
|
|
|
215
246
|
_container = None
|
|
216
247
|
|
|
217
248
|
def init(root_package, *, exclude: Optional[Callable[[str], bool]] = None, auto_exclude_caller: bool = True):
|
|
249
|
+
"""
|
|
250
|
+
Initialize the global container and scan the given root package/module.
|
|
251
|
+
While scanning, re-entrant userland access to container.get() is blocked
|
|
252
|
+
to avoid import-time side effects.
|
|
253
|
+
"""
|
|
218
254
|
global _container
|
|
219
255
|
if _container:
|
|
220
256
|
return _container
|
|
221
257
|
|
|
222
258
|
combined_exclude = exclude
|
|
223
259
|
if auto_exclude_caller:
|
|
224
|
-
# módulo que invoca a init()
|
|
225
260
|
try:
|
|
226
261
|
caller_frame = inspect.stack()[1].frame
|
|
227
262
|
caller_module = inspect.getmodule(caller_frame)
|
|
@@ -240,7 +275,13 @@ def init(root_package, *, exclude: Optional[Callable[[str], bool]] = None, auto_
|
|
|
240
275
|
|
|
241
276
|
_container = PicoContainer()
|
|
242
277
|
logging.info("🔌 Initializing 'pico-ioc'...")
|
|
243
|
-
|
|
278
|
+
|
|
279
|
+
tok = _scanning.set(True)
|
|
280
|
+
try:
|
|
281
|
+
_scan_and_configure(root_package, _container, exclude=combined_exclude)
|
|
282
|
+
finally:
|
|
283
|
+
_scanning.reset(tok)
|
|
284
|
+
|
|
244
285
|
logging.info("✅ Container configured and ready.")
|
|
245
286
|
return _container
|
|
246
287
|
|
|
@@ -252,19 +293,26 @@ def factory_component(cls):
|
|
|
252
293
|
return cls
|
|
253
294
|
|
|
254
295
|
def provides(key: Any, *, lazy: bool = True):
|
|
296
|
+
"""
|
|
297
|
+
Declare that a factory method provides a component under 'key'.
|
|
298
|
+
By default, returns a LazyProxy that instantiates upon first real use.
|
|
299
|
+
"""
|
|
255
300
|
def decorator(func):
|
|
256
301
|
@functools.wraps(func)
|
|
257
302
|
def wrapper(*args, **kwargs):
|
|
258
303
|
return LazyProxy(lambda: func(*args, **kwargs)) if lazy else func(*args, **kwargs)
|
|
259
|
-
|
|
260
|
-
setattr(wrapper, '_provides_name', key)
|
|
304
|
+
setattr(wrapper, '_provides_name', key) # legacy-compatible storage
|
|
261
305
|
return wrapper
|
|
262
306
|
return decorator
|
|
263
307
|
|
|
264
308
|
def component(cls=None, *, name: str = None):
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
309
|
+
"""
|
|
310
|
+
Mark a class as a component. Registered by class type by default,
|
|
311
|
+
or by 'name' if provided.
|
|
312
|
+
"""
|
|
313
|
+
def decorator(c):
|
|
314
|
+
setattr(c, '_is_component', True)
|
|
315
|
+
setattr(c, '_component_key', name if name is not None else c)
|
|
316
|
+
return c
|
|
269
317
|
return decorator(cls) if cls else decorator
|
|
270
318
|
|
pico_ioc/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '0.2.
|
|
1
|
+
__version__ = '0.2.1'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pico-ioc
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
Project-URL: Homepage, https://github.com/dperezcabrera/pico-ioc
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
pico_ioc/__init__.py,sha256=wCgi07l_0ZqhgZRfoRgWld_Q3_0qkqazUPWsO0XbPQI,13474
|
|
2
|
+
pico_ioc/_version.py,sha256=PmcQ2PI2oP8irnLtJLJby2YfW6sBvLAmL-VpABzTqwc,22
|
|
3
|
+
pico_ioc-0.2.1.dist-info/METADATA,sha256=xinAC7i03pGdzTtRDoSqm9Q-N2lbA-OLc7WQ0o_u5hY,6599
|
|
4
|
+
pico_ioc-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
+
pico_ioc-0.2.1.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
|
|
6
|
+
pico_ioc-0.2.1.dist-info/RECORD,,
|
pico_ioc-0.2.0.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
pico_ioc/__init__.py,sha256=JkCWRwZb_Mv86Q6gI7wJnsFuKWh0A0B7u-uuAfyeF6Y,11781
|
|
2
|
-
pico_ioc/_version.py,sha256=FVHPBGkfhbQDi_z3v0PiKJrXXqXOx0vGW_1VaqNJi7U,22
|
|
3
|
-
pico_ioc-0.2.0.dist-info/METADATA,sha256=CgJTdTIs1-MeabncD7fl9sez6t5a41jdYVea8qUDKOY,6599
|
|
4
|
-
pico_ioc-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
-
pico_ioc-0.2.0.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
|
|
6
|
-
pico_ioc-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|