pico-ioc 2.0.5__py3-none-any.whl → 2.1.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 +7 -11
- pico_ioc/_version.py +1 -1
- pico_ioc/analysis.py +92 -0
- pico_ioc/aop.py +3 -6
- pico_ioc/api.py +27 -1084
- pico_ioc/component_scanner.py +166 -0
- pico_ioc/config_builder.py +91 -0
- pico_ioc/config_registrar.py +219 -0
- pico_ioc/config_runtime.py +17 -2
- pico_ioc/container.py +197 -156
- pico_ioc/decorators.py +192 -0
- pico_ioc/dependency_validator.py +103 -0
- pico_ioc/exceptions.py +0 -16
- pico_ioc/factory.py +2 -1
- pico_ioc/locator.py +80 -2
- pico_ioc/provider_selector.py +35 -0
- pico_ioc/registrar.py +169 -0
- {pico_ioc-2.0.5.dist-info → pico_ioc-2.1.0.dist-info}/METADATA +93 -44
- pico_ioc-2.1.0.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.0.dist-info}/WHEEL +0 -0
- {pico_ioc-2.0.5.dist-info → pico_ioc-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-2.0.5.dist-info → pico_ioc-2.1.0.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,91 @@
|
|
|
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
|
|
7
|
+
from .exceptions import ConfigurationError
|
|
8
|
+
|
|
9
|
+
class Value:
|
|
10
|
+
def __init__(self, value: Any):
|
|
11
|
+
self.value = value
|
|
12
|
+
|
|
13
|
+
class ConfigSource(Protocol):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
class EnvSource(ConfigSource):
|
|
17
|
+
def __init__(self, prefix: str = "") -> None:
|
|
18
|
+
self.prefix = prefix
|
|
19
|
+
def get(self, key: str) -> Optional[str]:
|
|
20
|
+
return os.environ.get(self.prefix + key)
|
|
21
|
+
|
|
22
|
+
class FileSource(ConfigSource):
|
|
23
|
+
def __init__(self, path: str, prefix: str = "") -> None:
|
|
24
|
+
self.prefix = prefix
|
|
25
|
+
try:
|
|
26
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
27
|
+
self._data = json.load(f)
|
|
28
|
+
except Exception:
|
|
29
|
+
self._data = {}
|
|
30
|
+
def get(self, key: str) -> Optional[str]:
|
|
31
|
+
k = self.prefix + key
|
|
32
|
+
v = self._data
|
|
33
|
+
for part in k.split("__"):
|
|
34
|
+
if isinstance(v, dict) and part in v:
|
|
35
|
+
v = v[part]
|
|
36
|
+
else:
|
|
37
|
+
return None
|
|
38
|
+
if isinstance(v, (str, int, float, bool)):
|
|
39
|
+
return str(v)
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
class FlatDictSource(ConfigSource):
|
|
43
|
+
def __init__(self, data: Mapping[str, Any], prefix: str = "", case_sensitive: bool = True):
|
|
44
|
+
base = dict(data)
|
|
45
|
+
if case_sensitive:
|
|
46
|
+
self._data = {str(k): v for k, v in base.items()}
|
|
47
|
+
self._prefix = prefix
|
|
48
|
+
else:
|
|
49
|
+
self._data = {str(k).upper(): v for k, v in base.items()}
|
|
50
|
+
self._prefix = prefix.upper()
|
|
51
|
+
self._case_sensitive = case_sensitive
|
|
52
|
+
def get(self, key: str) -> Optional[str]:
|
|
53
|
+
if not key:
|
|
54
|
+
return None
|
|
55
|
+
k = f"{self._prefix}{key}" if self._prefix else key
|
|
56
|
+
if not self._case_sensitive:
|
|
57
|
+
k = k.upper()
|
|
58
|
+
v = self._data.get(k)
|
|
59
|
+
if v is None:
|
|
60
|
+
return None
|
|
61
|
+
if isinstance(v, (str, int, float, bool)):
|
|
62
|
+
return str(v)
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class ContextConfig:
|
|
67
|
+
flat_sources: Tuple[Union[EnvSource, FileSource, FlatDictSource], ...]
|
|
68
|
+
tree_sources: Tuple[TreeSource, ...]
|
|
69
|
+
overrides: Dict[str, Any]
|
|
70
|
+
|
|
71
|
+
def configuration(
|
|
72
|
+
*sources: Any,
|
|
73
|
+
overrides: Optional[Dict[str, Any]] = None
|
|
74
|
+
) -> ContextConfig:
|
|
75
|
+
|
|
76
|
+
flat: List[Union[EnvSource, FileSource, FlatDictSource]] = []
|
|
77
|
+
tree: List[TreeSource] = []
|
|
78
|
+
|
|
79
|
+
for src in sources:
|
|
80
|
+
if isinstance(src, (EnvSource, FileSource, FlatDictSource)):
|
|
81
|
+
flat.append(src)
|
|
82
|
+
elif isinstance(src, TreeSource):
|
|
83
|
+
tree.append(src)
|
|
84
|
+
else:
|
|
85
|
+
raise ConfigurationError(f"Unknown configuration source type: {type(src)}")
|
|
86
|
+
|
|
87
|
+
return ContextConfig(
|
|
88
|
+
flat_sources=tuple(flat),
|
|
89
|
+
tree_sources=tuple(tree),
|
|
90
|
+
overrides=dict(overrides or {})
|
|
91
|
+
)
|
|
@@ -0,0 +1,219 @@
|
|
|
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
|
|
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
|
+
base_key = _upper_key(f.name)
|
|
72
|
+
keys_to_try = []
|
|
73
|
+
if prefix:
|
|
74
|
+
keys_to_try.append(prefix + base_key)
|
|
75
|
+
keys_to_try.append(base_key)
|
|
76
|
+
|
|
77
|
+
raw = None
|
|
78
|
+
for k in keys_to_try:
|
|
79
|
+
raw = self._lookup_flat(k)
|
|
80
|
+
if raw is not None:
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
if raw is None:
|
|
84
|
+
if f.default is not MISSING or f.default_factory is not MISSING:
|
|
85
|
+
continue
|
|
86
|
+
raise ConfigurationError(f"Missing configuration key: {(prefix or '') + base_key}")
|
|
87
|
+
values[f.name] = _coerce(raw, f.type if isinstance(f.type, type) or get_origin(f.type) else str)
|
|
88
|
+
return cls(**values)
|
|
89
|
+
|
|
90
|
+
def _auto_detect_mapping(self, target_type: type) -> str:
|
|
91
|
+
if not is_dataclass(target_type):
|
|
92
|
+
return "tree"
|
|
93
|
+
|
|
94
|
+
primitives = (str, int, float, bool)
|
|
95
|
+
for f in fields(target_type):
|
|
96
|
+
t = f.type
|
|
97
|
+
|
|
98
|
+
if get_origin(t) is Annotated:
|
|
99
|
+
args = get_args(t)
|
|
100
|
+
t = args[0] if args else Any
|
|
101
|
+
|
|
102
|
+
origin = get_origin(t)
|
|
103
|
+
|
|
104
|
+
if origin in (list, List, dict, Dict, Union):
|
|
105
|
+
return "tree"
|
|
106
|
+
if isinstance(t, type) and is_dataclass(t):
|
|
107
|
+
return "tree"
|
|
108
|
+
|
|
109
|
+
base_type = t
|
|
110
|
+
is_optional = origin is Union and type(None) in get_args(t)
|
|
111
|
+
if is_optional:
|
|
112
|
+
real_args = [a for a in get_args(t) if a is not type(None)]
|
|
113
|
+
if real_args:
|
|
114
|
+
base_type = real_args[0]
|
|
115
|
+
else:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
if isinstance(base_type, type) and base_type not in primitives:
|
|
119
|
+
return "tree"
|
|
120
|
+
|
|
121
|
+
return "flat"
|
|
122
|
+
|
|
123
|
+
def register_configured_class(self, cls: type, enabled: bool) -> Optional[Tuple[KeyT, Provider, ProviderMetadata]]:
|
|
124
|
+
if not enabled:
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
meta = getattr(cls, PICO_META, {})
|
|
128
|
+
cfg = meta.get("configured", None)
|
|
129
|
+
if not cfg:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
target = cfg.get("target")
|
|
133
|
+
prefix = cfg.get("prefix")
|
|
134
|
+
mapping = cfg.get("mapping", "auto")
|
|
135
|
+
|
|
136
|
+
if target == "self":
|
|
137
|
+
target = cls
|
|
138
|
+
|
|
139
|
+
if not isinstance(target, type):
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
if mapping == "auto":
|
|
143
|
+
mapping = self._auto_detect_mapping(target)
|
|
144
|
+
|
|
145
|
+
graph_builder = self._graph
|
|
146
|
+
qset = set(str(q) for q in meta.get("qualifier", ()))
|
|
147
|
+
sc = meta.get("scope", "singleton")
|
|
148
|
+
|
|
149
|
+
if mapping == "tree":
|
|
150
|
+
provider = DeferredProvider(lambda pico, loc, t=target, p=prefix, g=graph_builder: g.build_from_prefix(t, p))
|
|
151
|
+
deps = ()
|
|
152
|
+
if not is_dataclass(target) and hasattr(target, "__init__"):
|
|
153
|
+
deps = analyze_callable_dependencies(target.__init__)
|
|
154
|
+
md = ProviderMetadata(
|
|
155
|
+
key=target,
|
|
156
|
+
provided_type=target,
|
|
157
|
+
concrete_class=None,
|
|
158
|
+
factory_class=None,
|
|
159
|
+
factory_method=None,
|
|
160
|
+
qualifiers=qset,
|
|
161
|
+
primary=True,
|
|
162
|
+
lazy=False,
|
|
163
|
+
infra="configured",
|
|
164
|
+
pico_name=prefix,
|
|
165
|
+
scope=sc,
|
|
166
|
+
dependencies=deps
|
|
167
|
+
)
|
|
168
|
+
return (target, provider, md)
|
|
169
|
+
|
|
170
|
+
elif mapping == "flat":
|
|
171
|
+
if not is_dataclass(target):
|
|
172
|
+
raise ConfigurationError(f"Target class {target.__name__} for flat mapping must be a dataclass")
|
|
173
|
+
|
|
174
|
+
provider = DeferredProvider(lambda pico, loc, c=target, p=prefix: self._build_flat_instance(c, p))
|
|
175
|
+
md = ProviderMetadata(
|
|
176
|
+
key=target,
|
|
177
|
+
provided_type=target,
|
|
178
|
+
concrete_class=target,
|
|
179
|
+
factory_class=None,
|
|
180
|
+
factory_method=None,
|
|
181
|
+
qualifiers=qset,
|
|
182
|
+
primary=True,
|
|
183
|
+
lazy=False,
|
|
184
|
+
infra="configured",
|
|
185
|
+
pico_name=prefix,
|
|
186
|
+
scope=sc,
|
|
187
|
+
dependencies=()
|
|
188
|
+
)
|
|
189
|
+
return (target, provider, md)
|
|
190
|
+
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
def prefix_exists(self, md: ProviderMetadata) -> bool:
|
|
194
|
+
if md.infra != "configured":
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
target_type = md.provided_type or md.concrete_class
|
|
198
|
+
if not isinstance(target_type, type):
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
meta = getattr(target_type, PICO_META, {})
|
|
202
|
+
cfg = meta.get("configured", {})
|
|
203
|
+
mapping = cfg.get("mapping", "auto")
|
|
204
|
+
|
|
205
|
+
if mapping == "auto":
|
|
206
|
+
mapping = self._auto_detect_mapping(target_type)
|
|
207
|
+
|
|
208
|
+
if mapping == "tree":
|
|
209
|
+
try:
|
|
210
|
+
_ = self._resolver.subtree(md.pico_name)
|
|
211
|
+
return True
|
|
212
|
+
except Exception:
|
|
213
|
+
return False
|
|
214
|
+
else:
|
|
215
|
+
if not is_dataclass(target_type):
|
|
216
|
+
return False
|
|
217
|
+
prefix = md.pico_name or ""
|
|
218
|
+
keys = [_upper_key(f.name) for f in fields(target_type)]
|
|
219
|
+
return any(self._lookup_flat(prefix + k) is not None for k in keys)
|
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
|
|
@@ -153,18 +152,23 @@ class ObjectGraphBuilder:
|
|
|
153
152
|
def _build(self, node: Any, t: Any, path: Tuple[str, ...]) -> Any:
|
|
154
153
|
if t is Any or t is object:
|
|
155
154
|
return node
|
|
155
|
+
|
|
156
156
|
adapter = self._registry.get(t) if isinstance(t, type) else None
|
|
157
157
|
if adapter:
|
|
158
158
|
return adapter(node)
|
|
159
|
+
|
|
159
160
|
org = get_origin(t)
|
|
161
|
+
|
|
160
162
|
if org is Annotated:
|
|
161
163
|
base, meta = self._split_annotated(t)
|
|
162
164
|
return self._build_discriminated(node, base, meta, path)
|
|
165
|
+
|
|
163
166
|
if org in (list, List):
|
|
164
167
|
elem_t = get_args(t)[0] if get_args(t) else Any
|
|
165
168
|
if not isinstance(node, list):
|
|
166
169
|
raise ConfigurationError(f"Expected list at {'.'.join(path)}")
|
|
167
170
|
return [self._build(x, elem_t, path + (str(i),)) for i, x in enumerate(node)]
|
|
171
|
+
|
|
168
172
|
if org in (dict, Dict, Mapping):
|
|
169
173
|
args = get_args(t)
|
|
170
174
|
kt = args[0] if args else str
|
|
@@ -174,6 +178,7 @@ class ObjectGraphBuilder:
|
|
|
174
178
|
if not isinstance(node, dict):
|
|
175
179
|
raise ConfigurationError(f"Expected dict at {'.'.join(path)}")
|
|
176
180
|
return {k: self._build(v, vt, path + (k,)) for k, v in node.items()}
|
|
181
|
+
|
|
177
182
|
if org is Union:
|
|
178
183
|
args = [a for a in get_args(t)]
|
|
179
184
|
if not isinstance(node, dict):
|
|
@@ -183,6 +188,7 @@ class ObjectGraphBuilder:
|
|
|
183
188
|
except Exception:
|
|
184
189
|
continue
|
|
185
190
|
raise ConfigurationError(f"No union match at {'.'.join(path)}")
|
|
191
|
+
|
|
186
192
|
if "$type" in node:
|
|
187
193
|
tn = str(node["$type"])
|
|
188
194
|
for cand in args:
|
|
@@ -190,12 +196,14 @@ class ObjectGraphBuilder:
|
|
|
190
196
|
cleaned = {k: v for k, v in node.items() if k != "$type"}
|
|
191
197
|
return self._build(cleaned, cand, path)
|
|
192
198
|
raise ConfigurationError(f"Discriminator $type did not match at {'.'.join(path)}")
|
|
199
|
+
|
|
193
200
|
for cand in args:
|
|
194
201
|
try:
|
|
195
202
|
return self._build(node, cand, path)
|
|
196
203
|
except Exception:
|
|
197
204
|
continue
|
|
198
205
|
raise ConfigurationError(f"No union match at {'.'.join(path)}")
|
|
206
|
+
|
|
199
207
|
if isinstance(t, type) and issubclass(t, Enum):
|
|
200
208
|
if isinstance(node, str):
|
|
201
209
|
try:
|
|
@@ -205,6 +213,7 @@ class ObjectGraphBuilder:
|
|
|
205
213
|
if str(e.value) == node:
|
|
206
214
|
return e
|
|
207
215
|
raise ConfigurationError(f"Invalid enum at {'.'.join(path)}")
|
|
216
|
+
|
|
208
217
|
if isinstance(t, type) and is_dataclass(t):
|
|
209
218
|
if not isinstance(node, dict):
|
|
210
219
|
raise ConfigurationError(f"Expected object at {'.'.join(path)}")
|
|
@@ -219,6 +228,7 @@ class ObjectGraphBuilder:
|
|
|
219
228
|
else:
|
|
220
229
|
continue
|
|
221
230
|
return t(**vals)
|
|
231
|
+
|
|
222
232
|
if isinstance(t, type):
|
|
223
233
|
if t in (str, int, float, bool):
|
|
224
234
|
return self._coerce_prim(node, t, path)
|
|
@@ -234,18 +244,22 @@ class ObjectGraphBuilder:
|
|
|
234
244
|
if name in node:
|
|
235
245
|
kwargs[name] = self._build(node[name], p.annotation if p.annotation is not inspect._empty else Any, path + (name,))
|
|
236
246
|
return t(**kwargs)
|
|
247
|
+
|
|
237
248
|
return node
|
|
249
|
+
|
|
238
250
|
def _split_annotated(self, t: Any) -> Tuple[Any, Tuple[Any, ...]]:
|
|
239
251
|
args = get_args(t)
|
|
240
252
|
base = args[0] if args else Any
|
|
241
253
|
metas = tuple(args[1:]) if len(args) > 1 else ()
|
|
242
254
|
return base, metas
|
|
255
|
+
|
|
243
256
|
def _build_discriminated(self, node: Any, base: Any, metas: Tuple[Any, ...], path: Tuple[str, ...]) -> Any:
|
|
244
257
|
disc_name = None
|
|
245
258
|
for m in metas:
|
|
246
259
|
if isinstance(m, Discriminator):
|
|
247
260
|
disc_name = m.name
|
|
248
261
|
break
|
|
262
|
+
|
|
249
263
|
if disc_name and isinstance(node, dict) and disc_name in node:
|
|
250
264
|
if get_origin(base) is Union:
|
|
251
265
|
tn = str(node[disc_name])
|
|
@@ -254,7 +268,9 @@ class ObjectGraphBuilder:
|
|
|
254
268
|
cleaned = {k: v for k, v in node.items() if k != disc_name}
|
|
255
269
|
return self._build(cleaned, cand, path)
|
|
256
270
|
raise ConfigurationError(f"Discriminator {disc_name} did not match at {'.'.join(path)}")
|
|
271
|
+
|
|
257
272
|
return self._build(node, base, path)
|
|
273
|
+
|
|
258
274
|
def _coerce_prim(self, node: Any, t: type, path: Tuple[str, ...]) -> Any:
|
|
259
275
|
if t is str:
|
|
260
276
|
if isinstance(node, str):
|
|
@@ -286,4 +302,3 @@ class ObjectGraphBuilder:
|
|
|
286
302
|
return False
|
|
287
303
|
raise ConfigurationError(f"Expected bool at {'.'.join(path)}")
|
|
288
304
|
return node
|
|
289
|
-
|