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/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
- INTERCEPTOR_META,
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[Any, dict]]]:
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 # type: ignore[return-value]
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__ # type: ignore[attr-defined]
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[Any, dict]]]:
73
+ ) -> Tuple[List[type], List[type], List[tuple[type, dict]]]:
98
74
  comps: List[type] = []
99
75
  facts: List[type] = []
100
- interceptors: List[tuple[Any, dict]] = []
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
- meta_class = getattr(cls, INTERCEPTOR_META, None)
109
- if meta_class:
110
- interceptors.append((cls, dict(meta_class)))
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, interceptors
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, 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
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
- 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))
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 (base_method_its + base_container_its)}
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.4.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
@@ -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,,
@@ -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,,