pico-ioc 1.3.0__py3-none-any.whl → 1.4.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/resolver.py CHANGED
@@ -1,15 +1,16 @@
1
- # pico_ioc/resolver.py (Python 3.10+)
2
-
1
+ # src/pico_ioc/resolver.py
3
2
  from __future__ import annotations
3
+
4
4
  import inspect
5
- from typing import Any, Annotated, get_args, get_origin, get_type_hints, Callable
5
+ from typing import Any, Annotated, Callable, get_args, get_origin, get_type_hints
6
6
  from contextvars import ContextVar
7
7
 
8
8
 
9
9
  _path: ContextVar[list[tuple[str, str]]] = ContextVar("pico_resolve_path", default=[])
10
10
 
11
+
11
12
  def _get_hints(obj, owner_cls=None) -> dict:
12
- """type hints with include_extras=True and correct globals/locals."""
13
+ """Return type hints with include_extras=True, using correct globals/locals."""
13
14
  mod = inspect.getmodule(obj)
14
15
  g = getattr(mod, "__dict__", {})
15
16
  l = vars(owner_cls) if owner_cls is not None else None
@@ -17,15 +18,14 @@ def _get_hints(obj, owner_cls=None) -> dict:
17
18
 
18
19
 
19
20
  def _is_collection_hint(tp) -> bool:
20
- """True if tp is a list[...] or tuple[...]."""
21
21
  origin = get_origin(tp) or tp
22
22
  return origin in (list, tuple)
23
23
 
24
24
 
25
25
  def _base_and_qualifiers_from_hint(tp):
26
26
  """
27
- Extract (base, qualifiers, container_kind) from a collection hint.
28
- Supports list[T] / tuple[T] and Annotated[T, "qual1", ...].
27
+ Extract (base, qualifiers, container_kind) from a type hint.
28
+ Supports list[T], tuple[T], Annotated[T, "qual1", ...].
29
29
  """
30
30
  origin = get_origin(tp) or tp
31
31
  args = get_args(tp) or ()
@@ -48,12 +48,13 @@ class Resolver:
48
48
  self.c = container
49
49
  self._prefer_name_first = bool(prefer_name_first)
50
50
 
51
+ # --- core resolution ---
51
52
 
52
53
  def _resolve_dependencies_for_callable(self, fn: Callable, owner_cls: Any = None) -> dict:
53
54
  sig = inspect.signature(fn)
54
55
  hints = _get_hints(fn, owner_cls=owner_cls)
55
56
  kwargs = {}
56
-
57
+
57
58
  path_owner = getattr(owner_cls, "__name__", getattr(fn, "__qualname__", "callable"))
58
59
  if fn.__name__ == "__init__" and owner_cls:
59
60
  path_owner = f"{path_owner}.__init__"
@@ -61,23 +62,18 @@ class Resolver:
61
62
  for name, param in sig.parameters.items():
62
63
  if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD) or name == "self":
63
64
  continue
64
-
65
+
65
66
  ann = hints.get(name, param.annotation)
66
67
  st = _path.get()
67
68
  _path.set(st + [(path_owner, name)])
68
69
  try:
69
- value = self._resolve_param(name, ann)
70
- kwargs[name] = value
70
+ kwargs[name] = self._resolve_param(name, ann)
71
71
  except NameError as e:
72
72
  if param.default is not inspect.Parameter.empty:
73
73
  _path.set(st)
74
74
  continue
75
-
76
- # If the error is already formatted with a chain, re-raise to preserve the full context.
77
75
  if "(required by" in str(e):
78
76
  raise
79
-
80
- # Otherwise, this is a fresh error; add the full chain for the first time.
81
77
  chain = " -> ".join(f"{owner}.{param}" for owner, param in _path.get())
82
78
  raise NameError(f"{e} (required by {chain})") from e
83
79
  finally:
@@ -87,43 +83,50 @@ class Resolver:
87
83
  return kwargs
88
84
 
89
85
  def create_instance(self, cls: type) -> Any:
90
- """Creates an instance of a class by resolving its __init__ dependencies."""
91
- constructor_kwargs = self._resolve_dependencies_for_callable(cls.__init__, owner_cls=cls)
92
- return cls(**constructor_kwargs)
86
+ """Instantiate a class by resolving its __init__ dependencies."""
87
+ ctor_kwargs = self._resolve_dependencies_for_callable(cls.__init__, owner_cls=cls)
88
+ return cls(**ctor_kwargs)
93
89
 
94
90
  def kwargs_for_callable(self, fn: Callable, *, owner_cls: Any = None) -> dict:
95
- """Resolves all keyword arguments for any callable."""
91
+ """Resolve all keyword arguments for any callable."""
96
92
  return self._resolve_dependencies_for_callable(fn, owner_cls=owner_cls)
97
93
 
94
+ # --- param resolution ---
98
95
 
99
96
  def _notify_resolve(self, key, ann, quals=()):
100
97
  for ci in getattr(self.c, "_container_interceptors", ()):
