pico-ioc 1.1.0__py3-none-any.whl → 1.3.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 CHANGED
@@ -1,16 +1,20 @@
1
1
  # pico_ioc/__init__.py
2
-
3
2
  try:
4
3
  from ._version import __version__
5
4
  except Exception:
6
5
  __version__ = "0.0.0"
7
6
 
8
7
  from .container import PicoContainer, Binder
9
- from .decorators import component, factory_component, provides, plugin, Qualifier, qualifier
8
+ from .decorators import (
9
+ component, factory_component, provides, plugin,
10
+ Qualifier, qualifier,
11
+ on_missing, primary, conditional, interceptor,
12
+ )
10
13
  from .plugins import PicoPlugin
11
14
  from .resolver import Resolver
12
- from .api import init, reset
13
- from .proxy import ComponentProxy
15
+ from .api import init, reset, scope, container_fingerprint
16
+ from .proxy import ComponentProxy, IoCProxy
17
+ from .interceptors import Invocation, MethodInterceptor, ContainerInterceptor
14
18
 
15
19
  __all__ = [
16
20
  "__version__",
@@ -18,14 +22,24 @@ __all__ = [
18
22
  "Binder",
19
23
  "PicoPlugin",
20
24
  "ComponentProxy",
25
+ "IoCProxy",
26
+ "Invocation",
27
+ "MethodInterceptor",
28
+ "ContainerInterceptor",
21
29
  "init",
30
+ "scope",
22
31
  "reset",
32
+ "container_fingerprint",
23
33
  "component",
24
34
  "factory_component",
25
35
  "provides",
26
36
  "plugin",
27
37
  "Qualifier",
28
38
  "qualifier",
39
+ "on_missing",
40
+ "primary",
41
+ "conditional",
42
+ "interceptor",
29
43
  "Resolver",
30
44
  ]
31
45
 
pico_ioc/_state.py CHANGED
@@ -1,10 +1,40 @@
1
1
  # pico_ioc/_state.py
2
2
  from contextvars import ContextVar
3
3
  from typing import Optional
4
+ from contextlib import contextmanager
4
5
 
5
6
  _scanning: ContextVar[bool] = ContextVar("pico_scanning", default=False)
6
7
  _resolving: ContextVar[bool] = ContextVar("pico_resolving", default=False)
7
8
 
8
9
  _container = None
9
10
  _root_name: Optional[str] = None
11
+ _fingerprint: Optional[tuple] = None
12
+ _fp_observed: bool = False
10
13
 
14
+ @contextmanager
15
+ def scanning_flag():
16
+ """Context manager: mark scanning=True within the block."""
17
+ tok = _scanning.set(True)
18
+ try:
19
+ yield
20
+ finally:
21
+ _scanning.reset(tok)
22
+
23
+ # ---- fingerprint helpers (public via api) ----
24
+ def set_fingerprint(fp: Optional[tuple]) -> None:
25
+ global _fingerprint
26
+ _fingerprint = fp
27
+
28
+ def get_fingerprint() -> Optional[tuple]:
29
+ return _fingerprint
30
+
31
+ def reset_fp_observed() -> None:
32
+ global _fp_observed
33
+ _fp_observed = False
34
+
35
+ def mark_fp_observed() -> None:
36
+ global _fp_observed
37
+ _fp_observed = True
38
+
39
+ def was_fp_observed() -> bool:
40
+ return _fp_observed
pico_ioc/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '1.1.0'
1
+ __version__ = '1.3.0'
pico_ioc/api.py CHANGED
@@ -1,140 +1,240 @@
1
- # pico_ioc/api.py
1
+ # src/pico_ioc/api.py
2
+ from __future__ import annotations
2
3
 
3
- import inspect
4
+ import inspect as _inspect
5
+ import importlib
4
6
  import logging
5
- from contextlib import contextmanager
6
- from typing import Callable, Optional, Tuple, Any, Dict # ⬅️ Any, Dict
7
+ import os
8
+ from types import ModuleType
9
+ from typing import Callable, Optional, Tuple, Any, Dict, Iterable, Sequence
7
10
 
8
- from .container import PicoContainer, Binder
11
+ from .container import PicoContainer
9
12
  from .plugins import PicoPlugin
10
- from .scanner import scan_and_configure
11
13
  from . import _state
14
+ from .builder import PicoContainerBuilder
12
15
 
13
-
16
+ # The only helpers left are those directly related to the public API signature or fingerprinting
14
17
  def reset() -> None:
15
- """Reset the global container."""
16
18
  _state._container = None
17
19
  _state._root_name = None
20
+ _state.set_fingerprint(None)
18
21
 
22
+ def _combine_excludes(a: Optional[Callable[[str], bool]], b: Optional[Callable[[str], bool]]):
23
+ if not a and not b: return None
24
+ if a and not b: return a
25
+ if b and not a: return b
26
+ return lambda mod, _a=a, _b=b: _a(mod) or _b(mod)
19
27
 
20
- def init(
21
- root_package,
22
- *,
23
- exclude: Optional[Callable[[str], bool]] = None,
24
- auto_exclude_caller: bool = True,
25
- plugins: Tuple[PicoPlugin, ...] = (),
26
- reuse: bool = True,
27
- overrides: Optional[Dict[Any, Any]] = None, # ⬅️ NUEVO
28
- ) -> PicoContainer:
29
-
30
- root_name = root_package if isinstance(root_package, str) else getattr(root_package, "__name__", None)
31
-
32
- if reuse and _state._container and _state._root_name == root_name:
33
- if overrides:
34
- _apply_overrides(_state._container, overrides)
35
- return _state._container
36
-
37
- combined_exclude = _build_exclude(exclude, auto_exclude_caller, root_name=root_name)
38
-
39
- container = PicoContainer()
40
- binder = Binder(container)
41
- logging.info("Initializing pico-ioc...")
42
-
43
- with _scanning_flag():
44
- scan_and_configure(
45
- root_package,
46
- container,
47
- exclude=combined_exclude,
48
- plugins=plugins,
49
- )
50
-
51
- if overrides:
52
- _apply_overrides(container, overrides)
53
-
54
- _run_hooks(plugins, "after_bind", container, binder)
55
- _run_hooks(plugins, "before_eager", container, binder)
56
-
57
- container.eager_instantiate_all()
58
-
59
- _run_hooks(plugins, "after_ready", container, binder)
60
-
61
- logging.info("Container configured and ready.")
62
- _state._container = container
63
- _state._root_name = root_name
64
- return container
65
-
66
-
67
- # -------------------- helpers --------------------
68
-
69
- def _apply_overrides(container: PicoContainer, overrides: Dict[Any, Any]) -> None:
70
- for key, val in overrides.items():
71
- lazy = False
72
- if isinstance(val, tuple) and len(val) == 2 and callable(val[0]) and isinstance(val[1], bool):
73
- provider = val[0]
74
- lazy = val[1]
75
- elif callable(val):
76
- provider = val
28
+ # -------- fingerprint helpers --------
29
+ def _callable_id(cb) -> tuple:
30
+ try:
31
+ mod = getattr(cb, "__module__", None)
32
+ qn = getattr(cb, "__qualname__", None)
33
+ code = getattr(cb, "__code__", None)
34
+ fn_line = getattr(code, "co_firstlineno", None) if code else None
35
+ return (mod, qn, fn_line)
36
+ except Exception:
37
+ return (repr(cb),)
38
+
39
+ def _plugins_id(plugins: Tuple[PicoPlugin, ...]) -> tuple:
40
+ out = [(type(p).__module__, type(p).__qualname__) for p in plugins or ()]
41
+ return tuple(sorted(out))
42
+
43
+ def _normalize_for_fp(value):
44
+ if isinstance(value, ModuleType):
45
+ return getattr(value, "__name__", repr(value))
46
+ if isinstance(value, (tuple, list)):
47
+ return tuple(_normalize_for_fp(v) for v in value)
48
+ if isinstance(value, set):
49
+ return tuple(sorted(_normalize_for_fp(v) for v in value))
50
+ if callable(value):
51
+ return ("callable",) + _callable_id(value)
52
+ return value
53
+
54
+ _FP_EXCLUDE_KEYS = set()
55
+
56
+ def _normalize_overrides_for_fp(overrides: Optional[Dict[Any, Any]]) -> tuple:
57
+ if not overrides:
58
+ return ()
59
+ items = []
60
+ for k, v in overrides.items():
61
+ nk = _normalize_for_fp(k)
62
+ nv = _normalize_for_fp(v)
63
+ items.append((nk, nv))
64
+ return tuple(sorted(items))
65
+
66
+ def _make_fingerprint_from_signature(locals_in_init: dict) -> tuple:
67
+ sig = _inspect.signature(init)
68
+ entries = []
69
+ for name in sig.parameters.keys():
70
+ if name in _FP_EXCLUDE_KEYS: continue
71
+ if name == "root_package":
72
+ rp = locals_in_init.get("root_package")
73
+ root_name = rp if isinstance(rp, str) else getattr(rp, "__name__", None)
74
+ entries.append(("root", root_name))
75
+ continue
76
+ val = locals_in_init.get(name, None)
77
+ if name == "plugins":
78
+ val = _plugins_id(val or ())
79
+ elif name in ("profiles", "auto_scan"):
80
+ val = tuple(val or ())
81
+ elif name in ("exclude", "auto_scan_exclude"):
82
+ val = _callable_id(val) if val else None
83
+ elif name == "overrides":
84
+ val = _normalize_overrides_for_fp(val)
77
85
  else:
78
- def provider(v=val):
79
- return v
80
- container.bind(key, provider, lazy=lazy)
86
+ val = _normalize_for_fp(val)
87
+ entries.append((name, val))
88
+ return tuple(sorted(entries))
81
89
 
90
+ # -------- container reuse and caller exclusion helpers --------
91
+ def _maybe_reuse_existing(fp: tuple, overrides: Optional[Dict[Any, Any]]) -> Optional[PicoContainer]:
92
+ if _state.get_fingerprint() == fp:
93
+ return _state._container
94
+ return None
82
95
 
83
96
  def _build_exclude(
84
- exclude: Optional[Callable[[str], bool]],
85
- auto_exclude_caller: bool,
86
- *,
87
- root_name: Optional[str] = None,
97
+ exclude: Optional[Callable[[str], bool]], auto_exclude_caller: bool, *, root_name: Optional[str] = None
88
98
  ) -> Optional[Callable[[str], bool]]:
89
- if not auto_exclude_caller:
90
- return exclude
91
-
99
+ if not auto_exclude_caller: return exclude
92
100
  caller = _get_caller_module_name()
93
- if not caller:
94
- return exclude
95
-
101
+ if not caller: return exclude
96
102
  def _under_root(mod: str) -> bool:
97
103
  return bool(root_name) and (mod == root_name or mod.startswith(root_name + "."))
98
-
99
104
  if exclude is None:
100
105
  return lambda mod, _caller=caller: (mod == _caller) and not _under_root(mod)
101
-
102
- prev = exclude
103
- return lambda mod, _caller=caller, _prev=prev: (((mod == _caller) and not _under_root(mod)) or _prev(mod))
104
-
106
+ return lambda mod, _caller=caller, _prev=exclude: (((mod == _caller) and not _under_root(mod)) or _prev(mod))
105
107
 
106
108
  def _get_caller_module_name() -> Optional[str]:
107
109
  try:
108
- f = inspect.currentframe()
109
- # frame -> _get_caller_module_name -> _build_exclude -> init
110
+ f = _inspect.currentframe()
111
+ # Stack: _get_caller -> _build_exclude -> init -> caller
110
112
  if f and f.f_back and f.f_back.f_back and f.f_back.f_back.f_back:
111
- mod = inspect.getmodule(f.f_back.f_back.f_back)
113
+ mod = _inspect.getmodule(f.f_back.f_back.f_back)
112
114
  return getattr(mod, "__name__", None)
113
115
  except Exception:
114
116
  pass
115
117
  return None
116
118
 
119
+ # ---------------- public API ----------------
120
+ def init(
121
+ root_package, *, profiles: Optional[list[str]] = None, exclude: Optional[Callable[[str], bool]] = None,
122
+ auto_exclude_caller: bool = True, plugins: Tuple[PicoPlugin, ...] = (), reuse: bool = True,
123
+ overrides: Optional[Dict[Any, Any]] = None, auto_scan: Sequence[str] = (),
124
+ auto_scan_exclude: Optional[Callable[[str], bool]] = None, strict_autoscan: bool = False,
125
+ ) -> PicoContainer:
126
+ root_name = root_package if isinstance(root_package, str) else getattr(root_package, "__name__", None)
127
+ fp = _make_fingerprint_from_signature(locals())
117
128
 
118
- def _run_hooks(
119
- plugins: Tuple[PicoPlugin, ...],
120
- hook_name: str,
121
- container: PicoContainer,
122
- binder: Binder,
123
- ) -> None:
124
- for pl in plugins:
125
- try:
126
- fn = getattr(pl, hook_name, None)
127
- if fn:
128
- fn(container, binder)
129
- except Exception:
130
- logging.exception("Plugin %s failed", hook_name)
129
+ if reuse:
130
+ reused = _maybe_reuse_existing(fp, overrides)
131
+ if reused is not None:
132
+ return reused
131
133
 
134
+ builder = PicoContainerBuilder().with_plugins(plugins).with_profiles(profiles).with_overrides(overrides)
132
135
 
133
- @contextmanager
134
- def _scanning_flag():
135
- tok = _state._scanning.set(True)
136
- try:
137
- yield
138
- finally:
139
- _state._scanning.reset(tok)
136
+ combined_exclude = _build_exclude(exclude, auto_exclude_caller, root_name=root_name)
137
+ builder.add_scan_package(root_package, exclude=combined_exclude)
138
+
139
+ if auto_scan:
140
+ for pkg in auto_scan:
141
+ try:
142
+ mod = importlib.import_module(pkg)
143
+ scan_exclude = _combine_excludes(exclude, auto_scan_exclude)
144
+ builder.add_scan_package(mod, exclude=scan_exclude)
145
+ except ImportError as e:
146
+ msg = f"pico-ioc: auto_scan package not found: {pkg}"
147
+ if strict_autoscan:
148
+ logging.error(msg)
149
+ raise e
150
+ logging.warning(msg)
151
+
152
+ container = builder.build()
153
+
154
+ _state._container = container
155
+ _state._root_name = root_name
156
+ _state.set_fingerprint(fp)
157
+ return container
140
158
 
159
+ def scope(
160
+ *, modules: Iterable[Any] = (), roots: Iterable[type] = (), profiles: Optional[list[str]] = None,
161
+ overrides: Optional[Dict[Any, Any]] = None, base: Optional[PicoContainer] = None,
162
+ include_tags: Optional[set[str]] = None, exclude_tags: Optional[set[str]] = None,
163
+ strict: bool = True, lazy: bool = True,
164
+ ) -> PicoContainer:
165
+ builder = PicoContainerBuilder()
166
+
167
+ if base is not None and not strict:
168
+ base_providers = getattr(base, "_providers", {})
169
+ builder._providers.update(base_providers)
170
+ if profiles is None:
171
+ builder.with_profiles(list(getattr(base, "_active_profiles", ())))
172
+
173
+ builder.with_profiles(profiles)\
174
+ .with_overrides(overrides)\
175
+ .with_tag_filters(include=include_tags, exclude=exclude_tags)\
176
+ .with_roots(roots)
177
+
178
+ for m in modules:
179
+ builder.add_scan_package(m)
180
+
181
+ built_container = builder.build()
182
+
183
+ scoped_container = _ScopedContainer(base=base, strict=strict, built_container=built_container)
184
+
185
+ if not lazy:
186
+ from .proxy import ComponentProxy
187
+ for rk in roots or ():
188
+ try:
189
+ obj = scoped_container.get(rk)
190
+ if isinstance(obj, ComponentProxy):
191
+ _ = obj._get_real_object()
192
+ except NameError:
193
+ if strict: raise
194
+
195
+ logging.info("Scope container ready.")
196
+ return scoped_container
197
+
198
+ class _ScopedContainer(PicoContainer):
199
+ def __init__(self, built_container: PicoContainer, base: Optional[PicoContainer], strict: bool):
200
+ super().__init__(providers=getattr(built_container, "_providers", {}).copy())
201
+
202
+ self._active_profiles = getattr(built_container, "_active_profiles", ())
203
+
204
+ base_method_its = getattr(base, "_method_interceptors", ()) if base else ()
205
+ base_container_its = getattr(base, "_container_interceptors", ()) if base else ()
206
+
207
+ self._method_interceptors = base_method_its
208
+ self._container_interceptors = base_container_its
209
+ self._seen_interceptor_types = {type(it) for it in (base_method_its + base_container_its)}
210
+
211
+ for it in getattr(built_container, "_method_interceptors", ()):
212
+ self.add_method_interceptor(it)
213
+ for it in getattr(built_container, "_container_interceptors", ()):
214
+ self.add_container_interceptor(it)
215
+
216
+ self._base = base
217
+ self._strict = strict
218
+
219
+ if base:
220
+ self._singletons.update(getattr(base, "_singletons", {}))
221
+
222
+ def __enter__(self): return self
223
+ def __exit__(self, exc_type, exc, tb): return False
224
+
225
+ def has(self, key: Any) -> bool:
226
+ if super().has(key): return True
227
+ if not self._strict and self._base is not None:
228
+ return self._base.has(key)
229
+ return False
230
+
231
+ def get(self, key: Any):
232
+ try:
233
+ return super().get(key)
234
+ except NameError as e:
235
+ if not self._strict and self._base is not None and self._base.has(key):
236
+ return self._base.get(key)
237
+ raise e
238
+
239
+ def container_fingerprint() -> Optional[tuple]:
240
+ return _state.get_fingerprint()