pico-ioc 2.0.5__py3-none-any.whl → 2.1.1__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 +9 -12
- pico_ioc/_version.py +1 -1
- pico_ioc/analysis.py +92 -0
- pico_ioc/aop.py +20 -12
- pico_ioc/api.py +63 -1088
- pico_ioc/component_scanner.py +166 -0
- pico_ioc/config_builder.py +87 -0
- pico_ioc/config_registrar.py +236 -0
- pico_ioc/config_runtime.py +54 -13
- pico_ioc/container.py +208 -155
- pico_ioc/decorators.py +193 -0
- pico_ioc/dependency_validator.py +103 -0
- pico_ioc/event_bus.py +24 -25
- pico_ioc/exceptions.py +0 -16
- pico_ioc/factory.py +2 -1
- pico_ioc/locator.py +76 -2
- pico_ioc/provider_selector.py +35 -0
- pico_ioc/registrar.py +188 -0
- pico_ioc/scope.py +13 -4
- {pico_ioc-2.0.5.dist-info → pico_ioc-2.1.1.dist-info}/METADATA +94 -45
- pico_ioc-2.1.1.dist-info/RECORD +25 -0
- pico_ioc-2.0.5.dist-info/RECORD +0 -17
- {pico_ioc-2.0.5.dist-info → pico_ioc-2.1.1.dist-info}/WHEEL +0 -0
- {pico_ioc-2.0.5.dist-info → pico_ioc-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-2.0.5.dist-info → pico_ioc-2.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union, Set
|
|
4
|
+
from .constants import PICO_INFRA, PICO_NAME, PICO_KEY, PICO_META
|
|
5
|
+
from .factory import ProviderMetadata, DeferredProvider
|
|
6
|
+
from .decorators import get_return_type
|
|
7
|
+
from .config_registrar import ConfigurationManager
|
|
8
|
+
from .analysis import analyze_callable_dependencies, DependencyRequest
|
|
9
|
+
|
|
10
|
+
KeyT = Union[str, type]
|
|
11
|
+
Provider = Callable[[], Any]
|
|
12
|
+
|
|
13
|
+
class ComponentScanner:
|
|
14
|
+
def __init__(self, profiles: Set[str], environ: Dict[str, str], config_manager: ConfigurationManager):
|
|
15
|
+
self._profiles = profiles
|
|
16
|
+
self._environ = environ
|
|
17
|
+
self._config_manager = config_manager
|
|
18
|
+
self._candidates: Dict[KeyT, List[Tuple[bool, Provider, ProviderMetadata]]] = {}
|
|
19
|
+
self._on_missing: List[Tuple[int, KeyT, type]] = []
|
|
20
|
+
self._deferred: List[DeferredProvider] = []
|
|
21
|
+
self._provides_functions: Dict[KeyT, Callable[..., Any]] = {}
|
|
22
|
+
|
|
23
|
+
def get_scan_results(self) -> Tuple[Dict[KeyT, List[Tuple[bool, Provider, ProviderMetadata]]], List[Tuple[int, KeyT, type]], List[DeferredProvider], Dict[KeyT, Callable[..., Any]]]:
|
|
24
|
+
return self._candidates, self._on_missing, self._deferred, self._provides_functions
|
|
25
|
+
|
|
26
|
+
def _queue(self, key: KeyT, provider: Provider, md: ProviderMetadata) -> None:
|
|
27
|
+
lst = self._candidates.setdefault(key, [])
|
|
28
|
+
lst.append((md.primary, provider, md))
|
|
29
|
+
if isinstance(provider, DeferredProvider):
|
|
30
|
+
self._deferred.append(provider)
|
|
31
|
+
|
|
32
|
+
def _enabled_by_condition(self, obj: Any) -> bool:
|
|
33
|
+
meta = getattr(obj, PICO_META, {})
|
|
34
|
+
c = meta.get("conditional", None)
|
|
35
|
+
if not c:
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
p = set(c.get("profiles") or ())
|
|
39
|
+
if p and not (p & self._profiles):
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
req = c.get("require_env") or ()
|
|
43
|
+
for k in req:
|
|
44
|
+
if k not in self._environ or not self._environ.get(k):
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
pred = c.get("predicate")
|
|
48
|
+
if pred is None:
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
ok = bool(pred())
|
|
53
|
+
except Exception:
|
|
54
|
+
return False
|
|
55
|
+
if not ok:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
def _register_component_class(self, cls: type) -> None:
|
|
61
|
+
if not self._enabled_by_condition(cls):
|
|
62
|
+
return
|
|
63
|
+
key = getattr(cls, PICO_KEY, cls)
|
|
64
|
+
qset = set(str(q) for q in getattr(cls, PICO_META, {}).get("qualifier", ()))
|
|
65
|
+
sc = getattr(cls, PICO_META, {}).get("scope", "singleton")
|
|
66
|
+
deps = analyze_callable_dependencies(cls.__init__)
|
|
67
|
+
provider = DeferredProvider(lambda pico, loc, c=cls, d=deps: pico.build_class(c, loc, d))
|
|
68
|
+
md = ProviderMetadata(key=key, provided_type=cls, concrete_class=cls, factory_class=None, factory_method=None, qualifiers=qset, primary=bool(getattr(cls, PICO_META, {}).get("primary")), lazy=bool(getattr(cls, PICO_META, {}).get("lazy", False)), infra=getattr(cls, PICO_INFRA, None), pico_name=getattr(cls, PICO_NAME, None), scope=sc, dependencies=deps)
|
|
69
|
+
self._queue(key, provider, md)
|
|
70
|
+
|
|
71
|
+
def _register_factory_class(self, cls: type) -> None:
|
|
72
|
+
if not self._enabled_by_condition(cls):
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
factory_deps: Optional[Tuple[DependencyRequest, ...]] = None
|
|
76
|
+
has_instance_provides = False
|
|
77
|
+
for name in dir(cls):
|
|
78
|
+
try:
|
|
79
|
+
raw = inspect.getattr_static(cls, name)
|
|
80
|
+
if inspect.isfunction(raw) and getattr(raw, PICO_INFRA, None) == "provides":
|
|
81
|
+
has_instance_provides = True
|
|
82
|
+
break
|
|
83
|
+
except Exception:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
if has_instance_provides:
|
|
87
|
+
factory_deps = analyze_callable_dependencies(cls.__init__)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
for name in dir(cls):
|
|
91
|
+
try:
|
|
92
|
+
raw = inspect.getattr_static(cls, name)
|
|
93
|
+
except Exception:
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
fn = None
|
|
97
|
+
kind = None
|
|
98
|
+
if isinstance(raw, staticmethod):
|
|
99
|
+
fn = raw.__func__
|
|
100
|
+
kind = "static"
|
|
101
|
+
elif isinstance(raw, classmethod):
|
|
102
|
+
fn = raw.__func__
|
|
103
|
+
kind = "class"
|
|
104
|
+
elif inspect.isfunction(raw):
|
|
105
|
+
fn = raw
|
|
106
|
+
kind = "instance"
|
|
107
|
+
else:
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
if getattr(fn, PICO_INFRA, None) != "provides":
|
|
111
|
+
continue
|
|
112
|
+
if not self._enabled_by_condition(fn):
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
k = getattr(fn, PICO_KEY)
|
|
116
|
+
deps = analyze_callable_dependencies(fn)
|
|
117
|
+
|
|
118
|
+
if kind == "instance":
|
|
119
|
+
if factory_deps is None: factory_deps = analyze_callable_dependencies(cls.__init__)
|
|
120
|
+
provider = DeferredProvider(lambda pico, loc, fc=cls, mn=name, df=factory_deps, dm=deps: pico.build_method(getattr(pico.build_class(fc, loc, df), mn), loc, dm))
|
|
121
|
+
else:
|
|
122
|
+
provider = DeferredProvider(lambda pico, loc, f=fn, d=deps: pico.build_method(f, loc, d))
|
|
123
|
+
|
|
124
|
+
rt = get_return_type(fn)
|
|
125
|
+
qset = set(str(q) for q in getattr(fn, PICO_META, {}).get("qualifier", ()))
|
|
126
|
+
sc = getattr(fn, PICO_META, {}).get("scope", getattr(cls, PICO_META, {}).get("scope", "singleton"))
|
|
127
|
+
md = ProviderMetadata(key=k, provided_type=rt if isinstance(rt, type) else (k if isinstance(k, type) else None), concrete_class=None, factory_class=cls, factory_method=name, qualifiers=qset, primary=bool(getattr(fn, PICO_META, {}).get("primary")), lazy=bool(getattr(fn, PICO_META, {}).get("lazy", False)), infra=getattr(cls, PICO_INFRA, None), pico_name=getattr(fn, PICO_NAME, None), scope=sc, dependencies=deps)
|
|
128
|
+
self._queue(k, provider, md)
|
|
129
|
+
|
|
130
|
+
def _register_provides_function(self, fn: Callable[..., Any]) -> None:
|
|
131
|
+
if not self._enabled_by_condition(fn):
|
|
132
|
+
return
|
|
133
|
+
k = getattr(fn, PICO_KEY)
|
|
134
|
+
deps = analyze_callable_dependencies(fn)
|
|
135
|
+
provider = DeferredProvider(lambda pico, loc, f=fn, d=deps: pico.build_method(f, loc, d))
|
|
136
|
+
rt = get_return_type(fn)
|
|
137
|
+
qset = set(str(q) for q in getattr(fn, PICO_META, {}).get("qualifier", ()))
|
|
138
|
+
sc = getattr(fn, PICO_META, {}).get("scope", "singleton")
|
|
139
|
+
md = ProviderMetadata(key=k, provided_type=rt if isinstance(rt, type) else (k if isinstance(k, type) else None), concrete_class=None, factory_class=None, factory_method=getattr(fn, "__name__", None), qualifiers=qset, primary=bool(getattr(fn, PICO_META, {}).get("primary")), lazy=bool(getattr(fn, PICO_META, {}).get("lazy", False)), infra="provides", pico_name=getattr(fn, PICO_NAME, None), scope=sc, dependencies=deps)
|
|
140
|
+
self._queue(k, provider, md)
|
|
141
|
+
self._provides_functions[k] = fn
|
|
142
|
+
|
|
143
|
+
def scan_module(self, module: Any) -> None:
|
|
144
|
+
for _, obj in inspect.getmembers(module):
|
|
145
|
+
if inspect.isclass(obj):
|
|
146
|
+
meta = getattr(obj, PICO_META, {})
|
|
147
|
+
if "on_missing" in meta:
|
|
148
|
+
sel = meta["on_missing"]["selector"]
|
|
149
|
+
pr = int(meta["on_missing"].get("priority", 0))
|
|
150
|
+
self._on_missing.append((pr, sel, obj))
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
infra = getattr(obj, PICO_INFRA, None)
|
|
154
|
+
if infra == "component":
|
|
155
|
+
self._register_component_class(obj)
|
|
156
|
+
elif infra == "factory":
|
|
157
|
+
self._register_factory_class(obj)
|
|
158
|
+
elif infra == "configured":
|
|
159
|
+
enabled = self._enabled_by_condition(obj)
|
|
160
|
+
reg_data = self._config_manager.register_configured_class(obj, enabled)
|
|
161
|
+
if reg_data:
|
|
162
|
+
self._queue(reg_data[0], reg_data[1], reg_data[2])
|
|
163
|
+
|
|
164
|
+
for _, fn in inspect.getmembers(module, predicate=inspect.isfunction):
|
|
165
|
+
if getattr(fn, PICO_INFRA, None) == "provides":
|
|
166
|
+
self._register_provides_function(fn)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Optional, Protocol, Mapping, List, Tuple, Dict, Union
|
|
5
|
+
|
|
6
|
+
from .config_runtime import TreeSource, DictSource, JsonTreeSource, YamlTreeSource, Value
|
|
7
|
+
from .exceptions import ConfigurationError
|
|
8
|
+
|
|
9
|
+
class ConfigSource(Protocol):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
class EnvSource(ConfigSource):
|
|
13
|
+
def __init__(self, prefix: str = "") -> None:
|
|
14
|
+
self.prefix = prefix
|
|
15
|
+
def get(self, key: str) -> Optional[str]:
|
|
16
|
+
return os.environ.get(self.prefix + key)
|
|
17
|
+
|
|
18
|
+
class FileSource(ConfigSource):
|
|
19
|
+
def __init__(self, path: str, prefix: str = "") -> None:
|
|
20
|
+
self.prefix = prefix
|
|
21
|
+
try:
|
|
22
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
23
|
+
self._data = json.load(f)
|
|
24
|
+
except Exception:
|
|
25
|
+
self._data = {}
|
|
26
|
+
def get(self, key: str) -> Optional[str]:
|
|
27
|
+
k = self.prefix + key
|
|
28
|
+
v = self._data
|
|
29
|
+
for part in k.split("__"):
|
|
30
|
+
if isinstance(v, dict) and part in v:
|
|
31
|
+
v = v[part]
|
|
32
|
+
else:
|
|
33
|
+
return None
|
|
34
|
+
if isinstance(v, (str, int, float, bool)):
|
|
35
|
+
return str(v)
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
class FlatDictSource(ConfigSource):
|
|
39
|
+
def __init__(self, data: Mapping[str, Any], prefix: str = "", case_sensitive: bool = True):
|
|
40
|
+
base = dict(data)
|
|
41
|
+
if case_sensitive:
|
|
42
|
+
self._data = {str(k): v for k, v in base.items()}
|
|
43
|
+
self._prefix = prefix
|
|
44
|
+
else:
|
|
45
|
+
self._data = {str(k).upper(): v for k, v in base.items()}
|
|
46
|
+
self._prefix = prefix.upper()
|
|
47
|
+
self._case_sensitive = case_sensitive
|
|
48
|
+
def get(self, key: str) -> Optional[str]:
|
|
49
|
+
if not key:
|
|
50
|
+
return None
|
|
51
|
+
k = f"{self._prefix}{key}" if self._prefix else key
|
|
52
|
+
if not self._case_sensitive:
|
|
53
|
+
k = k.upper()
|
|
54
|
+
v = self._data.get(k)
|
|
55
|
+
if v is None:
|
|
56
|
+
return None
|
|
57
|
+
if isinstance(v, (str, int, float, bool)):
|
|
58
|
+
return str(v)
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class ContextConfig:
|
|
63
|
+
flat_sources: Tuple[Union[EnvSource, FileSource, FlatDictSource], ...]
|
|
64
|
+
tree_sources: Tuple[TreeSource, ...]
|
|
65
|
+
overrides: Dict[str, Any]
|
|
66
|
+
|
|
67
|
+
def configuration(
|
|
68
|
+
*sources: Any,
|
|
69
|
+
overrides: Optional[Dict[str, Any]] = None
|
|
70
|
+
) -> ContextConfig:
|
|
71
|
+
|
|
72
|
+
flat: List[Union[EnvSource, FileSource, FlatDictSource]] = []
|
|
73
|
+
tree: List[TreeSource] = []
|
|
74
|
+
|
|
75
|
+
for src in sources:
|
|
76
|
+
if isinstance(src, (EnvSource, FileSource, FlatDictSource)):
|
|
77
|
+
flat.append(src)
|
|
78
|
+
elif isinstance(src, TreeSource):
|
|
79
|
+
tree.append(src)
|
|
80
|
+
else:
|
|
81
|
+
raise ConfigurationError(f"Unknown configuration source type: {type(src)}")
|
|
82
|
+
|
|
83
|
+
return ContextConfig(
|
|
84
|
+
flat_sources=tuple(flat),
|
|
85
|
+
tree_sources=tuple(tree),
|
|
86
|
+
overrides=dict(overrides or {})
|
|
87
|
+
)
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from dataclasses import is_dataclass, fields, MISSING
|
|
3
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union, get_args, get_origin, Annotated
|
|
4
|
+
from .constants import PICO_INFRA, PICO_NAME, PICO_META
|
|
5
|
+
from .exceptions import ConfigurationError
|
|
6
|
+
from .factory import ProviderMetadata, DeferredProvider
|
|
7
|
+
from .config_builder import ContextConfig, ConfigSource, FlatDictSource, Value
|
|
8
|
+
from .config_runtime import ConfigResolver, TypeAdapterRegistry, ObjectGraphBuilder, TreeSource
|
|
9
|
+
from .analysis import analyze_callable_dependencies, DependencyRequest
|
|
10
|
+
|
|
11
|
+
KeyT = Union[str, type]
|
|
12
|
+
Provider = Callable[[], Any]
|
|
13
|
+
|
|
14
|
+
def _truthy(s: str) -> bool:
|
|
15
|
+
return s.strip().lower() in {"1", "true", "yes", "on", "y", "t"}
|
|
16
|
+
|
|
17
|
+
def _coerce(val: Optional[str], t: type) -> Any:
|
|
18
|
+
if val is None:
|
|
19
|
+
return None
|
|
20
|
+
if t is str:
|
|
21
|
+
return val
|
|
22
|
+
if t is int:
|
|
23
|
+
return int(val)
|
|
24
|
+
if t is float:
|
|
25
|
+
return float(val)
|
|
26
|
+
if t is bool:
|
|
27
|
+
return _truthy(val)
|
|
28
|
+
org = get_origin(t)
|
|
29
|
+
if org is Union:
|
|
30
|
+
args = [a for a in get_args(t) if a is not type(None)]
|
|
31
|
+
if not args:
|
|
32
|
+
return None
|
|
33
|
+
return _coerce(val, args[0])
|
|
34
|
+
return val
|
|
35
|
+
|
|
36
|
+
def _upper_key(name: str) -> str:
|
|
37
|
+
return name.upper()
|
|
38
|
+
|
|
39
|
+
def _lookup(sources: Tuple[ConfigSource, ...], key: str) -> Optional[str]:
|
|
40
|
+
for src in sources:
|
|
41
|
+
v = src.get(key)
|
|
42
|
+
if v is not None:
|
|
43
|
+
return v
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
class ConfigurationManager:
|
|
47
|
+
def __init__(self, config: Optional[ContextConfig]) -> None:
|
|
48
|
+
cfg = config or ContextConfig(flat_sources=(), tree_sources=(), overrides={})
|
|
49
|
+
|
|
50
|
+
self._flat_config = cfg.flat_sources
|
|
51
|
+
self._overrides = cfg.overrides
|
|
52
|
+
|
|
53
|
+
self._resolver = ConfigResolver(cfg.tree_sources)
|
|
54
|
+
self._adapters = TypeAdapterRegistry()
|
|
55
|
+
self._graph = ObjectGraphBuilder(self._resolver, self._adapters)
|
|
56
|
+
|
|
57
|
+
def _lookup_flat(self, key: str) -> Optional[str]:
|
|
58
|
+
if key in self._overrides:
|
|
59
|
+
return str(self._overrides[key])
|
|
60
|
+
for src in self._flat_config:
|
|
61
|
+
v = src.get(key)
|
|
62
|
+
if v is not None:
|
|
63
|
+
return v
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
def _build_flat_instance(self, cls: type, prefix: Optional[str]) -> Any:
|
|
67
|
+
if not is_dataclass(cls):
|
|
68
|
+
raise ConfigurationError(f"Configuration class {getattr(cls, '__name__', str(cls))} must be a dataclass")
|
|
69
|
+
values: Dict[str, Any] = {}
|
|
70
|
+
for f in fields(cls):
|
|
71
|
+
field_type = f.type
|
|
72
|
+
value_override = None
|
|
73
|
+
|
|
74
|
+
if get_origin(field_type) is Annotated:
|
|
75
|
+
args = get_args(field_type)
|
|
76
|
+
field_type = args[0] if args else Any
|
|
77
|
+
metas = args[1:] if len(args) > 1 else ()
|
|
78
|
+
for m in metas:
|
|
79
|
+
if isinstance(m, Value):
|
|
80
|
+
value_override = m.value
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
if value_override is not None:
|
|
84
|
+
values[f.name] = value_override
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
base_key = _upper_key(f.name)
|
|
88
|
+
keys_to_try = []
|
|
89
|
+
if prefix:
|
|
90
|
+
keys_to_try.append(prefix + base_key)
|
|
91
|
+
keys_to_try.append(base_key)
|
|
92
|
+
|
|
93
|
+
raw = None
|
|
94
|
+
for k in keys_to_try:
|
|
95
|
+
raw = self._lookup_flat(k)
|
|
96
|
+
if raw is not None:
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
if raw is None:
|
|
100
|
+
if f.default is not MISSING or f.default_factory is not MISSING:
|
|
101
|
+
continue
|
|
102
|
+
raise ConfigurationError(f"Missing configuration key: {(prefix or '') + base_key}")
|
|
103
|
+
values[f.name] = _coerce(raw, field_type if isinstance(field_type, type) or get_origin(field_type) else str)
|
|
104
|
+
return cls(**values)
|
|
105
|
+
|
|
106
|
+
def _auto_detect_mapping(self, target_type: type) -> str:
|
|
107
|
+
if not is_dataclass(target_type):
|
|
108
|
+
return "tree"
|
|
109
|
+
|
|
110
|
+
primitives = (str, int, float, bool)
|
|
111
|
+
for f in fields(target_type):
|
|
112
|
+
t = f.type
|
|
113
|
+
|
|
114
|
+
if get_origin(t) is Annotated:
|
|
115
|
+
args = get_args(t)
|
|
116
|
+
t = args[0] if args else Any
|
|
117
|
+
|
|
118
|
+
origin = get_origin(t)
|
|
119
|
+
|
|
120
|
+
if origin in (list, List, dict, Dict, Union):
|
|
121
|
+
return "tree"
|
|
122
|
+
if isinstance(t, type) and is_dataclass(t):
|
|
123
|
+
return "tree"
|
|
124
|
+
|
|
125
|
+
base_type = t
|
|
126
|
+
is_optional = origin is Union and type(None) in get_args(t)
|
|
127
|
+
if is_optional:
|
|
128
|
+
real_args = [a for a in get_args(t) if a is not type(None)]
|
|
129
|
+
if real_args:
|
|
130
|
+
base_type = real_args[0]
|
|
131
|
+
else:
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
if isinstance(base_type, type) and base_type not in primitives:
|
|
135
|
+
return "tree"
|
|
136
|
+
|
|
137
|
+
return "flat"
|
|
138
|
+
|
|
139
|
+
def register_configured_class(self, cls: type, enabled: bool) -> Optional[Tuple[KeyT, Provider, ProviderMetadata]]:
|
|
140
|
+
if not enabled:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
meta = getattr(cls, PICO_META, {})
|
|
144
|
+
cfg = meta.get("configured", None)
|
|
145
|
+
if not cfg:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
target = cfg.get("target")
|
|
149
|
+
prefix = cfg.get("prefix")
|
|
150
|
+
mapping = cfg.get("mapping", "auto")
|
|
151
|
+
|
|
152
|
+
if target == "self":
|
|
153
|
+
target = cls
|
|
154
|
+
|
|
155
|
+
if not isinstance(target, type):
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
if mapping == "auto":
|
|
159
|
+
mapping = self._auto_detect_mapping(target)
|
|
160
|
+
|
|
161
|
+
graph_builder = self._graph
|
|
162
|
+
qset = set(str(q) for q in meta.get("qualifier", ()))
|
|
163
|
+
sc = meta.get("scope", "singleton")
|
|
164
|
+
|
|
165
|
+
if mapping == "tree":
|
|
166
|
+
provider = DeferredProvider(lambda pico, loc, t=target, p=prefix, g=graph_builder: g.build_from_prefix(t, p))
|
|
167
|
+
deps = ()
|
|
168
|
+
if not is_dataclass(target) and hasattr(target, "__init__"):
|
|
169
|
+
deps = analyze_callable_dependencies(target.__init__)
|
|
170
|
+
md = ProviderMetadata(
|
|
171
|
+
key=target,
|
|
172
|
+
provided_type=target,
|
|
173
|
+
concrete_class=None,
|
|
174
|
+
factory_class=None,
|
|
175
|
+
factory_method=None,
|
|
176
|
+
qualifiers=qset,
|
|
177
|
+
primary=True,
|
|
178
|
+
lazy=bool(meta.get("lazy", False)),
|
|
179
|
+
infra="configured",
|
|
180
|
+
pico_name=prefix,
|
|
181
|
+
scope=sc,
|
|
182
|
+
dependencies=deps
|
|
183
|
+
)
|
|
184
|
+
return (target, provider, md)
|
|
185
|
+
|
|
186
|
+
elif mapping == "flat":
|
|
187
|
+
if not is_dataclass(target):
|
|
188
|
+
raise ConfigurationError(f"Target class {target.__name__} for flat mapping must be a dataclass")
|
|
189
|
+
|
|
190
|
+
provider = DeferredProvider(lambda pico, loc, c=target, p=prefix: self._build_flat_instance(c, p))
|
|
191
|
+
md = ProviderMetadata(
|
|
192
|
+
key=target,
|
|
193
|
+
provided_type=target,
|
|
194
|
+
concrete_class=target,
|
|
195
|
+
factory_class=None,
|
|
196
|
+
factory_method=None,
|
|
197
|
+
qualifiers=qset,
|
|
198
|
+
primary=True,
|
|
199
|
+
lazy=bool(meta.get("lazy", False)),
|
|
200
|
+
infra="configured",
|
|
201
|
+
pico_name=prefix,
|
|
202
|
+
scope=sc,
|
|
203
|
+
dependencies=()
|
|
204
|
+
)
|
|
205
|
+
return (target, provider, md)
|
|
206
|
+
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
def prefix_exists(self, md: ProviderMetadata) -> bool:
|
|
210
|
+
if md.infra != "configured":
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
target_type = md.provided_type or md.concrete_class
|
|
214
|
+
if not isinstance(target_type, type):
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
meta = getattr(target_type, PICO_META, {})
|
|
218
|
+
cfg = meta.get("configured", {})
|
|
219
|
+
mapping = cfg.get("mapping", "auto")
|
|
220
|
+
|
|
221
|
+
if mapping == "auto":
|
|
222
|
+
mapping = self._auto_detect_mapping(target_type)
|
|
223
|
+
|
|
224
|
+
if mapping == "tree":
|
|
225
|
+
try:
|
|
226
|
+
_ = self._resolver.subtree(md.pico_name)
|
|
227
|
+
return True
|
|
228
|
+
except Exception:
|
|
229
|
+
return False
|
|
230
|
+
else:
|
|
231
|
+
if not is_dataclass(target_type):
|
|
232
|
+
return False
|
|
233
|
+
prefix = md.pico_name or ""
|
|
234
|
+
keys = [_upper_key(f.name) for f in fields(target_type)]
|
|
235
|
+
return any(self._lookup_flat(prefix + k) is not None for k in keys)
|
|
236
|
+
|
pico_ioc/config_runtime.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# src/pico_ioc/config_runtime.py
|
|
2
1
|
import json
|
|
3
2
|
import os
|
|
4
3
|
import re
|
|
@@ -9,6 +8,10 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple, Union, g
|
|
|
9
8
|
from .exceptions import ConfigurationError
|
|
10
9
|
from .constants import PICO_META
|
|
11
10
|
|
|
11
|
+
class Value:
|
|
12
|
+
def __init__(self, value: Any):
|
|
13
|
+
self.value = value
|
|
14
|
+
|
|
12
15
|
class Discriminator:
|
|
13
16
|
def __init__(self, name: str):
|
|
14
17
|
self.name = name
|
|
@@ -153,18 +156,23 @@ class ObjectGraphBuilder:
|
|
|
153
156
|
def _build(self, node: Any, t: Any, path: Tuple[str, ...]) -> Any:
|
|
154
157
|
if t is Any or t is object:
|
|
155
158
|
return node
|
|
159
|
+
|
|
156
160
|
adapter = self._registry.get(t) if isinstance(t, type) else None
|
|
157
161
|
if adapter:
|
|
158
162
|
return adapter(node)
|
|
163
|
+
|
|
159
164
|
org = get_origin(t)
|
|
165
|
+
|
|
160
166
|
if org is Annotated:
|
|
161
|
-
base,
|
|
162
|
-
return self._build_discriminated(node, base,
|
|
167
|
+
base, metas = self._split_annotated(t)
|
|
168
|
+
return self._build_discriminated(node, base, metas, path)
|
|
169
|
+
|
|
163
170
|
if org in (list, List):
|
|
164
171
|
elem_t = get_args(t)[0] if get_args(t) else Any
|
|
165
172
|
if not isinstance(node, list):
|
|
166
173
|
raise ConfigurationError(f"Expected list at {'.'.join(path)}")
|
|
167
174
|
return [self._build(x, elem_t, path + (str(i),)) for i, x in enumerate(node)]
|
|
175
|
+
|
|
168
176
|
if org in (dict, Dict, Mapping):
|
|
169
177
|
args = get_args(t)
|
|
170
178
|
kt = args[0] if args else str
|
|
@@ -174,6 +182,7 @@ class ObjectGraphBuilder:
|
|
|
174
182
|
if not isinstance(node, dict):
|
|
175
183
|
raise ConfigurationError(f"Expected dict at {'.'.join(path)}")
|
|
176
184
|
return {k: self._build(v, vt, path + (k,)) for k, v in node.items()}
|
|
185
|
+
|
|
177
186
|
if org is Union:
|
|
178
187
|
args = [a for a in get_args(t)]
|
|
179
188
|
if not isinstance(node, dict):
|
|
@@ -183,6 +192,7 @@ class ObjectGraphBuilder:
|
|
|
183
192
|
except Exception:
|
|
184
193
|
continue
|
|
185
194
|
raise ConfigurationError(f"No union match at {'.'.join(path)}")
|
|
195
|
+
|
|
186
196
|
if "$type" in node:
|
|
187
197
|
tn = str(node["$type"])
|
|
188
198
|
for cand in args:
|
|
@@ -190,12 +200,14 @@ class ObjectGraphBuilder:
|
|
|
190
200
|
cleaned = {k: v for k, v in node.items() if k != "$type"}
|
|
191
201
|
return self._build(cleaned, cand, path)
|
|
192
202
|
raise ConfigurationError(f"Discriminator $type did not match at {'.'.join(path)}")
|
|
203
|
+
|
|
193
204
|
for cand in args:
|
|
194
205
|
try:
|
|
195
206
|
return self._build(node, cand, path)
|
|
196
207
|
except Exception:
|
|
197
208
|
continue
|
|
198
209
|
raise ConfigurationError(f"No union match at {'.'.join(path)}")
|
|
210
|
+
|
|
199
211
|
if isinstance(t, type) and issubclass(t, Enum):
|
|
200
212
|
if isinstance(node, str):
|
|
201
213
|
try:
|
|
@@ -205,6 +217,7 @@ class ObjectGraphBuilder:
|
|
|
205
217
|
if str(e.value) == node:
|
|
206
218
|
return e
|
|
207
219
|
raise ConfigurationError(f"Invalid enum at {'.'.join(path)}")
|
|
220
|
+
|
|
208
221
|
if isinstance(t, type) and is_dataclass(t):
|
|
209
222
|
if not isinstance(node, dict):
|
|
210
223
|
raise ConfigurationError(f"Expected object at {'.'.join(path)}")
|
|
@@ -219,6 +232,7 @@ class ObjectGraphBuilder:
|
|
|
219
232
|
else:
|
|
220
233
|
continue
|
|
221
234
|
return t(**vals)
|
|
235
|
+
|
|
222
236
|
if isinstance(t, type):
|
|
223
237
|
if t in (str, int, float, bool):
|
|
224
238
|
return self._coerce_prim(node, t, path)
|
|
@@ -234,27 +248,55 @@ class ObjectGraphBuilder:
|
|
|
234
248
|
if name in node:
|
|
235
249
|
kwargs[name] = self._build(node[name], p.annotation if p.annotation is not inspect._empty else Any, path + (name,))
|
|
236
250
|
return t(**kwargs)
|
|
251
|
+
|
|
237
252
|
return node
|
|
253
|
+
|
|
238
254
|
def _split_annotated(self, t: Any) -> Tuple[Any, Tuple[Any, ...]]:
|
|
239
255
|
args = get_args(t)
|
|
240
256
|
base = args[0] if args else Any
|
|
241
257
|
metas = tuple(args[1:]) if len(args) > 1 else ()
|
|
242
258
|
return base, metas
|
|
259
|
+
|
|
243
260
|
def _build_discriminated(self, node: Any, base: Any, metas: Tuple[Any, ...], path: Tuple[str, ...]) -> Any:
|
|
244
261
|
disc_name = None
|
|
262
|
+
disc_value = None
|
|
263
|
+
has_value = False
|
|
264
|
+
|
|
245
265
|
for m in metas:
|
|
246
266
|
if isinstance(m, Discriminator):
|
|
247
267
|
disc_name = m.name
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
268
|
+
if isinstance(m, Value):
|
|
269
|
+
disc_value = m.value
|
|
270
|
+
has_value = True
|
|
271
|
+
|
|
272
|
+
tn: Optional[str] = None
|
|
273
|
+
|
|
274
|
+
if disc_name and has_value:
|
|
275
|
+
tn = str(disc_value)
|
|
276
|
+
elif disc_name and isinstance(node, dict) and disc_name in node:
|
|
277
|
+
tn = str(node[disc_name])
|
|
278
|
+
|
|
279
|
+
if tn is not None and get_origin(base) is Union:
|
|
280
|
+
for cand in get_args(base):
|
|
281
|
+
if isinstance(cand, type) and getattr(cand, "__name__", "") == tn:
|
|
282
|
+
|
|
283
|
+
cleaned_node = {k: v for k, v in node.items() if k != disc_name}
|
|
284
|
+
|
|
285
|
+
if has_value:
|
|
286
|
+
cleaned_node[disc_name] = tn
|
|
287
|
+
|
|
288
|
+
return self._build(cleaned_node, cand, path)
|
|
289
|
+
|
|
290
|
+
raise ConfigurationError(
|
|
291
|
+
f"Discriminator value '{tn}' for field '{disc_name}' "
|
|
292
|
+
f"did not match any type in Union {base} at {'.'.join(path)}"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if has_value and not disc_name:
|
|
296
|
+
return disc_value
|
|
297
|
+
|
|
257
298
|
return self._build(node, base, path)
|
|
299
|
+
|
|
258
300
|
def _coerce_prim(self, node: Any, t: type, path: Tuple[str, ...]) -> Any:
|
|
259
301
|
if t is str:
|
|
260
302
|
if isinstance(node, str):
|
|
@@ -286,4 +328,3 @@ class ObjectGraphBuilder:
|
|
|
286
328
|
return False
|
|
287
329
|
raise ConfigurationError(f"Expected bool at {'.'.join(path)}")
|
|
288
330
|
return node
|
|
289
|
-
|