101
- try: ci.on_resolve(key, ann, tuple(quals) if quals else ())
102
- except Exception: pass
98
+ try:
99
+ ci.on_resolve(key, ann, tuple(quals) if quals else ())
100
+ except Exception:
101
+ pass
103
102
 
104
103
  def _resolve_param(self, name: str, ann: Any):
105
- # Colecciones (list/tuple)
104
+ # collections
106
105
  if _is_collection_hint(ann):
107
- base, quals, container_kind = _base_and_qualifiers_from_hint(ann)
106
+ base, quals, kind = _base_and_qualifiers_from_hint(ann)
108
107
  self._notify_resolve(base, ann, quals)
109
108
  items = self.c._resolve_all_for_base(base, qualifiers=quals)
110
- return list(items) if container_kind is list else tuple(items)
109
+ return list(items) if kind is list else tuple(items)
111
110
 
112
- # Precedencias
111
+ # precedence
113
112
  if self._prefer_name_first and self.c.has(name):
114
113
  self._notify_resolve(name, ann, ())
115
114
  return self.c.get(name)
115
+
116
116
  if ann is not inspect._empty and self.c.has(ann):
117
117
  self._notify_resolve(ann, ann, ())
118
118
  return self.c.get(ann)
119
+
119
120
  if ann is not inspect._empty and isinstance(ann, type):
120
121
  for base in ann.__mro__[1:]:
121
122
  if self.c.has(base):
122
123
  self._notify_resolve(base, ann, ())
123
124
  return self.c.get(base)
125
+
124
126
  if self.c.has(name):
125
127
  self._notify_resolve(name, ann, ())
126
128
  return self.c.get(name)
127
129
 
128
130
  missing = ann if ann is not inspect._empty else name
129
131
  raise NameError(f"No provider found for key {missing!r}")
132
+
pico_ioc/scanner.py CHANGED
@@ -1,4 +1,6 @@
1
- # pico_ioc/scanner.py
1
+ # src/pico_ioc/scanner.py
2
+ from __future__ import annotations
3
+
2
4
  import importlib
3
5
  import inspect
4
6
  import logging
@@ -6,7 +8,7 @@ import pkgutil
6
8
  from types import ModuleType
7
9
  from typing import Any, Callable, Optional, Tuple, List, Iterable
8
10
 
9
- from .plugins import run_plugin_hook
11
+ from .plugins import run_plugin_hook, PicoPlugin
10
12
  from .container import PicoContainer, Binder
