pico-ioc 1.4.0__py3-none-any.whl → 1.5.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/__init__.py +21 -11
- pico_ioc/_version.py +1 -1
- pico_ioc/api.py +3 -2
- pico_ioc/builder.py +31 -115
- pico_ioc/container.py +18 -55
- pico_ioc/decorators.py +11 -49
- pico_ioc/infra.py +196 -0
- pico_ioc/interceptors.py +59 -39
- pico_ioc/proxy.py +9 -23
- pico_ioc/resolver.py +4 -35
- pico_ioc/scanner.py +30 -55
- pico_ioc/scope.py +2 -7
- {pico_ioc-1.4.0.dist-info → pico_ioc-1.5.0.dist-info}/METADATA +10 -2
- pico_ioc-1.5.0.dist-info/RECORD +23 -0
- pico_ioc-1.4.0.dist-info/RECORD +0 -22
- {pico_ioc-1.4.0.dist-info → pico_ioc-1.5.0.dist-info}/WHEEL +0 -0
- {pico_ioc-1.4.0.dist-info → pico_ioc-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-1.4.0.dist-info → pico_ioc-1.5.0.dist-info}/top_level.txt +0 -0
pico_ioc/scanner.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
# src/pico_ioc/scanner.py
|
|
2
1
|
from __future__ import annotations
|
|
3
|
-
|
|
4
2
|
import importlib
|
|
5
3
|
import inspect
|
|
6
4
|
import logging
|
|
@@ -19,7 +17,7 @@ from .decorators import (
|
|
|
19
17
|
PROVIDES_LAZY,
|
|
20
18
|
COMPONENT_TAGS,
|
|
21
19
|
PROVIDES_TAGS,
|
|
22
|
-
|
|
20
|
+
INFRA_META,
|
|
23
21
|
)
|
|
24
22
|
from .proxy import ComponentProxy
|
|
25
23
|
from .resolver import Resolver
|
|
@@ -27,103 +25,72 @@ from . import _state
|
|
|
27
25
|
from .utils import _provider_from_class, _provider_from_callable
|
|
28
26
|
from .config import is_config_component, build_component_instance, ConfigRegistry
|
|
29
27
|
|
|
30
|
-
|
|
31
28
|
def scan_and_configure(
|
|
32
29
|
package_or_name: Any,
|
|
33
30
|
container: PicoContainer,
|
|
34
31
|
*,
|
|
35
32
|
exclude: Optional[Callable[[str], bool]] = None,
|
|
36
33
|
plugins: Tuple[PicoPlugin, ...] = (),
|
|
37
|
-
) -> tuple[int, int, list[tuple[
|
|
38
|
-
"""
|
|
39
|
-
Scan a package, bind components/factories, and collect interceptor declarations.
|
|
40
|
-
|
|
41
|
-
Returns: (component_count, factory_count, interceptor_decls)
|
|
42
|
-
- interceptor_decls entries:
|
|
43
|
-
(cls, meta) for @interceptor class
|
|
44
|
-
(fn, meta) for @interceptor function
|
|
45
|
-
((owner_cls, fn), meta) for @interceptor methods
|
|
46
|
-
"""
|
|
34
|
+
) -> tuple[int, int, list[tuple[type, dict]]]:
|
|
47
35
|
package = _as_module(package_or_name)
|
|
48
36
|
logging.info("Scanning in '%s'...", getattr(package, "__name__", repr(package)))
|
|
49
|
-
|
|
50
37
|
binder = Binder(container)
|
|
51
38
|
resolver = Resolver(container)
|
|
52
|
-
|
|
53
39
|
run_plugin_hook(plugins, "before_scan", package, binder)
|
|
54
|
-
|
|
55
|
-
comp_classes, factory_classes, interceptor_decls = _collect_decorated(
|
|
40
|
+
comp_classes, factory_classes, infra_decls = _collect_decorated(
|
|
56
41
|
package=package,
|
|
57
42
|
exclude=exclude,
|
|
58
43
|
plugins=plugins,
|
|
59
44
|
binder=binder,
|
|
60
45
|
)
|
|
61
|
-
|
|
62
46
|
run_plugin_hook(plugins, "after_scan", package, binder)
|
|
63
|
-
|
|
64
47
|
_register_component_classes(classes=comp_classes, container=container, resolver=resolver)
|
|
65
48
|
_register_factory_classes(factory_classes=factory_classes, container=container, resolver=resolver)
|
|
66
|
-
|
|
67
|
-
return len(comp_classes), len(factory_classes), interceptor_decls
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
# -------------------- helpers --------------------
|
|
49
|
+
return len(comp_classes), len(factory_classes), infra_decls
|
|
71
50
|
|
|
72
51
|
def _as_module(package_or_name: Any) -> ModuleType:
|
|
73
52
|
if isinstance(package_or_name, str):
|
|
74
53
|
return importlib.import_module(package_or_name)
|
|
75
54
|
if hasattr(package_or_name, "__spec__"):
|
|
76
|
-
return package_or_name
|
|
55
|
+
return package_or_name
|
|
77
56
|
raise TypeError("package_or_name must be a module or importable package name (str).")
|
|
78
57
|
|
|
79
|
-
|
|
80
58
|
def _iter_package_modules(package: ModuleType) -> Iterable[str]:
|
|
81
|
-
"""Yield fully-qualified module names under a package (recursive)."""
|
|
82
59
|
try:
|
|
83
|
-
pkg_path = package.__path__
|
|
60
|
+
pkg_path = package.__path__
|
|
84
61
|
except Exception:
|
|
85
62
|
return
|
|
86
63
|
prefix = package.__name__ + "."
|
|
87
64
|
for _finder, name, _is_pkg in pkgutil.walk_packages(pkg_path, prefix):
|
|
88
65
|
yield name
|
|
89
66
|
|
|
90
|
-
|
|
91
67
|
def _collect_decorated(
|
|
92
68
|
*,
|
|
93
69
|
package: ModuleType,
|
|
94
70
|
exclude: Optional[Callable[[str], bool]],
|
|
95
71
|
plugins: Tuple[PicoPlugin, ...],
|
|
96
72
|
binder: Binder,
|
|
97
|
-
) -> Tuple[List[type], List[type], List[tuple[
|
|
73
|
+
) -> Tuple[List[type], List[type], List[tuple[type, dict]]]:
|
|
98
74
|
comps: List[type] = []
|
|
99
75
|
facts: List[type] = []
|
|
100
|
-
|
|
76
|
+
infras: List[tuple[type, dict]] = []
|
|
101
77
|
|
|
102
78
|
def _collect_from_class(cls: type):
|
|
103
79
|
if getattr(cls, COMPONENT_FLAG, False):
|
|
104
80
|
comps.append(cls)
|
|
105
81
|
elif getattr(cls, FACTORY_FLAG, False):
|
|
106
82
|
facts.append(cls)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
for _nm, fn in inspect.getmembers(cls, predicate=inspect.isfunction):
|
|
113
|
-
meta_m = getattr(fn, INTERCEPTOR_META, None)
|
|
114
|
-
if meta_m:
|
|
115
|
-
interceptors.append(((cls, fn), dict(meta_m)))
|
|
83
|
+
infra_m = getattr(cls, INFRA_META, None)
|
|
84
|
+
if infra_m:
|
|
85
|
+
infras.append((cls, dict(infra_m)))
|
|
86
|
+
for _nm, _fn in inspect.getmembers(cls, predicate=inspect.isfunction):
|
|
87
|
+
pass
|
|
116
88
|
|
|
117
89
|
def _visit_module(module: ModuleType):
|
|
118
90
|
for _name, obj in inspect.getmembers(module, inspect.isclass):
|
|
119
91
|
run_plugin_hook(plugins, "visit_class", module, obj, binder)
|
|
120
92
|
_collect_from_class(obj)
|
|
121
93
|
|
|
122
|
-
for _name, fn in inspect.getmembers(module, predicate=inspect.isfunction):
|
|
123
|
-
meta = getattr(fn, INTERCEPTOR_META, None)
|
|
124
|
-
if meta:
|
|
125
|
-
interceptors.append((fn, dict(meta)))
|
|
126
|
-
|
|
127
94
|
for mod_name in _iter_package_modules(package):
|
|
128
95
|
if exclude and exclude(mod_name):
|
|
129
96
|
logging.info("Skipping module %s (excluded)", mod_name)
|
|
@@ -138,8 +105,7 @@ def _collect_decorated(
|
|
|
138
105
|
if not hasattr(package, "__path__"):
|
|
139
106
|
_visit_module(package)
|
|
140
107
|
|
|
141
|
-
return comps, facts,
|
|
142
|
-
|
|
108
|
+
return comps, facts, infras
|
|
143
109
|
|
|
144
110
|
def _register_component_classes(
|
|
145
111
|
*,
|
|
@@ -162,7 +128,6 @@ def _register_component_classes(
|
|
|
162
128
|
provider = _provider_from_class(cls, resolver=resolver, lazy=is_lazy)
|
|
163
129
|
container.bind(key, provider, lazy=is_lazy, tags=tags)
|
|
164
130
|
|
|
165
|
-
|
|
166
131
|
def _register_factory_classes(
|
|
167
132
|
*,
|
|
168
133
|
factory_classes: List[type],
|
|
@@ -179,18 +144,28 @@ def _register_factory_classes(
|
|
|
179
144
|
except Exception:
|
|
180
145
|
logging.exception("Error in factory %s", fcls.__name__)
|
|
181
146
|
continue
|
|
182
|
-
|
|
183
|
-
for attr_name,
|
|
147
|
+
raw_dict = getattr(fcls, "__dict__", {})
|
|
148
|
+
for attr_name, attr in inspect.getmembers(fcls):
|
|
149
|
+
func = None
|
|
150
|
+
raw = raw_dict.get(attr_name, None)
|
|
151
|
+
if isinstance(raw, classmethod):
|
|
152
|
+
func = raw.__func__
|
|
153
|
+
elif isinstance(raw, staticmethod):
|
|
154
|
+
func = raw.__func__
|
|
155
|
+
elif inspect.isfunction(attr):
|
|
156
|
+
func = attr
|
|
157
|
+
if func is None:
|
|
158
|
+
continue
|
|
184
159
|
provided_key = getattr(func, PROVIDES_KEY, None)
|
|
185
160
|
if provided_key is None:
|
|
186
161
|
continue
|
|
187
|
-
|
|
188
162
|
is_lazy = bool(getattr(func, PROVIDES_LAZY, False))
|
|
189
163
|
tags = tuple(getattr(func, PROVIDES_TAGS, ()))
|
|
190
|
-
|
|
191
|
-
|
|
164
|
+
if isinstance(raw, (classmethod, staticmethod)):
|
|
165
|
+
bound = getattr(fcls, attr_name)
|
|
166
|
+
else:
|
|
167
|
+
bound = getattr(finst, attr_name, func.__get__(finst, fcls))
|
|
192
168
|
prov = _provider_from_callable(bound, owner_cls=fcls, resolver=resolver, lazy=is_lazy)
|
|
193
|
-
|
|
194
169
|
if isinstance(provided_key, type):
|
|
195
170
|
try:
|
|
196
171
|
setattr(prov, "_pico_alias_for", provided_key)
|
pico_ioc/scope.py
CHANGED
|
@@ -7,24 +7,18 @@ from .container import PicoContainer
|
|
|
7
7
|
class ScopedContainer(PicoContainer):
|
|
8
8
|
def __init__(self, built_container: PicoContainer, base: Optional[PicoContainer], strict: bool):
|
|
9
9
|
super().__init__(providers=getattr(built_container, "_providers", {}).copy())
|
|
10
|
-
|
|
11
10
|
self._active_profiles = getattr(built_container, "_active_profiles", ())
|
|
12
|
-
|
|
13
11
|
base_method_its = getattr(base, "_method_interceptors", ()) if base else ()
|
|
14
12
|
base_container_its = getattr(base, "_container_interceptors", ()) if base else ()
|
|
15
|
-
|
|
16
13
|
self._method_interceptors = base_method_its
|
|
17
14
|
self._container_interceptors = base_container_its
|
|
18
|
-
self._seen_interceptor_types = {type(it) for it in
|
|
19
|
-
|
|
15
|
+
self._seen_interceptor_types = {type(it) for it in base_container_its}
|
|
20
16
|
for it in getattr(built_container, "_method_interceptors", ()):
|
|
21
17
|
self.add_method_interceptor(it)
|
|
22
18
|
for it in getattr(built_container, "_container_interceptors", ()):
|
|
23
19
|
self.add_container_interceptor(it)
|
|
24
|
-
|
|
25
20
|
self._base = base
|
|
26
21
|
self._strict = strict
|
|
27
|
-
|
|
28
22
|
if base:
|
|
29
23
|
self._singletons.update(getattr(base, "_singletons", {}))
|
|
30
24
|
|
|
@@ -44,3 +38,4 @@ class ScopedContainer(PicoContainer):
|
|
|
44
38
|
if not self._strict and self._base is not None and self._base.has(key):
|
|
45
39
|
return self._base.get(key)
|
|
46
40
|
raise e
|
|
41
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pico-ioc
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
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
|
License: MIT License
|
|
@@ -224,9 +224,17 @@ tox
|
|
|
224
224
|
## 📜 Overview
|
|
225
225
|
|
|
226
226
|
See [OVERVIEW.md](.llm/OVERVIEW.md) Just need a quick summary?
|
|
227
|
-
|
|
228
227
|
---
|
|
229
228
|
|
|
229
|
+
## 🔔 Important Changes
|
|
230
|
+
|
|
231
|
+
### 1.5.0 (2025-09-17)
|
|
232
|
+
- Introduced **`@infrastructure`** classes for bootstrap-time configuration.
|
|
233
|
+
→ They can query the model, add interceptors, wrap/replace providers, and adjust tags/qualifiers.
|
|
234
|
+
- Added new **around-style interceptors** (`MethodInterceptor.invoke`, `ContainerInterceptor.around_*`) with deterministic ordering.
|
|
235
|
+
- **Removed legacy `@interceptor` API** (before/after/error style). All interceptors must be migrated to the new contracts.
|
|
236
|
+
|
|
237
|
+
---
|
|
230
238
|
## 📜 Changelog
|
|
231
239
|
|
|
232
240
|
See [CHANGELOG.md](./CHANGELOG.md) for version history.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
pico_ioc/__init__.py,sha256=Nlj8wsCtOz1uGC9eg7GICsGc4EecU2t9AkY74E1hx_s,1400
|
|
2
|
+
pico_ioc/_state.py,sha256=C98XQZIfKy98j8fzR730eUCoqSnCkRkxUS4bH7mp73c,2154
|
|
3
|
+
pico_ioc/_version.py,sha256=wShy9YfBfroz0HjRH_aNNehkEu1_PLsd_GjTU5aCDPk,22
|
|
4
|
+
pico_ioc/api.py,sha256=-zouzi8Uel7IXpiBYwra6ctqT71LLslcvU1trS4kKh8,8359
|
|
5
|
+
pico_ioc/builder.py,sha256=lwhKliYYonar4JOUi-Nfa8sej06mwSUp0bvKBJLAYes,9007
|
|
6
|
+
pico_ioc/config.py,sha256=J3k7_2vRB2HCpikzeMzT4Ut9COFM4kcydkwZorncqSk,12317
|
|
7
|
+
pico_ioc/container.py,sha256=Chbi8Mhz_OlhD03tf1l4hQq2yGDpELL0eNKtjyjn0us,6389
|
|
8
|
+
pico_ioc/decorators.py,sha256=LSMW5DYfQIV2y60E4tIVprMDUc4M7qCNCe7Pz5nN0Hg,3448
|
|
9
|
+
pico_ioc/infra.py,sha256=IvYA2kliG6opMG8SXX6eneWCQ6tBfSeSCiLq0-f9rJ8,7924
|
|
10
|
+
pico_ioc/interceptors.py,sha256=ZJjIRyTgHh9CTRpNZwTjM8iKvMLIw5OBQ7DrpF29HHY,3465
|
|
11
|
+
pico_ioc/plugins.py,sha256=GP7WEMshggQ-FEjiShkcuLrSMxfueUnhbY9I8PcIyPU,1039
|
|
12
|
+
pico_ioc/policy.py,sha256=p7maTHNfU-zoaz3j7CY4P3ry-bYfaGxAOklcTAuF6dY,8648
|
|
13
|
+
pico_ioc/proxy.py,sha256=2XR5mwNTEpYzZFUCcFJW1psSgOgEFooeO91_2wdnRnk,6403
|
|
14
|
+
pico_ioc/public_api.py,sha256=E3sArCoI1xxkIw7xQBvLYAWcIoVJjcq1s0kH-0qIVDE,2383
|
|
15
|
+
pico_ioc/resolver.py,sha256=Fi4dHY4NuqxNxZwVVtBR1lBnhZSTBd3dDXzr8lRNe-g,4191
|
|
16
|
+
pico_ioc/scanner.py,sha256=ieJ1A2UPWLqfybjPdWQowEx7ZCkN9693pa8sUc83LGE,6612
|
|
17
|
+
pico_ioc/scope.py,sha256=pdSKcO14jt7_rB5ymLpbBI9qS9FbdvTGZfPj3Mc0Znc,1705
|
|
18
|
+
pico_ioc/utils.py,sha256=OyhOKnyepwGQ_uQKlQLt-fymEV1bQ6hCq4Me7h3dfco,1002
|
|
19
|
+
pico_ioc-1.5.0.dist-info/licenses/LICENSE,sha256=N1_nOvHTM6BobYnOTNXiQkroDqCEi6EzfGBv8lWtyZ0,1077
|
|
20
|
+
pico_ioc-1.5.0.dist-info/METADATA,sha256=11GMlO6Z1S-PhiLBBHD5gTLfrpTxAR8q-HC6zFfHDwk,10844
|
|
21
|
+
pico_ioc-1.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
22
|
+
pico_ioc-1.5.0.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
|
|
23
|
+
pico_ioc-1.5.0.dist-info/RECORD,,
|
pico_ioc-1.4.0.dist-info/RECORD
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
pico_ioc/__init__.py,sha256=s_9v-pMM5X7r5vhbzaOmQQEHBLOmZCV7o6QNtRcfMAU,1282
|
|
2
|
-
pico_ioc/_state.py,sha256=C98XQZIfKy98j8fzR730eUCoqSnCkRkxUS4bH7mp73c,2154
|
|
3
|
-
pico_ioc/_version.py,sha256=EyMGX1ADFzN6XVXHWbJUtKPONYKeFkvWoKIFPDDB2I8,22
|
|
4
|
-
pico_ioc/api.py,sha256=cc9c3db6_dIfeobP_VvFRpNV9qNIsIPbUct5X9mmW9w,8348
|
|
5
|
-
pico_ioc/builder.py,sha256=ZvIpOaAzBpByw-u5V52GM5cJAMH_E_5FMDLlgyqd-g4,11379
|
|
6
|
-
pico_ioc/config.py,sha256=J3k7_2vRB2HCpikzeMzT4Ut9COFM4kcydkwZorncqSk,12317
|
|
7
|
-
pico_ioc/container.py,sha256=V9X0qvNPZYU80C65X3Dqifek6RWt9kgEwG0CkX1Hpow,6461
|
|
8
|
-
pico_ioc/decorators.py,sha256=Jyq7PhSM3uFVfBEaCq6x_mFV9V3B5fTEK4o3I6ZvG5A,4492
|
|
9
|
-
pico_ioc/interceptors.py,sha256=rBdpI7ca5L30N-zR7LKroCIc5FgfNb9M5P7OEGw-TtY,1955
|
|
10
|
-
pico_ioc/plugins.py,sha256=GP7WEMshggQ-FEjiShkcuLrSMxfueUnhbY9I8PcIyPU,1039
|
|
11
|
-
pico_ioc/policy.py,sha256=p7maTHNfU-zoaz3j7CY4P3ry-bYfaGxAOklcTAuF6dY,8648
|
|
12
|
-
pico_ioc/proxy.py,sha256=VJA-QaO8yvejcHmX5mlXMHfyuyXFxD7cazONSzBGrf0,6308
|
|
13
|
-
pico_ioc/public_api.py,sha256=E3sArCoI1xxkIw7xQBvLYAWcIoVJjcq1s0kH-0qIVDE,2383
|
|
14
|
-
pico_ioc/resolver.py,sha256=clIS9wwhOKzIwzBQFXxCrmPX2gM2X2eVyS8P_VEeyDw,4798
|
|
15
|
-
pico_ioc/scanner.py,sha256=TmDLkklO-e2LBoVducQD4-uuZKFDg_dMwgwO9vM8-pU,7129
|
|
16
|
-
pico_ioc/scope.py,sha256=5oRCir1Dqu8Jlgl_R-q900my1u6_7zq5VUbq8ahV280,1754
|
|
17
|
-
pico_ioc/utils.py,sha256=OyhOKnyepwGQ_uQKlQLt-fymEV1bQ6hCq4Me7h3dfco,1002
|
|
18
|
-
pico_ioc-1.4.0.dist-info/licenses/LICENSE,sha256=N1_nOvHTM6BobYnOTNXiQkroDqCEi6EzfGBv8lWtyZ0,1077
|
|
19
|
-
pico_ioc-1.4.0.dist-info/METADATA,sha256=DdvaybzEQOnC-HD563NJZqMDL7zwY3SnEpLGwjPxVzU,10346
|
|
20
|
-
pico_ioc-1.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
21
|
-
pico_ioc-1.4.0.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
|
|
22
|
-
pico_ioc-1.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|