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 +18 -4
- pico_ioc/_state.py +30 -0
- pico_ioc/_version.py +1 -1
- pico_ioc/api.py +206 -106
- pico_ioc/builder.py +242 -0
- pico_ioc/container.py +62 -34
- pico_ioc/decorators.py +76 -18
- pico_ioc/interceptors.py +50 -0
- pico_ioc/plugins.py +17 -1
- pico_ioc/policy.py +332 -0
- pico_ioc/proxy.py +41 -1
- pico_ioc/resolver.py +52 -33
- pico_ioc/scanner.py +76 -111
- pico_ioc/utils.py +25 -0
- pico_ioc-1.3.0.dist-info/METADATA +235 -0
- pico_ioc-1.3.0.dist-info/RECORD +20 -0
- pico_ioc/typing_utils.py +0 -29
- pico_ioc-1.1.0.dist-info/METADATA +0 -166
- pico_ioc-1.1.0.dist-info/RECORD +0 -17
- {pico_ioc-1.1.0.dist-info → pico_ioc-1.3.0.dist-info}/WHEEL +0 -0
- {pico_ioc-1.1.0.dist-info → pico_ioc-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-1.1.0.dist-info → pico_ioc-1.3.0.dist-info}/top_level.txt +0 -0
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
|
|
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
|
+
__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
|
-
|
|
6
|
-
from
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
elif
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 =
|
|
109
|
-
#
|
|
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 =
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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()
|