pico-ioc 1.3.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/scanner.py CHANGED
@@ -1,4 +1,4 @@
1
- # pico_ioc/scanner.py
1
+ from __future__ import annotations
2
2
  import importlib
3
3
  import inspect
4
4
  import logging
@@ -6,7 +6,7 @@ import pkgutil
6
6
  from types import ModuleType
7
7
  from typing import Any, Callable, Optional, Tuple, List, Iterable
8
8
 
9
- from .plugins import run_plugin_hook
9
+ from .plugins import run_plugin_hook, PicoPlugin
10
10
  from .container import PicoContainer, Binder
11
11
  from .decorators import (
12
12
  COMPONENT_FLAG,
@@ -17,14 +17,13 @@ from .decorators import (
17
17
  PROVIDES_LAZY,
18
18
  COMPONENT_TAGS,
19
19
  PROVIDES_TAGS,
20
- INTERCEPTOR_META,
20
+ INFRA_META,
21
21
  )
22
22
  from .proxy import ComponentProxy
23
23
  from .resolver import Resolver
24
- from .plugins import PicoPlugin
25
24
  from . import _state
26
25
  from .utils import _provider_from_class, _provider_from_callable
27
-
26
+ from .config import is_config_component, build_component_instance, ConfigRegistry
28
27
 
29
28
  def scan_and_configure(
30
29
  package_or_name: Any,
@@ -32,103 +31,66 @@ def scan_and_configure(
32
31
  *,
33
32
  exclude: Optional[Callable[[str], bool]] = None,
34
33
  plugins: Tuple[PicoPlugin, ...] = (),
35
- ) -> tuple[int, int, list[tuple[Any, dict]]]:
36
- """
37
- Scan a package, bind components/factories, and collect interceptor declarations.
38
- Returns: (component_count, factory_count, interceptor_decls)
39
-
40
- interceptor_decls contains entries of the form:
41
- - (cls, meta) for class-level @interceptor on a class
42
- - (fn, meta) for module-level function with @interceptor
43
- - ((owner_cls, fn), meta) for methods on a class decorated with @interceptor
44
- """
34
+ ) -> tuple[int, int, list[tuple[type, dict]]]:
45
35
  package = _as_module(package_or_name)
46
36
  logging.info("Scanning in '%s'...", getattr(package, "__name__", repr(package)))
47
-
48
37
  binder = Binder(container)
49
38
  resolver = Resolver(container)
50
-
51
39
  run_plugin_hook(plugins, "before_scan", package, binder)
52
-
53
- comp_classes, factory_classes, interceptor_decls = _collect_decorated(
40
+ comp_classes, factory_classes, infra_decls = _collect_decorated(
54
41
  package=package,
55
42
  exclude=exclude,
56
43
  plugins=plugins,
57
44
  binder=binder,
58
45
  )
59
-
60
46
  run_plugin_hook(plugins, "after_scan", package, binder)
61
-
62
47
  _register_component_classes(classes=comp_classes, container=container, resolver=resolver)
63
48
  _register_factory_classes(factory_classes=factory_classes, container=container, resolver=resolver)
64
-
65
- return len(comp_classes), len(factory_classes), interceptor_decls
66
-
67
-
68
- # -------------------- Helpers --------------------
49
+ return len(comp_classes), len(factory_classes), infra_decls
69
50
 
70
51
  def _as_module(package_or_name: Any) -> ModuleType:
71
52
  if isinstance(package_or_name, str):
72
53
  return importlib.import_module(package_or_name)
73
54
  if hasattr(package_or_name, "__spec__"):
74
- return package_or_name # type: ignore[return-value]
55
+ return package_or_name
75
56
  raise TypeError("package_or_name must be a module or importable package name (str).")
76
57
 
77
-
78
58
  def _iter_package_modules(package: ModuleType) -> Iterable[str]:
79
- """Yield fully-qualified module names under a package (recursive)."""
80
59
  try:
81
- pkg_path = package.__path__ # type: ignore[attr-defined]
60
+ pkg_path = package.__path__
82
61
  except Exception:
83
62
  return
84
63
  prefix = package.__name__ + "."
85
64
  for _finder, name, _is_pkg in pkgutil.walk_packages(pkg_path, prefix):
86
65
  yield name
87
66
 
88
-
89
67
  def _collect_decorated(
90
68
  *,
91
69
  package: ModuleType,
92
70
  exclude: Optional[Callable[[str], bool]],
93
71
  plugins: Tuple[PicoPlugin, ...],
94
72
  binder: Binder,
95
- ) -> Tuple[List[type], List[type], List[tuple[Any, dict]]]:
73
+ ) -> Tuple[List[type], List[type], List[tuple[type, dict]]]:
96
74
  comps: List[type] = []
97
75
  facts: List[type] = []
98
- interceptors: List[tuple[Any, dict]] = []
76
+ infras: List[tuple[type, dict]] = []
99
77
 
100
78
  def _collect_from_class(cls: type):
101
- # Class decorators
102
79
  if getattr(cls, COMPONENT_FLAG, False):
103
80
  comps.append(cls)
104
81
  elif getattr(cls, FACTORY_FLAG, False):
105
82
  facts.append(cls)
106
-
107
- # Class-level interceptor (decorated class itself)
108
- meta_class = getattr(cls, INTERCEPTOR_META, None)
109
- if meta_class:
110
- interceptors.append((cls, dict(meta_class)))
111
-
112
- # Method-level interceptors
113
- for _nm, fn in inspect.getmembers(cls, predicate=inspect.isfunction):
114
- meta_m = getattr(fn, INTERCEPTOR_META, None)
115
- if meta_m:
116
- # Preserve the owner to allow proper binding (self) later
117
- 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
118
88
 
119
89
  def _visit_module(module: ModuleType):
120
- # Classes
121
90
  for _name, obj in inspect.getmembers(module, inspect.isclass):
122
91
  run_plugin_hook(plugins, "visit_class", module, obj, binder)
123
92
  _collect_from_class(obj)
124
93
 
125
- # Module-level functions that declare interceptors
126
- for _name, fn in inspect.getmembers(module, predicate=inspect.isfunction):
127
- meta = getattr(fn, INTERCEPTOR_META, None)
128
- if meta:
129
- interceptors.append((fn, dict(meta)))
130
-
131
- # Walk submodules
132
94
  for mod_name in _iter_package_modules(package):
133
95
  if exclude and exclude(mod_name):
134
96
  logging.info("Skipping module %s (excluded)", mod_name)
@@ -140,11 +102,10 @@ def _collect_decorated(
140
102
  continue
141
103
  _visit_module(module)
142
104
 
143
- # Also visit the root module itself (in case it's a single-file module)
144
105
  if not hasattr(package, "__path__"):
145
106
  _visit_module(package)
146
107
 
147
- return comps, facts, interceptors
108
+ return comps, facts, infras
148
109
 
149
110
  def _register_component_classes(
150
111
  *,
@@ -156,10 +117,17 @@ def _register_component_classes(
156
117
  key = getattr(cls, COMPONENT_KEY, cls)
157
118
  is_lazy = bool(getattr(cls, COMPONENT_LAZY, False))
158
119
  tags = tuple(getattr(cls, COMPONENT_TAGS, ()))
159
- provider = _provider_from_class(cls, resolver=resolver, lazy=is_lazy)
120
+ if is_config_component(cls):
121
+ registry: ConfigRegistry | None = getattr(container, "_config_registry", None)
122
+ def _prov(_c=cls, _reg=registry):
123
+ if _reg is None:
124
+ raise RuntimeError(f"No config registry found to build {_c.__name__}")
125
+ return build_component_instance(_c, _reg)
126
+ provider = (lambda p=_prov: ComponentProxy(p)) if is_lazy else _prov
127
+ else:
128
+ provider = _provider_from_class(cls, resolver=resolver, lazy=is_lazy)
160
129
  container.bind(key, provider, lazy=is_lazy, tags=tags)
161
130
 
162
-
163
131
  def _register_factory_classes(
164
132
  *,
165
133
  factory_classes: List[type],
@@ -168,7 +136,6 @@ def _register_factory_classes(
168
136
  ) -> None:
169
137
  for fcls in factory_classes:
170
138
  try:
171
- # Prevent accidental container access recursion while constructing factories
172
139
  tok_res = _state._resolving.set(True)
173
140
  try:
174
141
  finst = resolver.create_instance(fcls)
@@ -177,21 +144,29 @@ def _register_factory_classes(
177
144
  except Exception:
178
145
  logging.exception("Error in factory %s", fcls.__name__)
179
146
  continue
180
-
181
- for attr_name, func in inspect.getmembers(fcls, predicate=inspect.isfunction):
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
182
159
  provided_key = getattr(func, PROVIDES_KEY, None)
183
160
  if provided_key is None:
184
161
  continue
185
-
186
162
  is_lazy = bool(getattr(func, PROVIDES_LAZY, False))
187
163
  tags = tuple(getattr(func, PROVIDES_TAGS, ()))
188
-
189
- # bind the method to the concrete factory instance
190
- bound = getattr(finst, attr_name, func.__get__(finst, fcls))
164
+ if isinstance(raw, (classmethod, staticmethod)):
165
+ bound = getattr(fcls, attr_name)
166
+ else:
167
+ bound = getattr(finst, attr_name, func.__get__(finst, fcls))
191
168
  prov = _provider_from_callable(bound, owner_cls=fcls, resolver=resolver, lazy=is_lazy)
192
-
193
169
  if isinstance(provided_key, type):
194
- # Mark for aliasing policy pipeline and ensure uniqueness of the provider key
195
170
  try:
196
171
  setattr(prov, "_pico_alias_for", provided_key)
197
172
  except Exception:
pico_ioc/scope.py ADDED
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+ from .container import PicoContainer
6
+
7
+ class ScopedContainer(PicoContainer):
8
+ def __init__(self, built_container: PicoContainer, base: Optional[PicoContainer], strict: bool):
9
+ super().__init__(providers=getattr(built_container, "_providers", {}).copy())
10
+ self._active_profiles = getattr(built_container, "_active_profiles", ())
11
+ base_method_its = getattr(base, "_method_interceptors", ()) if base else ()
12
+ base_container_its = getattr(base, "_container_interceptors", ()) if base else ()
13
+ self._method_interceptors = base_method_its
14
+ self._container_interceptors = base_container_its
15
+ self._seen_interceptor_types = {type(it) for it in base_container_its}
16
+ for it in getattr(built_container, "_method_interceptors", ()):
17
+ self.add_method_interceptor(it)
18
+ for it in getattr(built_container, "_container_interceptors", ()):
19
+ self.add_container_interceptor(it)
20
+ self._base = base
21
+ self._strict = strict
22
+ if base:
23
+ self._singletons.update(getattr(base, "_singletons", {}))
24
+
25
+ def __enter__(self): return self
26
+ def __exit__(self, exc_type, exc, tb): return False
27
+
28
+ def has(self, key: Any) -> bool:
29
+ if super().has(key): return True
30
+ if not self._strict and self._base is not None:
31
+ return self._base.has(key)
32
+ return False
33
+
34
+ def get(self, key: Any):
35
+ try:
36
+ return super().get(key)
37
+ except NameError as e:
38
+ if not self._strict and self._base is not None and self._base.has(key):
39
+ return self._base.get(key)
40
+ raise e
41
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pico-ioc
3
- Version: 1.3.0
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
@@ -221,6 +221,20 @@ tox
221
221
 
222
222
  ---
223
223
 
224
+ ## 📜 Overview
225
+
226
+ See [OVERVIEW.md](.llm/OVERVIEW.md) Just need a quick summary?
227
+ ---
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
+ ---
224
238
  ## 📜 Changelog
225
239
 
226
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,,
@@ -1,20 +0,0 @@
1
- pico_ioc/__init__.py,sha256=Vl0nnRou3BZY1QSUOSyZ8-PFIFrz979DAzhXp0KYvIg,1014
2
- pico_ioc/_state.py,sha256=XG3Q8NsRN-Di5MrWn6kLzJXg25PXp2_qAzfgytNoP-s,1021
3
- pico_ioc/_version.py,sha256=zi_LaUT_OsChAtsPXbOeRpQkCohSsOyeXfavQPM0GoE,22
4
- pico_ioc/api.py,sha256=PbR9_VX3ipxgv7-vKq1TyjKC75RqyDr65QUCG4i7eeI,9172
5
- pico_ioc/builder.py,sha256=QPn1yC3JQYkjV08XpCAIFs95Bi0VQG5NZnSXKx4IgsI,10730
6
- pico_ioc/container.py,sha256=YCrjAhChHtDcPBg1Zz0PnMbY-6x6V313nA1liPNaUaM,6306
7
- pico_ioc/decorators.py,sha256=jhJxpaR9wCeBsNm1W0ziTCF3C0LGhu866-9x4IJ9-4U,3581
8
- pico_ioc/interceptors.py,sha256=-ZH-AG4h_6vUgTJGbP8YESlODhOEkdC6r82GHTRrKxk,1978
9
- pico_ioc/plugins.py,sha256=GP7WEMshggQ-FEjiShkcuLrSMxfueUnhbY9I8PcIyPU,1039
10
- pico_ioc/policy.py,sha256=XQs8Nr7aTq0xbNPGaeFiBgJCI65r5rXK4yQdhhQuLjM,12054
11
- pico_ioc/proxy.py,sha256=ZQ0g5QT32QV_v72N9oUWtQRnTh5An66GPtvaF1HtNp8,6187
12
- pico_ioc/public_api.py,sha256=E3sArCoI1xxkIw7xQBvLYAWcIoVJjcq1s0kH-0qIVDE,2383
13
- pico_ioc/resolver.py,sha256=qCzyjsfq59b_XEa5LNAgWzPniqoJYxyG-mXn2fzqFsk,5063
14
- pico_ioc/scanner.py,sha256=VjIW6e2nsYuI1pm38RX8yIGs7SUrvuKHPN5vgTss3h4,7257
15
- pico_ioc/utils.py,sha256=OyhOKnyepwGQ_uQKlQLt-fymEV1bQ6hCq4Me7h3dfco,1002
16
- pico_ioc-1.3.0.dist-info/licenses/LICENSE,sha256=N1_nOvHTM6BobYnOTNXiQkroDqCEi6EzfGBv8lWtyZ0,1077
17
- pico_ioc-1.3.0.dist-info/METADATA,sha256=GRIv4XBIAY8vFFXv0BVFyT4lVqryoUEQU9qF6jOlftc,10259
18
- pico_ioc-1.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
- pico_ioc-1.3.0.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
20
- pico_ioc-1.3.0.dist-info/RECORD,,