pico-ioc 1.2.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/__init__.py +30 -4
- pico_ioc/_state.py +69 -4
- pico_ioc/_version.py +1 -1
- pico_ioc/api.py +183 -251
- pico_ioc/builder.py +294 -0
- pico_ioc/config.py +332 -0
- pico_ioc/container.py +73 -26
- pico_ioc/decorators.py +88 -9
- pico_ioc/interceptors.py +56 -0
- pico_ioc/plugins.py +17 -1
- pico_ioc/policy.py +245 -0
- pico_ioc/proxy.py +59 -7
- pico_ioc/resolver.py +54 -46
- pico_ioc/scanner.py +75 -102
- pico_ioc/scope.py +46 -0
- pico_ioc/utils.py +25 -0
- {pico_ioc-1.2.0.dist-info → pico_ioc-1.4.0.dist-info}/METADATA +65 -16
- pico_ioc-1.4.0.dist-info/RECORD +22 -0
- pico_ioc/typing_utils.py +0 -29
- pico_ioc-1.2.0.dist-info/RECORD +0 -17
- {pico_ioc-1.2.0.dist-info → pico_ioc-1.4.0.dist-info}/WHEEL +0 -0
- {pico_ioc-1.2.0.dist-info → pico_ioc-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-1.2.0.dist-info → pico_ioc-1.4.0.dist-info}/top_level.txt +0 -0
pico_ioc/proxy.py
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
|
-
# pico_ioc/proxy.py
|
|
1
|
+
# src/pico_ioc/proxy.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import inspect
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from typing import Any, Callable, Sequence
|
|
7
|
+
|
|
8
|
+
from .interceptors import Invocation, dispatch, MethodInterceptor
|
|
2
9
|
|
|
3
|
-
from typing import Any, Callable
|
|
4
10
|
|
|
5
11
|
class ComponentProxy:
|
|
12
|
+
"""Proxy for lazy components. Creates the real object only when accessed."""
|
|
6
13
|
def __init__(self, object_creator: Callable[[], Any]):
|
|
7
14
|
object.__setattr__(self, "_object_creator", object_creator)
|
|
8
15
|
object.__setattr__(self, "__real_object", None)
|
|
9
16
|
|
|
10
17
|
def _get_real_object(self) -> Any:
|
|
11
|
-
|
|
12
|
-
if
|
|
13
|
-
|
|
14
|
-
object.__setattr__(self, "__real_object",
|
|
15
|
-
return
|
|
18
|
+
real = object.__getattribute__(self, "__real_object")
|
|
19
|
+
if real is None:
|
|
20
|
+
real = object.__getattribute__(self, "_object_creator")()
|
|
21
|
+
object.__setattr__(self, "__real_object", real)
|
|
22
|
+
return real
|
|
16
23
|
|
|
17
24
|
@property
|
|
18
25
|
def __class__(self):
|
|
@@ -24,6 +31,8 @@ class ComponentProxy:
|
|
|
24
31
|
def __str__(self): return str(self._get_real_object())
|
|
25
32
|
def __repr__(self): return repr(self._get_real_object())
|
|
26
33
|
def __dir__(self): return dir(self._get_real_object())
|
|
34
|
+
|
|
35
|
+
# container-like behavior
|
|
27
36
|
def __len__(self): return len(self._get_real_object())
|
|
28
37
|
def __getitem__(self, key): return self._get_real_object()[key]
|
|
29
38
|
def __setitem__(self, key, value): self._get_real_object()[key] = value
|
|
@@ -31,6 +40,8 @@ class ComponentProxy:
|
|
|
31
40
|
def __iter__(self): return iter(self._get_real_object())
|
|
32
41
|
def __reversed__(self): return reversed(self._get_real_object())
|
|
33
42
|
def __contains__(self, item): return item in self._get_real_object()
|
|
43
|
+
|
|
44
|
+
# operators
|
|
34
45
|
def __add__(self, other): return self._get_real_object() + other
|
|
35
46
|
def __sub__(self, other): return self._get_real_object() - other
|
|
36
47
|
def __mul__(self, other): return self._get_real_object() * other
|
|
@@ -45,6 +56,8 @@ class ComponentProxy:
|
|
|
45
56
|
def __and__(self, other): return self._get_real_object() & other
|
|
46
57
|
def __xor__(self, other): return self._get_real_object() ^ other
|
|
47
58
|
def __or__(self, other): return self._get_real_object() | other
|
|
59
|
+
|
|
60
|
+
# reflected operators
|
|
48
61
|
def __radd__(self, other): return other + self._get_real_object()
|
|
49
62
|
def __rsub__(self, other): return other - self._get_real_object()
|
|
50
63
|
def __rmul__(self, other): return other * self._get_real_object()
|
|
@@ -59,6 +72,8 @@ class ComponentProxy:
|
|
|
59
72
|
def __rand__(self, other): return other & self._get_real_object()
|
|
60
73
|
def __rxor__(self, other): return other ^ self._get_real_object()
|
|
61
74
|
def __ror__(self, other): return other | self._get_real_object()
|
|
75
|
+
|
|
76
|
+
# misc
|
|
62
77
|
def __neg__(self): return -self._get_real_object()
|
|
63
78
|
def __pos__(self): return +self._get_real_object()
|
|
64
79
|
def __abs__(self): return abs(self._get_real_object())
|
|
@@ -71,7 +86,44 @@ class ComponentProxy:
|
|
|
71
86
|
def __ge__(self, other): return self._get_real_object() >= other
|
|
72
87
|
def __hash__(self): return hash(self._get_real_object())
|
|
73
88
|
def __bool__(self): return bool(self._get_real_object())
|
|
89
|
+
|
|
90
|
+
# callables & context
|
|
74
91
|
def __call__(self, *args, **kwargs): return self._get_real_object()(*args, **kwargs)
|
|
75
92
|
def __enter__(self): return self._get_real_object().__enter__()
|
|
76
93
|
def __exit__(self, exc_type, exc_val, exc_tb): return self._get_real_object().__exit__(exc_type, exc_val, exc_tb)
|
|
77
94
|
|
|
95
|
+
|
|
96
|
+
class IoCProxy:
|
|
97
|
+
"""Proxy that wraps an object and applies MethodInterceptors on method calls."""
|
|
98
|
+
__slots__ = ("_target", "_interceptors")
|
|
99
|
+
|
|
100
|
+
def __init__(self, target: object, interceptors: Sequence[MethodInterceptor]):
|
|
101
|
+
self._target = target
|
|
102
|
+
self._interceptors = tuple(interceptors)
|
|
103
|
+
|
|
104
|
+
def __getattr__(self, name: str) -> Any:
|
|
105
|
+
attr = getattr(self._target, name)
|
|
106
|
+
if not callable(attr):
|
|
107
|
+
return attr
|
|
108
|
+
if hasattr(attr, "__get__"):
|
|
109
|
+
bound_fn = attr.__get__(self._target, type(self._target))
|
|
110
|
+
else:
|
|
111
|
+
bound_fn = attr
|
|
112
|
+
|
|
113
|
+
@lru_cache(maxsize=None)
|
|
114
|
+
def _wrap(fn: Callable[..., Any]):
|
|
115
|
+
if inspect.iscoroutinefunction(fn):
|
|
116
|
+
async def aw(*args, **kwargs):
|
|
117
|
+
inv = Invocation(self._target, name, fn, args, kwargs)
|
|
118
|
+
return await dispatch(self._interceptors, inv)
|
|
119
|
+
return aw
|
|
120
|
+
else:
|
|
121
|
+
def sw(*args, **kwargs):
|
|
122
|
+
inv = Invocation(self._target, name, fn, args, kwargs)
|
|
123
|
+
res = dispatch(self._interceptors, inv)
|
|
124
|
+
if inspect.isawaitable(res):
|
|
125
|
+
raise RuntimeError(f"Async interceptor on sync method: {name}")
|
|
126
|
+
return res
|
|
127
|
+
return sw
|
|
128
|
+
return _wrap(bound_fn)
|
|
129
|
+
|
pico_ioc/resolver.py
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
# pico_ioc/resolver.py
|
|
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
|
|
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
|
_path: ContextVar[list[tuple[str, str]]] = ContextVar("pico_resolve_path", default=[])
|
|
9
10
|
|
|
11
|
+
|
|
10
12
|
def _get_hints(obj, owner_cls=None) -> dict:
|
|
11
|
-
"""type hints with include_extras=True
|
|
13
|
+
"""Return type hints with include_extras=True, using correct globals/locals."""
|
|
12
14
|
mod = inspect.getmodule(obj)
|
|
13
15
|
g = getattr(mod, "__dict__", {})
|
|
14
16
|
l = vars(owner_cls) if owner_cls is not None else None
|
|
@@ -16,15 +18,14 @@ def _get_hints(obj, owner_cls=None) -> dict:
|
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
def _is_collection_hint(tp) -> bool:
|
|
19
|
-
"""True if tp is a list[...] or tuple[...]."""
|
|
20
21
|
origin = get_origin(tp) or tp
|
|
21
22
|
return origin in (list, tuple)
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
def _base_and_qualifiers_from_hint(tp):
|
|
25
26
|
"""
|
|
26
|
-
Extract (base, qualifiers, container_kind) from a
|
|
27
|
-
Supports list[T]
|
|
27
|
+
Extract (base, qualifiers, container_kind) from a type hint.
|
|
28
|
+
Supports list[T], tuple[T], Annotated[T, "qual1", ...].
|
|
28
29
|
"""
|
|
29
30
|
origin = get_origin(tp) or tp
|
|
30
31
|
args = get_args(tp) or ()
|
|
@@ -47,78 +48,85 @@ class Resolver:
|
|
|
47
48
|
self.c = container
|
|
48
49
|
self._prefer_name_first = bool(prefer_name_first)
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
sig = inspect.signature(cls.__init__)
|
|
52
|
-
hints = _get_hints(cls.__init__, owner_cls=cls)
|
|
53
|
-
kwargs = {}
|
|
54
|
-
for name, param in sig.parameters.items():
|
|
55
|
-
if name == "self" or param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
56
|
-
continue
|
|
57
|
-
ann = hints.get(name, param.annotation)
|
|
58
|
-
st = _path.get()
|
|
59
|
-
_path.set(st + [(cls.__name__, name)])
|
|
60
|
-
try:
|
|
61
|
-
value = self._resolve_param(name, ann)
|
|
62
|
-
except NameError as e:
|
|
63
|
-
# ⬅️ Important: skip if parameter has a default
|
|
64
|
-
if param.default is not inspect._empty:
|
|
65
|
-
continue
|
|
66
|
-
chain = " -> ".join(f"{c}.__init__.{p}" for c, p in _path.get())
|
|
67
|
-
raise NameError(f"{e} (required by {chain})") from e
|
|
68
|
-
finally:
|
|
69
|
-
cur = _path.get()
|
|
70
|
-
_path.set(cur[:-1] if cur else [])
|
|
71
|
-
kwargs[name] = value
|
|
72
|
-
return cls(**kwargs)
|
|
51
|
+
# --- core resolution ---
|
|
73
52
|
|
|
74
|
-
def
|
|
53
|
+
def _resolve_dependencies_for_callable(self, fn: Callable, owner_cls: Any = None) -> dict:
|
|
75
54
|
sig = inspect.signature(fn)
|
|
76
55
|
hints = _get_hints(fn, owner_cls=owner_cls)
|
|
77
56
|
kwargs = {}
|
|
78
|
-
|
|
57
|
+
|
|
58
|
+
path_owner = getattr(owner_cls, "__name__", getattr(fn, "__qualname__", "callable"))
|
|
59
|
+
if fn.__name__ == "__init__" and owner_cls:
|
|
60
|
+
path_owner = f"{path_owner}.__init__"
|
|
61
|
+
|
|
79
62
|
for name, param in sig.parameters.items():
|
|
80
|
-
if
|
|
63
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD) or name == "self":
|
|
81
64
|
continue
|
|
65
|
+
|
|
82
66
|
ann = hints.get(name, param.annotation)
|
|
83
67
|
st = _path.get()
|
|
84
|
-
_path.set(st + [(
|
|
68
|
+
_path.set(st + [(path_owner, name)])
|
|
85
69
|
try:
|
|
86
|
-
|
|
70
|
+
kwargs[name] = self._resolve_param(name, ann)
|
|
87
71
|
except NameError as e:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
# do not include in kwargs
|
|
91
|
-
_path.set(st) # pop before continue
|
|
72
|
+
if param.default is not inspect.Parameter.empty:
|
|
73
|
+
_path.set(st)
|
|
92
74
|
continue
|
|
93
|
-
|
|
75
|
+
if "(required by" in str(e):
|
|
76
|
+
raise
|
|
77
|
+
chain = " -> ".join(f"{owner}.{param}" for owner, param in _path.get())
|
|
94
78
|
raise NameError(f"{e} (required by {chain})") from e
|
|
95
79
|
finally:
|
|
96
80
|
cur = _path.get()
|
|
97
|
-
|
|
98
|
-
|
|
81
|
+
if cur:
|
|
82
|
+
_path.set(cur[:-1])
|
|
99
83
|
return kwargs
|
|
100
84
|
|
|
85
|
+
def create_instance(self, cls: type) -> Any:
|
|
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)
|
|
89
|
+
|
|
90
|
+
def kwargs_for_callable(self, fn: Callable, *, owner_cls: Any = None) -> dict:
|
|
91
|
+
"""Resolve all keyword arguments for any callable."""
|
|
92
|
+
return self._resolve_dependencies_for_callable(fn, owner_cls=owner_cls)
|
|
93
|
+
|
|
94
|
+
# --- param resolution ---
|
|
95
|
+
|
|
96
|
+
def _notify_resolve(self, key, ann, quals=()):
|
|
97
|
+
for ci in getattr(self.c, "_container_interceptors", ()):
|
|
98
|
+
try:
|
|
99
|
+
ci.on_resolve(key, ann, tuple(quals) if quals else ())
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
|
|
101
103
|
def _resolve_param(self, name: str, ann: Any):
|
|
102
|
-
# collections
|
|
104
|
+
# collections
|
|
103
105
|
if _is_collection_hint(ann):
|
|
104
|
-
base, quals,
|
|
106
|
+
base, quals, kind = _base_and_qualifiers_from_hint(ann)
|
|
107
|
+
self._notify_resolve(base, ann, quals)
|
|
105
108
|
items = self.c._resolve_all_for_base(base, qualifiers=quals)
|
|
106
|
-
return list(items) if
|
|
109
|
+
return list(items) if kind is list else tuple(items)
|
|
107
110
|
|
|
108
|
-
# precedence
|
|
111
|
+
# precedence
|
|
109
112
|
if self._prefer_name_first and self.c.has(name):
|
|
113
|
+
self._notify_resolve(name, ann, ())
|
|
110
114
|
return self.c.get(name)
|
|
111
115
|
|
|
112
116
|
if ann is not inspect._empty and self.c.has(ann):
|
|
117
|
+
self._notify_resolve(ann, ann, ())
|
|
113
118
|
return self.c.get(ann)
|
|
114
119
|
|
|
115
120
|
if ann is not inspect._empty and isinstance(ann, type):
|
|
116
121
|
for base in ann.__mro__[1:]:
|
|
117
122
|
if self.c.has(base):
|
|
123
|
+
self._notify_resolve(base, ann, ())
|
|
118
124
|
return self.c.get(base)
|
|
119
125
|
|
|
120
126
|
if self.c.has(name):
|
|
127
|
+
self._notify_resolve(name, ann, ())
|
|
121
128
|
return self.c.get(name)
|
|
122
129
|
|
|
123
130
|
missing = ann if ann is not inspect._empty else name
|
|
124
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,6 +8,7 @@ import pkgutil
|
|
|
6
8
|
from types import ModuleType
|
|
7
9
|
from typing import Any, Callable, Optional, Tuple, List, Iterable
|
|
8
10
|
|
|
11
|
+
from .plugins import run_plugin_hook, PicoPlugin
|
|
9
12
|
from .container import PicoContainer, Binder
|
|
10
13
|
from .decorators import (
|
|
11
14
|
COMPONENT_FLAG,
|
|
@@ -14,13 +17,15 @@ from .decorators import (
|
|
|
14
17
|
FACTORY_FLAG,
|
|
15
18
|
PROVIDES_KEY,
|
|
16
19
|
PROVIDES_LAZY,
|
|
17
|
-
COMPONENT_TAGS,
|
|
20
|
+
COMPONENT_TAGS,
|
|
18
21
|
PROVIDES_TAGS,
|
|
22
|
+
INTERCEPTOR_META,
|
|
19
23
|
)
|
|
20
24
|
from .proxy import ComponentProxy
|
|
21
25
|
from .resolver import Resolver
|
|
22
|
-
from .plugins import PicoPlugin
|
|
23
26
|
from . import _state
|
|
27
|
+
from .utils import _provider_from_class, _provider_from_callable
|
|
28
|
+
from .config import is_config_component, build_component_instance, ConfigRegistry
|
|
24
29
|
|
|
25
30
|
|
|
26
31
|
def scan_and_configure(
|
|
@@ -29,15 +34,15 @@ def scan_and_configure(
|
|
|
29
34
|
*,
|
|
30
35
|
exclude: Optional[Callable[[str], bool]] = None,
|
|
31
36
|
plugins: Tuple[PicoPlugin, ...] = (),
|
|
32
|
-
) ->
|
|
37
|
+
) -> tuple[int, int, list[tuple[Any, dict]]]:
|
|
33
38
|
"""
|
|
34
|
-
Scan a package,
|
|
39
|
+
Scan a package, bind components/factories, and collect interceptor declarations.
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
41
46
|
"""
|
|
42
47
|
package = _as_module(package_or_name)
|
|
43
48
|
logging.info("Scanning in '%s'...", getattr(package, "__name__", repr(package)))
|
|
@@ -45,34 +50,26 @@ def scan_and_configure(
|
|
|
45
50
|
binder = Binder(container)
|
|
46
51
|
resolver = Resolver(container)
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
run_plugin_hook(plugins, "before_scan", package, binder)
|
|
49
54
|
|
|
50
|
-
comp_classes, factory_classes =
|
|
55
|
+
comp_classes, factory_classes, interceptor_decls = _collect_decorated(
|
|
51
56
|
package=package,
|
|
52
57
|
exclude=exclude,
|
|
53
58
|
plugins=plugins,
|
|
54
59
|
binder=binder,
|
|
55
60
|
)
|
|
56
61
|
|
|
57
|
-
|
|
62
|
+
run_plugin_hook(plugins, "after_scan", package, binder)
|
|
58
63
|
|
|
59
|
-
_register_component_classes(
|
|
60
|
-
|
|
61
|
-
container=container,
|
|
62
|
-
resolver=resolver,
|
|
63
|
-
)
|
|
64
|
+
_register_component_classes(classes=comp_classes, container=container, resolver=resolver)
|
|
65
|
+
_register_factory_classes(factory_classes=factory_classes, container=container, resolver=resolver)
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
factory_classes=factory_classes,
|
|
67
|
-
container=container,
|
|
68
|
-
resolver=resolver,
|
|
69
|
-
)
|
|
67
|
+
return len(comp_classes), len(factory_classes), interceptor_decls
|
|
70
68
|
|
|
71
69
|
|
|
72
|
-
# --------------------
|
|
70
|
+
# -------------------- helpers --------------------
|
|
73
71
|
|
|
74
72
|
def _as_module(package_or_name: Any) -> ModuleType:
|
|
75
|
-
"""Return a module from either a module object or an importable string name."""
|
|
76
73
|
if isinstance(package_or_name, str):
|
|
77
74
|
return importlib.import_module(package_or_name)
|
|
78
75
|
if hasattr(package_or_name, "__spec__"):
|
|
@@ -80,84 +77,68 @@ def _as_module(package_or_name: Any) -> ModuleType:
|
|
|
80
77
|
raise TypeError("package_or_name must be a module or importable package name (str).")
|
|
81
78
|
|
|
82
79
|
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
hook_name: str,
|
|
86
|
-
*args,
|
|
87
|
-
**kwargs,
|
|
88
|
-
) -> None:
|
|
89
|
-
"""Run a lifecycle hook across all plugins, logging (but not raising) exceptions."""
|
|
90
|
-
for pl in plugins:
|
|
91
|
-
try:
|
|
92
|
-
fn = getattr(pl, hook_name, None)
|
|
93
|
-
if fn:
|
|
94
|
-
fn(*args, **kwargs)
|
|
95
|
-
except Exception:
|
|
96
|
-
logging.exception("Plugin %s failed", hook_name)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def _iter_package_modules(
|
|
100
|
-
package: ModuleType,
|
|
101
|
-
) -> Iterable[str]:
|
|
102
|
-
"""
|
|
103
|
-
Yield fully qualified module names under the given package.
|
|
104
|
-
|
|
105
|
-
Requires the package to have a __path__ (i.e., be a package, not a single module).
|
|
106
|
-
"""
|
|
80
|
+
def _iter_package_modules(package: ModuleType) -> Iterable[str]:
|
|
81
|
+
"""Yield fully-qualified module names under a package (recursive)."""
|
|
107
82
|
try:
|
|
108
83
|
pkg_path = package.__path__ # type: ignore[attr-defined]
|
|
109
84
|
except Exception:
|
|
110
|
-
return
|
|
111
|
-
|
|
85
|
+
return
|
|
112
86
|
prefix = package.__name__ + "."
|
|
113
87
|
for _finder, name, _is_pkg in pkgutil.walk_packages(pkg_path, prefix):
|
|
114
88
|
yield name
|
|
115
89
|
|
|
116
90
|
|
|
117
|
-
def
|
|
91
|
+
def _collect_decorated(
|
|
118
92
|
*,
|
|
119
93
|
package: ModuleType,
|
|
120
94
|
exclude: Optional[Callable[[str], bool]],
|
|
121
95
|
plugins: Tuple[PicoPlugin, ...],
|
|
122
96
|
binder: Binder,
|
|
123
|
-
) -> Tuple[List[type], List[type]]:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
97
|
+
) -> Tuple[List[type], List[type], List[tuple[Any, dict]]]:
|
|
98
|
+
comps: List[type] = []
|
|
99
|
+
facts: List[type] = []
|
|
100
|
+
interceptors: List[tuple[Any, dict]] = []
|
|
101
|
+
|
|
102
|
+
def _collect_from_class(cls: type):
|
|
103
|
+
if getattr(cls, COMPONENT_FLAG, False):
|
|
104
|
+
comps.append(cls)
|
|
105
|
+
elif getattr(cls, FACTORY_FLAG, False):
|
|
106
|
+
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)))
|
|
130
116
|
|
|
131
117
|
def _visit_module(module: ModuleType):
|
|
132
118
|
for _name, obj in inspect.getmembers(module, inspect.isclass):
|
|
133
|
-
|
|
134
|
-
|
|
119
|
+
run_plugin_hook(plugins, "visit_class", module, obj, binder)
|
|
120
|
+
_collect_from_class(obj)
|
|
135
121
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
factory_classes.append(obj)
|
|
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)))
|
|
141
126
|
|
|
142
|
-
# 1) Si es un paquete, recorrer submódulos
|
|
143
127
|
for mod_name in _iter_package_modules(package):
|
|
144
128
|
if exclude and exclude(mod_name):
|
|
145
129
|
logging.info("Skipping module %s (excluded)", mod_name)
|
|
146
130
|
continue
|
|
147
|
-
|
|
148
131
|
try:
|
|
149
132
|
module = importlib.import_module(mod_name)
|
|
150
133
|
except Exception as e:
|
|
151
134
|
logging.warning("Module %s not processed: %s", mod_name, e)
|
|
152
135
|
continue
|
|
153
|
-
|
|
154
136
|
_visit_module(module)
|
|
155
137
|
|
|
156
|
-
# 2) Si el “paquete” raíz es un módulo (sin __path__), también hay que visitarlo.
|
|
157
138
|
if not hasattr(package, "__path__"):
|
|
158
139
|
_visit_module(package)
|
|
159
140
|
|
|
160
|
-
return
|
|
141
|
+
return comps, facts, interceptors
|
|
161
142
|
|
|
162
143
|
|
|
163
144
|
def _register_component_classes(
|
|
@@ -166,23 +147,20 @@ def _register_component_classes(
|
|
|
166
147
|
container: PicoContainer,
|
|
167
148
|
resolver: Resolver,
|
|
168
149
|
) -> None:
|
|
169
|
-
"""
|
|
170
|
-
Register @component classes into the container.
|
|
171
|
-
|
|
172
|
-
Binding key:
|
|
173
|
-
- If the class has COMPONENT_KEY, use it; otherwise, bind by the class itself.
|
|
174
|
-
Laziness:
|
|
175
|
-
- If COMPONENT_LAZY is True, provide a proxy that defers instantiation.
|
|
176
|
-
"""
|
|
177
150
|
for cls in classes:
|
|
178
151
|
key = getattr(cls, COMPONENT_KEY, cls)
|
|
179
152
|
is_lazy = bool(getattr(cls, COMPONENT_LAZY, False))
|
|
180
153
|
tags = tuple(getattr(cls, COMPONENT_TAGS, ()))
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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)
|
|
163
|
+
container.bind(key, provider, lazy=is_lazy, tags=tags)
|
|
186
164
|
|
|
187
165
|
|
|
188
166
|
def _register_factory_classes(
|
|
@@ -191,19 +169,8 @@ def _register_factory_classes(
|
|
|
191
169
|
container: PicoContainer,
|
|
192
170
|
resolver: Resolver,
|
|
193
171
|
) -> None:
|
|
194
|
-
"""
|
|
195
|
-
Register products of @factory_component classes.
|
|
196
|
-
|
|
197
|
-
For each factory class:
|
|
198
|
-
- Instantiate the factory via the resolver.
|
|
199
|
-
- For each method with @provides:
|
|
200
|
-
- Bind the provided key to a callable that calls the factory method.
|
|
201
|
-
- If PROVIDES_LAZY is True, bind a proxy that defers the method call.
|
|
202
|
-
"""
|
|
203
172
|
for fcls in factory_classes:
|
|
204
173
|
try:
|
|
205
|
-
# Durante el escaneo, permitir la resolución de dependencias de la factory
|
|
206
|
-
# elevando temporalmente el flag `_resolving` para no chocar con la guardia.
|
|
207
174
|
tok_res = _state._resolving.set(True)
|
|
208
175
|
try:
|
|
209
176
|
finst = resolver.create_instance(fcls)
|
|
@@ -217,14 +184,20 @@ def _register_factory_classes(
|
|
|
217
184
|
provided_key = getattr(func, PROVIDES_KEY, None)
|
|
218
185
|
if provided_key is None:
|
|
219
186
|
continue
|
|
187
|
+
|
|
220
188
|
is_lazy = bool(getattr(func, PROVIDES_LAZY, False))
|
|
221
189
|
tags = tuple(getattr(func, PROVIDES_TAGS, ()))
|
|
190
|
+
|
|
222
191
|
bound = getattr(finst, attr_name, func.__get__(finst, fcls))
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
192
|
+
prov = _provider_from_callable(bound, owner_cls=fcls, resolver=resolver, lazy=is_lazy)
|
|
193
|
+
|
|
194
|
+
if isinstance(provided_key, type):
|
|
195
|
+
try:
|
|
196
|
+
setattr(prov, "_pico_alias_for", provided_key)
|
|
197
|
+
except Exception:
|
|
198
|
+
pass
|
|
199
|
+
unique_key = (provided_key, f"{fcls.__name__}.{attr_name}")
|
|
200
|
+
container.bind(unique_key, prov, lazy=is_lazy, tags=tags)
|
|
201
|
+
else:
|
|
202
|
+
container.bind(provided_key, prov, lazy=is_lazy, tags=tags)
|
|
230
203
|
|
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
|
pico_ioc/utils.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# src/pico_ioc/utils.py
|
|
2
|
+
from typing import Any, Callable
|
|
3
|
+
from .container import PicoContainer
|
|
4
|
+
from .proxy import ComponentProxy
|
|
5
|
+
|
|
6
|
+
def _wrap_if_lazy(provider: Callable, is_lazy: bool) -> Callable:
|
|
7
|
+
"""Wraps a provider in a ComponentProxy if it's marked as lazy."""
|
|
8
|
+
return (lambda: ComponentProxy(provider)) if is_lazy else provider
|
|
9
|
+
|
|
10
|
+
def _provider_from_class(cls: type, *, resolver, lazy: bool):
|
|
11
|
+
def _new():
|
|
12
|
+
return resolver.create_instance(cls)
|
|
13
|
+
return _wrap_if_lazy(_new, lazy)
|
|
14
|
+
|
|
15
|
+
def _provider_from_callable(fn, *, owner_cls, resolver, lazy: bool):
|
|
16
|
+
def _invoke():
|
|
17
|
+
kwargs = resolver.kwargs_for_callable(fn, owner_cls=owner_cls)
|
|
18
|
+
return fn(**kwargs)
|
|
19
|
+
return _wrap_if_lazy(_invoke, lazy)
|
|
20
|
+
|
|
21
|
+
def create_alias_provider(container: PicoContainer, target_key: Any) -> Callable[[], Any]:
|
|
22
|
+
"""Creates a provider that delegates the get() call to the container for another key."""
|
|
23
|
+
def _provider():
|
|
24
|
+
return container.get(target_key)
|
|
25
|
+
return _provider
|