11
13
  from .decorators import (
12
14
  COMPONENT_FLAG,
@@ -21,9 +23,9 @@ from .decorators import (
21
23
  )
22
24
  from .proxy import ComponentProxy
23
25
  from .resolver import Resolver
24
- from .plugins import PicoPlugin
25
26
  from . import _state
26
27
  from .utils import _provider_from_class, _provider_from_callable
28
+ from .config import is_config_component, build_component_instance, ConfigRegistry
27
29
 
28
30
 
29
31
  def scan_and_configure(
@@ -35,12 +37,12 @@ def scan_and_configure(
35
37
  ) -> tuple[int, int, list[tuple[Any, dict]]]:
36
38
  """
37
39
  Scan a package, bind components/factories, and collect interceptor declarations.
38
- Returns: (component_count, factory_count, interceptor_decls)
39
40
 
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
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
44
46
  """
45
47
  package = _as_module(package_or_name)
46
48
  logging.info("Scanning in '%s'...", getattr(package, "__name__", repr(package)))
@@ -65,7 +67,7 @@ def scan_and_configure(
65
67
  return len(comp_classes), len(factory_classes), interceptor_decls
66
68
 
67
69
 
68
- # -------------------- Helpers --------------------
70
+ # -------------------- helpers --------------------
69
71
 
70
72
  def _as_module(package_or_name: Any) -> ModuleType:
71
73
  if isinstance(package_or_name, str):
@@ -98,37 +100,30 @@ def _collect_decorated(
98
100
  interceptors: List[tuple[Any, dict]] = []
99
101
 
100
102
  def _collect_from_class(cls: type):
101
- # Class decorators
102
103
  if getattr(cls, COMPONENT_FLAG, False):
103
104
  comps.append(cls)
104
105
  elif getattr(cls, FACTORY_FLAG, False):
105
106
  facts.append(cls)
106
107
 
107
- # Class-level interceptor (decorated class itself)
108
108
  meta_class = getattr(cls, INTERCEPTOR_META, None)
109
109
  if meta_class:
110
110
  interceptors.append((cls, dict(meta_class)))
111
111
 
112
- # Method-level interceptors
113
112
  for _nm, fn in inspect.getmembers(cls, predicate=inspect.isfunction):
114
113
  meta_m = getattr(fn, INTERCEPTOR_META, None)
115
114
  if meta_m:
116
- # Preserve the owner to allow proper binding (self) later
117
115
  interceptors.append(((cls, fn), dict(meta_m)))
118
116
 
119
117
  def _visit_module(module: ModuleType):
120
- # Classes
121
118
  for _name, obj in inspect.getmembers(module, inspect.isclass):
122
119
  run_plugin_hook(plugins, "visit_class", module, obj, binder)
123
120
  _collect_from_class(obj)
124
121
 
125
- # Module-level functions that declare interceptors
126
122
  for _name, fn in inspect.getmembers(module, predicate=inspect.isfunction):
127
123
  meta = getattr(fn, INTERCEPTOR_META, None)
128
124
  if meta:
129
125
  interceptors.append((fn, dict(meta)))
130
126
 
131
- # Walk submodules
132
127
  for mod_name in _iter_package_modules(package):
133
128
  if exclude and exclude(mod_name):
134
129
  logging.info("Skipping module %s (excluded)", mod_name)
@@ -140,12 +135,12 @@ def _collect_decorated(
140
135
  continue
141
136
  _visit_module(module)
142
137
 
143
- # Also visit the root module itself (in case it's a single-file module)
144
138
  if not hasattr(package, "__path__"):
145
139
  _visit_module(package)
146
140
 
147
141
  return comps, facts, interceptors
148
142
 
143
+
149
144
  def _register_component_classes(
150
145
  *,
151
146
  classes: List[type],
@@ -156,7 +151,15 @@ def _register_component_classes(
156
151
  key = getattr(cls, COMPONENT_KEY, cls)
157
152
  is_lazy = bool(getattr(cls, COMPONENT_LAZY, False))
158
153
  tags = tuple(getattr(cls, COMPONENT_TAGS, ()))
159
- provider = _provider_from_class(cls, resolver=resolver, lazy=is_lazy)
154
+ if is_config_component(cls):
155
+ registry: ConfigRegistry | None = getattr(container, "_config_registry", None)
156
+ def _prov(_c=cls, _reg=registry):
157
+ if _reg is None:
158
+ raise RuntimeError(f"No config registry found to build {_c.__name__}")
159
+ return build_component_instance(_c, _reg)
160
+ provider = (lambda p=_prov: ComponentProxy(p)) if is_lazy else _prov
161
+ else:
162
+ provider = _provider_from_class(cls, resolver=resolver, lazy=is_lazy)
160
163
  container.bind(key, provider, lazy=is_lazy, tags=tags)
161
164
 
162
165
 
@@ -168,7 +171,6 @@ def _register_factory_classes(
168
171
  ) -> None:
169
172
  for fcls in factory_classes:
170
173
  try:
171
- # Prevent accidental container access recursion while constructing factories
172
174
  tok_res = _state._resolving.set(True)
173
175
  try:
174
176
  finst = resolver.create_instance(fcls)
@@ -186,12 +188,10 @@ def _register_factory_classes(
186
188
  is_lazy = bool(getattr(func, PROVIDES_LAZY, False))
187
189
  tags = tuple(getattr(func, PROVIDES_TAGS, ()))
188
190
 
189
- # bind the method to the concrete factory instance
190
191
  bound = getattr(finst, attr_name, func.__get__(finst, fcls))
191
192
  prov = _provider_from_callable(bound, owner_cls=fcls, resolver=resolver, lazy=is_lazy)
192
193
 
193
194
  if isinstance(provided_key, type):
194
- # Mark for aliasing policy pipeline and ensure uniqueness of the provider key
195
195
  try:
196
196
  setattr(prov, "_pico_alias_for", provided_key)
197
197
  except Exception:
pico_ioc/scope.py ADDED
@@ -0,0 +1,46 @@
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
+
11
+ self._active_profiles = getattr(built_container, "_active_profiles", ())
12
+
13
+ base_method_its = getattr(base, "_method_interceptors", ()) if base else ()
14
+ base_container_its = getattr(base, "_container_interceptors", ()) if base else ()
15
+
16
+ self._method_interceptors = base_method_its
17
+ self._container_interceptors = base_container_its
18
+ self._seen_interceptor_types = {type(it) for it in (base_method_its + base_container_its)}
19
+
20
+ for it in getattr(built_container, "_method_interceptors", ()):
21
+ self.add_method_interceptor(it)
22
+ for it in getattr(built_container, "_container_interceptors", ()):
23
+ self.add_container_interceptor(it)
24
+
25
+ self._base = base
26
+ self._strict = strict
27
+
28
+ if base:
29
+ self._singletons.update(getattr(base, "_singletons", {}))
30
+
31
+ def __enter__(self): return self
32
+ def __exit__(self, exc_type, exc, tb): return False
33
+
34
+ def has(self, key: Any) -> bool:
35
+ if super().has(key): return True
36
+ if not self._strict and self._base is not None:
37
+ return self._base.has(key)
38
+ return False
39
+
40
+ def get(self, key: Any):
41
+ try:
42
+ return super().get(key)
43
+ except NameError as e:
44
+ if not self._strict and self._base is not None and self._base.has(key):
45
+ return self._base.get(key)
46
+ raise e
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pico-ioc
3
- Version: 1.3.0
3
+ Version: 1.4.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,12 @@ 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
+
224
230
  ## 📜 Changelog
225
231
 
226
232
  See [CHANGELOG.md](./CHANGELOG.md) for version history.
@@ -0,0 +1,22 @@
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,,
@@ -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,,