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.
@@ -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)
@@ -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
-