pico-ioc 0.5.2__py3-none-any.whl → 1.0.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 +14 -391
- pico_ioc/_state.py +10 -0
- pico_ioc/_version.py +1 -1
- pico_ioc/api.py +128 -0
- pico_ioc/container.py +155 -0
- pico_ioc/decorators.py +76 -0
- pico_ioc/plugins.py +12 -0
- pico_ioc/proxy.py +77 -0
- pico_ioc/public_api.py +76 -0
- pico_ioc/resolver.py +110 -0
- pico_ioc/scanner.py +238 -0
- pico_ioc/typing_utils.py +29 -0
- pico_ioc-1.0.0.dist-info/METADATA +145 -0
- pico_ioc-1.0.0.dist-info/RECORD +17 -0
- pico_ioc-0.5.2.dist-info/METADATA +0 -278
- pico_ioc-0.5.2.dist-info/RECORD +0 -7
- {pico_ioc-0.5.2.dist-info → pico_ioc-1.0.0.dist-info}/WHEEL +0 -0
- {pico_ioc-0.5.2.dist-info → pico_ioc-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-0.5.2.dist-info → pico_ioc-1.0.0.dist-info}/top_level.txt +0 -0
pico_ioc/container.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# pico_ioc/container.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Any, Dict, get_origin, get_args, Annotated
|
|
5
|
+
import typing as _t
|
|
6
|
+
|
|
7
|
+
from .decorators import QUALIFIERS_KEY
|
|
8
|
+
from . import _state # re-entrancy guard
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Binder:
|
|
12
|
+
def __init__(self, container: "PicoContainer"):
|
|
13
|
+
self._c = container
|
|
14
|
+
|
|
15
|
+
def bind(self, key: Any, provider, *, lazy: bool):
|
|
16
|
+
self._c.bind(key, provider, lazy=lazy)
|
|
17
|
+
|
|
18
|
+
def has(self, key: Any) -> bool:
|
|
19
|
+
return self._c.has(key)
|
|
20
|
+
|
|
21
|
+
def get(self, key: Any):
|
|
22
|
+
return self._c.get(key)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PicoContainer:
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self._providers: Dict[Any, Dict[str, Any]] = {}
|
|
28
|
+
self._singletons: Dict[Any, Any] = {}
|
|
29
|
+
|
|
30
|
+
def bind(self, key: Any, provider, *, lazy: bool):
|
|
31
|
+
meta = {"factory": provider, "lazy": bool(lazy)}
|
|
32
|
+
try:
|
|
33
|
+
q = getattr(key, QUALIFIERS_KEY, ())
|
|
34
|
+
except Exception:
|
|
35
|
+
q = ()
|
|
36
|
+
meta["qualifiers"] = tuple(q) if q else ()
|
|
37
|
+
self._providers[key] = meta
|
|
38
|
+
|
|
39
|
+
def has(self, key: Any) -> bool:
|
|
40
|
+
return key in self._providers
|
|
41
|
+
|
|
42
|
+
def get(self, key: Any):
|
|
43
|
+
# block only when scanning and NOT currently resolving a dependency
|
|
44
|
+
if _state._scanning.get() and not _state._resolving.get():
|
|
45
|
+
raise RuntimeError("re-entrant container access during scan")
|
|
46
|
+
|
|
47
|
+
prov = self._providers.get(key)
|
|
48
|
+
if prov is None:
|
|
49
|
+
raise NameError(f"No provider found for key {key!r}")
|
|
50
|
+
|
|
51
|
+
if key in self._singletons:
|
|
52
|
+
return self._singletons[key]
|
|
53
|
+
|
|
54
|
+
# mark resolving around factory execution
|
|
55
|
+
tok = _state._resolving.set(True)
|
|
56
|
+
try:
|
|
57
|
+
instance = prov["factory"]()
|
|
58
|
+
finally:
|
|
59
|
+
_state._resolving.reset(tok)
|
|
60
|
+
|
|
61
|
+
# memoize always (both lazy and non-lazy after first get)
|
|
62
|
+
self._singletons[key] = instance
|
|
63
|
+
return instance
|
|
64
|
+
|
|
65
|
+
def eager_instantiate_all(self):
|
|
66
|
+
for key, prov in list(self._providers.items()):
|
|
67
|
+
if not prov["lazy"]:
|
|
68
|
+
self.get(key)
|
|
69
|
+
|
|
70
|
+
def get_all(self, base_type: Any):
|
|
71
|
+
return tuple(self._resolve_all_for_base(base_type, qualifiers=()))
|
|
72
|
+
|
|
73
|
+
def get_all_qualified(self, base_type: Any, *qualifiers: str):
|
|
74
|
+
return tuple(self._resolve_all_for_base(base_type, qualifiers=qualifiers))
|
|
75
|
+
|
|
76
|
+
def _resolve_all_for_base(self, base_type: Any, qualifiers=()):
|
|
77
|
+
matches = []
|
|
78
|
+
for provider_key, meta in self._providers.items():
|
|
79
|
+
cls = provider_key if isinstance(provider_key, type) else None
|
|
80
|
+
if cls is None:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
# Avoid self-inclusion loops: if the class itself requires a collection
|
|
84
|
+
# of `base_type` in its __init__, don't treat it as an implementation
|
|
85
|
+
# of `base_type` when building that collection.
|
|
86
|
+
if _requires_collection_of_base(cls, base_type):
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
if _is_compatible(cls, base_type):
|
|
90
|
+
prov_qs = meta.get("qualifiers", ())
|
|
91
|
+
if all(q in prov_qs for q in qualifiers):
|
|
92
|
+
inst = self.get(provider_key)
|
|
93
|
+
matches.append(inst)
|
|
94
|
+
return matches
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _is_protocol(t) -> bool:
|
|
98
|
+
return getattr(t, "_is_protocol", False) is True
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _is_compatible(cls, base) -> bool:
|
|
102
|
+
try:
|
|
103
|
+
if isinstance(base, type) and issubclass(cls, base):
|
|
104
|
+
return True
|
|
105
|
+
except TypeError:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
if _is_protocol(base):
|
|
109
|
+
# simple structural check: ensure methods/attrs declared on the Protocol exist on the class
|
|
110
|
+
names = set(getattr(base, "__annotations__", {}).keys())
|
|
111
|
+
names.update(n for n in getattr(base, "__dict__", {}).keys() if not n.startswith("_"))
|
|
112
|
+
for n in names:
|
|
113
|
+
if n.startswith("__") and n.endswith("__"):
|
|
114
|
+
continue
|
|
115
|
+
if not hasattr(cls, n):
|
|
116
|
+
return False
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
def _requires_collection_of_base(cls, base) -> bool:
|
|
122
|
+
"""
|
|
123
|
+
Return True if `cls.__init__` has any parameter annotated as a collection
|
|
124
|
+
(list/tuple, including Annotated variants) of `base`. This prevents treating
|
|
125
|
+
`cls` as an implementation of `base` while building that collection,
|
|
126
|
+
avoiding recursion.
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
sig = inspect.signature(cls.__init__)
|
|
130
|
+
except Exception:
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
from .resolver import _get_hints # type: ignore
|
|
135
|
+
hints = _get_hints(cls.__init__, owner_cls=cls)
|
|
136
|
+
except Exception:
|
|
137
|
+
hints = {}
|
|
138
|
+
|
|
139
|
+
for name, param in sig.parameters.items():
|
|
140
|
+
if name == "self":
|
|
141
|
+
continue
|
|
142
|
+
ann = hints.get(name, param.annotation)
|
|
143
|
+
origin = get_origin(ann) or ann
|
|
144
|
+
if origin in (list, tuple, _t.List, _t.Tuple):
|
|
145
|
+
inner = (get_args(ann) or (object,))[0]
|
|
146
|
+
# Unwrap Annotated[T, ...] si aparece
|
|
147
|
+
if get_origin(inner) is Annotated:
|
|
148
|
+
args = get_args(inner)
|
|
149
|
+
if args:
|
|
150
|
+
inner = args[0]
|
|
151
|
+
if inner is base:
|
|
152
|
+
return True
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
|
pico_ioc/decorators.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# pico_ioc/decorators.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import functools
|
|
4
|
+
from typing import Any, Iterable
|
|
5
|
+
|
|
6
|
+
COMPONENT_FLAG = "_is_component"
|
|
7
|
+
COMPONENT_KEY = "_component_key"
|
|
8
|
+
COMPONENT_LAZY = "_component_lazy"
|
|
9
|
+
|
|
10
|
+
FACTORY_FLAG = "_is_factory_component"
|
|
11
|
+
PROVIDES_KEY = "_provides_name"
|
|
12
|
+
PROVIDES_LAZY = "_pico_lazy"
|
|
13
|
+
|
|
14
|
+
PLUGIN_FLAG = "_is_pico_plugin"
|
|
15
|
+
QUALIFIERS_KEY = "_pico_qualifiers"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def factory_component(cls):
|
|
19
|
+
setattr(cls, FACTORY_FLAG, True)
|
|
20
|
+
return cls
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def provides(key: Any, *, lazy: bool = False):
|
|
24
|
+
def dec(fn):
|
|
25
|
+
@functools.wraps(fn)
|
|
26
|
+
def w(*a, **k):
|
|
27
|
+
return fn(*a, **k)
|
|
28
|
+
setattr(w, PROVIDES_KEY, key)
|
|
29
|
+
setattr(w, PROVIDES_LAZY, bool(lazy))
|
|
30
|
+
return w
|
|
31
|
+
return dec
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def component(cls=None, *, name: Any = None, lazy: bool = False):
|
|
35
|
+
def dec(c):
|
|
36
|
+
setattr(c, COMPONENT_FLAG, True)
|
|
37
|
+
setattr(c, COMPONENT_KEY, name if name is not None else c)
|
|
38
|
+
setattr(c, COMPONENT_LAZY, bool(lazy))
|
|
39
|
+
return c
|
|
40
|
+
return dec(cls) if cls else dec
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def plugin(cls):
|
|
44
|
+
setattr(cls, PLUGIN_FLAG, True)
|
|
45
|
+
return cls
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Qualifier(str):
|
|
49
|
+
__slots__ = () # tiny memory win; immutable like str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def qualifier(*qs: Qualifier):
|
|
53
|
+
def dec(cls):
|
|
54
|
+
current: Iterable[Qualifier] = getattr(cls, QUALIFIERS_KEY, ())
|
|
55
|
+
seen = set(current)
|
|
56
|
+
merged = list(current)
|
|
57
|
+
for q in qs:
|
|
58
|
+
if q not in seen:
|
|
59
|
+
merged.append(q)
|
|
60
|
+
seen.add(q)
|
|
61
|
+
setattr(cls, QUALIFIERS_KEY, tuple(merged))
|
|
62
|
+
return cls
|
|
63
|
+
return dec
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
# decorators
|
|
68
|
+
"component", "factory_component", "provides", "plugin", "qualifier",
|
|
69
|
+
# qualifier type
|
|
70
|
+
"Qualifier",
|
|
71
|
+
# metadata keys (exported for advanced use/testing)
|
|
72
|
+
"COMPONENT_FLAG", "COMPONENT_KEY", "COMPONENT_LAZY",
|
|
73
|
+
"FACTORY_FLAG", "PROVIDES_KEY", "PROVIDES_LAZY",
|
|
74
|
+
"PLUGIN_FLAG", "QUALIFIERS_KEY",
|
|
75
|
+
]
|
|
76
|
+
|
pico_ioc/plugins.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# pico_ioc/plugins.py
|
|
2
|
+
from typing import Protocol, Any
|
|
3
|
+
from .container import Binder, PicoContainer
|
|
4
|
+
|
|
5
|
+
class PicoPlugin(Protocol):
|
|
6
|
+
def before_scan(self, package: Any, binder: Binder) -> None: ...
|
|
7
|
+
def visit_class(self, module: Any, cls: type, binder: Binder) -> None: ...
|
|
8
|
+
def after_scan(self, package: Any, binder: Binder) -> None: ...
|
|
9
|
+
def after_bind(self, container: PicoContainer, binder: Binder) -> None: ...
|
|
10
|
+
def before_eager(self, container: PicoContainer, binder: Binder) -> None: ...
|
|
11
|
+
def after_ready(self, container: PicoContainer, binder: Binder) -> None: ...
|
|
12
|
+
|
pico_ioc/proxy.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# pico_ioc/proxy.py
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
class ComponentProxy:
|
|
6
|
+
def __init__(self, object_creator: Callable[[], Any]):
|
|
7
|
+
object.__setattr__(self, "_object_creator", object_creator)
|
|
8
|
+
object.__setattr__(self, "__real_object", None)
|
|
9
|
+
|
|
10
|
+
def _get_real_object(self) -> Any:
|
|
11
|
+
real_obj = object.__getattribute__(self, "__real_object")
|
|
12
|
+
if real_obj is None:
|
|
13
|
+
real_obj = object.__getattribute__(self, "_object_creator")()
|
|
14
|
+
object.__setattr__(self, "__real_object", real_obj)
|
|
15
|
+
return real_obj
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def __class__(self):
|
|
19
|
+
return self._get_real_object().__class__
|
|
20
|
+
|
|
21
|
+
def __getattr__(self, name): return getattr(self._get_real_object(), name)
|
|
22
|
+
def __setattr__(self, name, value): setattr(self._get_real_object(), name, value)
|
|
23
|
+
def __delattr__(self, name): delattr(self._get_real_object(), name)
|
|
24
|
+
def __str__(self): return str(self._get_real_object())
|
|
25
|
+
def __repr__(self): return repr(self._get_real_object())
|
|
26
|
+
def __dir__(self): return dir(self._get_real_object())
|
|
27
|
+
def __len__(self): return len(self._get_real_object())
|
|
28
|
+
def __getitem__(self, key): return self._get_real_object()[key]
|
|
29
|
+
def __setitem__(self, key, value): self._get_real_object()[key] = value
|
|
30
|
+
def __delitem__(self, key): del self._get_real_object()[key]
|
|
31
|
+
def __iter__(self): return iter(self._get_real_object())
|
|
32
|
+
def __reversed__(self): return reversed(self._get_real_object())
|
|
33
|
+
def __contains__(self, item): return item in self._get_real_object()
|
|
34
|
+
def __add__(self, other): return self._get_real_object() + other
|
|
35
|
+
def __sub__(self, other): return self._get_real_object() - other
|
|
36
|
+
def __mul__(self, other): return self._get_real_object() * other
|
|
37
|
+
def __matmul__(self, other): return self._get_real_object() @ other
|
|
38
|
+
def __truediv__(self, other): return self._get_real_object() / other
|
|
39
|
+
def __floordiv__(self, other): return self._get_real_object() // other
|
|
40
|
+
def __mod__(self, other): return self._get_real_object() % other
|
|
41
|
+
def __divmod__(self, other): return divmod(self._get_real_object(), other)
|
|
42
|
+
def __pow__(self, other, modulo=None): return pow(self._get_real_object(), other, modulo)
|
|
43
|
+
def __lshift__(self, other): return self._get_real_object() << other
|
|
44
|
+
def __rshift__(self, other): return self._get_real_object() >> other
|
|
45
|
+
def __and__(self, other): return self._get_real_object() & other
|
|
46
|
+
def __xor__(self, other): return self._get_real_object() ^ other
|
|
47
|
+
def __or__(self, other): return self._get_real_object() | other
|
|
48
|
+
def __radd__(self, other): return other + self._get_real_object()
|
|
49
|
+
def __rsub__(self, other): return other - self._get_real_object()
|
|
50
|
+
def __rmul__(self, other): return other * self._get_real_object()
|
|
51
|
+
def __rmatmul__(self, other): return other @ self._get_real_object()
|
|
52
|
+
def __rtruediv__(self, other): return other / self._get_real_object()
|
|
53
|
+
def __rfloordiv__(self, other): return other // self._get_real_object()
|
|
54
|
+
def __rmod__(self, other): return other % self._get_real_object()
|
|
55
|
+
def __rdivmod__(self, other): return divmod(other, self._get_real_object())
|
|
56
|
+
def __rpow__(self, other): return pow(other, self._get_real_object())
|
|
57
|
+
def __rlshift__(self, other): return other << self._get_real_object()
|
|
58
|
+
def __rrshift__(self, other): return other >> self._get_real_object()
|
|
59
|
+
def __rand__(self, other): return other & self._get_real_object()
|
|
60
|
+
def __rxor__(self, other): return other ^ self._get_real_object()
|
|
61
|
+
def __ror__(self, other): return other | self._get_real_object()
|
|
62
|
+
def __neg__(self): return -self._get_real_object()
|
|
63
|
+
def __pos__(self): return +self._get_real_object()
|
|
64
|
+
def __abs__(self): return abs(self._get_real_object())
|
|
65
|
+
def __invert__(self): return ~self._get_real_object()
|
|
66
|
+
def __eq__(self, other): return self._get_real_object() == other
|
|
67
|
+
def __ne__(self, other): return self._get_real_object() != other
|
|
68
|
+
def __lt__(self, other): return self._get_real_object() < other
|
|
69
|
+
def __le__(self, other): return self._get_real_object() <= other
|
|
70
|
+
def __gt__(self, other): return self._get_real_object() > other
|
|
71
|
+
def __ge__(self, other): return self._get_real_object() >= other
|
|
72
|
+
def __hash__(self): return hash(self._get_real_object())
|
|
73
|
+
def __bool__(self): return bool(self._get_real_object())
|
|
74
|
+
def __call__(self, *args, **kwargs): return self._get_real_object()(*args, **kwargs)
|
|
75
|
+
def __enter__(self): return self._get_real_object().__enter__()
|
|
76
|
+
def __exit__(self, exc_type, exc_val, exc_tb): return self._get_real_object().__exit__(exc_type, exc_val, exc_tb)
|
|
77
|
+
|
pico_ioc/public_api.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# pico_ioc/public_api.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import importlib
|
|
5
|
+
import inspect
|
|
6
|
+
import pkgutil
|
|
7
|
+
import sys
|
|
8
|
+
from types import ModuleType
|
|
9
|
+
from typing import Dict, Iterable, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from .decorators import COMPONENT_FLAG, FACTORY_FLAG, PLUGIN_FLAG
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def export_public_symbols_decorated(
|
|
15
|
+
*packages: str,
|
|
16
|
+
include_also: Optional[Iterable[str]] = None,
|
|
17
|
+
include_plugins: bool = True,
|
|
18
|
+
):
|
|
19
|
+
index: Dict[str, Tuple[str, str]] = {}
|
|
20
|
+
|
|
21
|
+
def _collect(m: ModuleType):
|
|
22
|
+
names = getattr(m, "__all__", None)
|
|
23
|
+
if isinstance(names, (list, tuple, set)):
|
|
24
|
+
for n in names:
|
|
25
|
+
if hasattr(m, n):
|
|
26
|
+
index.setdefault(n, (m.__name__, n))
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
for n, obj in m.__dict__.items():
|
|
30
|
+
if not inspect.isclass(obj):
|
|
31
|
+
continue
|
|
32
|
+
is_component = getattr(obj, COMPONENT_FLAG, False)
|
|
33
|
+
is_factory = getattr(obj, FACTORY_FLAG, False)
|
|
34
|
+
is_plugin = include_plugins and getattr(obj, PLUGIN_FLAG, False)
|
|
35
|
+
if is_component or is_factory or is_plugin:
|
|
36
|
+
index.setdefault(n, (m.__name__, n))
|
|
37
|
+
|
|
38
|
+
for pkg_name in packages:
|
|
39
|
+
try:
|
|
40
|
+
base = importlib.import_module(pkg_name)
|
|
41
|
+
except Exception:
|
|
42
|
+
continue
|
|
43
|
+
if hasattr(base, "__path__"):
|
|
44
|
+
prefix = base.__name__ + "."
|
|
45
|
+
for _, modname, _ in pkgutil.walk_packages(base.__path__, prefix):
|
|
46
|
+
try:
|
|
47
|
+
m = importlib.import_module(modname)
|
|
48
|
+
except Exception:
|
|
49
|
+
continue
|
|
50
|
+
_collect(m)
|
|
51
|
+
else:
|
|
52
|
+
_collect(base)
|
|
53
|
+
|
|
54
|
+
for qual in tuple(include_also or ()):
|
|
55
|
+
modname, _, attr = qual.partition(":")
|
|
56
|
+
if modname and attr:
|
|
57
|
+
try:
|
|
58
|
+
m = importlib.import_module(modname)
|
|
59
|
+
if hasattr(m, attr):
|
|
60
|
+
index.setdefault(attr, (m.__name__, attr))
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
def __getattr__(name: str):
|
|
65
|
+
try:
|
|
66
|
+
modname, attr = index[name]
|
|
67
|
+
except KeyError as e:
|
|
68
|
+
raise AttributeError(f"module has no attribute {name!r}") from e
|
|
69
|
+
mod = sys.modules.get(modname) or importlib.import_module(modname)
|
|
70
|
+
return getattr(mod, attr)
|
|
71
|
+
|
|
72
|
+
def __dir__():
|
|
73
|
+
return sorted(index.keys())
|
|
74
|
+
|
|
75
|
+
return __getattr__, __dir__
|
|
76
|
+
|
pico_ioc/resolver.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# pico_ioc/resolver.py (Python 3.10+)
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import Any, Annotated, get_args, get_origin, get_type_hints
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _get_hints(obj, owner_cls=None) -> dict:
|
|
9
|
+
"""type hints with include_extras=True and correct globals/locals."""
|
|
10
|
+
mod = inspect.getmodule(obj)
|
|
11
|
+
g = getattr(mod, "__dict__", {})
|
|
12
|
+
l = vars(owner_cls) if owner_cls is not None else None
|
|
13
|
+
return get_type_hints(obj, globalns=g, localns=l, include_extras=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _is_collection_hint(tp) -> bool:
|
|
17
|
+
"""True if tp is a list[...] or tuple[...]."""
|
|
18
|
+
origin = get_origin(tp) or tp
|
|
19
|
+
return origin in (list, tuple)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _base_and_qualifiers_from_hint(tp):
|
|
23
|
+
"""
|
|
24
|
+
Extract (base, qualifiers, container_kind) from a collection hint.
|
|
25
|
+
Supports list[T] / tuple[T] and Annotated[T, "qual1", ...].
|
|
26
|
+
"""
|
|
27
|
+
origin = get_origin(tp) or tp
|
|
28
|
+
args = get_args(tp) or ()
|
|
29
|
+
container_kind = list if origin is list else tuple
|
|
30
|
+
|
|
31
|
+
if not args:
|
|
32
|
+
return (object, (), container_kind)
|
|
33
|
+
|
|
34
|
+
inner = args[0]
|
|
35
|
+
if get_origin(inner) is Annotated:
|
|
36
|
+
base, *extras = get_args(inner)
|
|
37
|
+
quals = tuple(a for a in extras if isinstance(a, str))
|
|
38
|
+
return (base, quals, container_kind)
|
|
39
|
+
|
|
40
|
+
return (inner, (), container_kind)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Resolver:
|
|
44
|
+
def __init__(self, container, *, prefer_name_first: bool = True):
|
|
45
|
+
self.c = container
|
|
46
|
+
self._prefer_name_first = bool(prefer_name_first)
|
|
47
|
+
|
|
48
|
+
def create_instance(self, cls):
|
|
49
|
+
sig = inspect.signature(cls.__init__)
|
|
50
|
+
hints = _get_hints(cls.__init__, owner_cls=cls)
|
|
51
|
+
kwargs = {}
|
|
52
|
+
for name, param in sig.parameters.items():
|
|
53
|
+
if name == "self":
|
|
54
|
+
continue
|
|
55
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
56
|
+
continue
|
|
57
|
+
ann = hints.get(name, param.annotation)
|
|
58
|
+
try:
|
|
59
|
+
value = self._resolve_param(name, ann)
|
|
60
|
+
except NameError:
|
|
61
|
+
if param.default is not inspect._empty:
|
|
62
|
+
continue
|
|
63
|
+
raise
|
|
64
|
+
kwargs[name] = value
|
|
65
|
+
return cls(**kwargs)
|
|
66
|
+
|
|
67
|
+
def kwargs_for_callable(self, fn, *, owner_cls=None):
|
|
68
|
+
sig = inspect.signature(fn)
|
|
69
|
+
hints = _get_hints(fn, owner_cls=owner_cls)
|
|
70
|
+
kwargs = {}
|
|
71
|
+
for name, param in sig.parameters.items():
|
|
72
|
+
if name == "self":
|
|
73
|
+
continue
|
|
74
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
75
|
+
continue
|
|
76
|
+
ann = hints.get(name, param.annotation)
|
|
77
|
+
try:
|
|
78
|
+
value = self._resolve_param(name, ann)
|
|
79
|
+
except NameError:
|
|
80
|
+
if param.default is not inspect._empty:
|
|
81
|
+
continue
|
|
82
|
+
raise
|
|
83
|
+
kwargs[name] = value
|
|
84
|
+
return kwargs
|
|
85
|
+
|
|
86
|
+
def _resolve_param(self, name: str, ann: Any):
|
|
87
|
+
# collections (list/tuple) with optional qualifiers via Annotated
|
|
88
|
+
if _is_collection_hint(ann):
|
|
89
|
+
base, quals, container_kind = _base_and_qualifiers_from_hint(ann)
|
|
90
|
+
items = self.c._resolve_all_for_base(base, qualifiers=quals)
|
|
91
|
+
return list(items) if container_kind is list else tuple(items)
|
|
92
|
+
|
|
93
|
+
# precedence: by name > by exact annotation > by MRO > by name again
|
|
94
|
+
if self._prefer_name_first and self.c.has(name):
|
|
95
|
+
return self.c.get(name)
|
|
96
|
+
|
|
97
|
+
if ann is not inspect._empty and self.c.has(ann):
|
|
98
|
+
return self.c.get(ann)
|
|
99
|
+
|
|
100
|
+
if ann is not inspect._empty and isinstance(ann, type):
|
|
101
|
+
for base in ann.__mro__[1:]:
|
|
102
|
+
if self.c.has(base):
|
|
103
|
+
return self.c.get(base)
|
|
104
|
+
|
|
105
|
+
if self.c.has(name):
|
|
106
|
+
return self.c.get(name)
|
|
107
|
+
|
|
108
|
+
missing = ann if ann is not inspect._empty else name
|
|
109
|
+
raise NameError(f"No provider found for key {missing!r}")
|
|
110
|
+
|