confarg 0.0.1.dev2__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.
- confarg/__init__.py +440 -0
- confarg/_argparse.py +958 -0
- confarg/_callable.py +593 -0
- confarg/_completion.py +318 -0
- confarg/_defaults.py +15 -0
- confarg/_errors.py +85 -0
- confarg/_files.py +426 -0
- confarg/_merge.py +284 -0
- confarg/_parse_cli.py +507 -0
- confarg/_parse_env.py +279 -0
- confarg/_serialize.py +206 -0
- confarg/_types.py +614 -0
- confarg/dictexpr/__init__.py +34 -0
- confarg/dictexpr/_expressions.py +566 -0
- confarg/typedload/__init__.py +44 -0
- confarg/typedload/_coerce.py +178 -0
- confarg/typedload/_construct.py +685 -0
- confarg-0.0.1.dev2.dist-info/METADATA +9 -0
- confarg-0.0.1.dev2.dist-info/RECORD +20 -0
- confarg-0.0.1.dev2.dist-info/WHEEL +4 -0
confarg/_types.py
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
"""Type introspection utilities for confarg."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import dataclasses
|
|
10
|
+
import enum
|
|
11
|
+
import inspect
|
|
12
|
+
import types
|
|
13
|
+
from collections.abc import (
|
|
14
|
+
Callable as CallableABC,
|
|
15
|
+
)
|
|
16
|
+
from collections.abc import (
|
|
17
|
+
Collection as CollectionABC,
|
|
18
|
+
)
|
|
19
|
+
from collections.abc import (
|
|
20
|
+
Iterable as IterableABC,
|
|
21
|
+
)
|
|
22
|
+
from collections.abc import (
|
|
23
|
+
Mapping as MappingABC,
|
|
24
|
+
)
|
|
25
|
+
from collections.abc import (
|
|
26
|
+
MutableMapping as MutableMappingABC,
|
|
27
|
+
)
|
|
28
|
+
from collections.abc import (
|
|
29
|
+
MutableSequence as MutableSequenceABC,
|
|
30
|
+
)
|
|
31
|
+
from collections.abc import (
|
|
32
|
+
MutableSet as MutableSetABC,
|
|
33
|
+
)
|
|
34
|
+
from collections.abc import (
|
|
35
|
+
Sequence as SequenceABC,
|
|
36
|
+
)
|
|
37
|
+
from collections.abc import (
|
|
38
|
+
Set as SetABC,
|
|
39
|
+
)
|
|
40
|
+
from pathlib import Path, PurePath
|
|
41
|
+
from typing import Any, Literal, Union, get_args, get_origin, get_type_hints
|
|
42
|
+
|
|
43
|
+
type TagPolicy = Literal["auto", "always"]
|
|
44
|
+
|
|
45
|
+
_MISSING = object()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _StrToken(str):
|
|
49
|
+
"""String value from CLI args or env vars — eligible for coercion to target type."""
|
|
50
|
+
|
|
51
|
+
__slots__ = ()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _resolve_type(tp: Any) -> Any:
|
|
55
|
+
"""Unwrap TypeAliasType and Annotated wrappers.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
tp: The type to resolve.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The unwrapped underlying type.
|
|
62
|
+
"""
|
|
63
|
+
while type(tp).__name__ == "TypeAliasType":
|
|
64
|
+
tp = tp.__value__
|
|
65
|
+
from typing import Annotated
|
|
66
|
+
|
|
67
|
+
if get_origin(tp) is Annotated:
|
|
68
|
+
tp = get_args(tp)[0]
|
|
69
|
+
return tp
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _is_struct_like(tp: Any) -> bool:
|
|
73
|
+
"""True if tp is a struct (dataclass or plain class), or a union with at least one struct variant."""
|
|
74
|
+
tp = _resolve_type(tp)
|
|
75
|
+
if _is_struct(tp):
|
|
76
|
+
return True
|
|
77
|
+
if _is_union(tp):
|
|
78
|
+
return any(_is_struct(_resolve_type(v)) for v in _union_args_no_none(tp))
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _is_dc(tp: Any) -> bool:
|
|
83
|
+
"""Check whether a type is a dataclass class (not an instance).
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
tp: The type to check.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if tp is a dataclass type.
|
|
90
|
+
"""
|
|
91
|
+
tp = _resolve_type(tp)
|
|
92
|
+
return dataclasses.is_dataclass(tp) and isinstance(tp, type)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _dc_fields(tp: Any) -> dict[str, Any]:
|
|
96
|
+
"""Return field names mapped to their resolved types for a dataclass.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
tp: A dataclass type.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
A dict mapping field names to their resolved type annotations.
|
|
103
|
+
"""
|
|
104
|
+
hints = get_type_hints(tp)
|
|
105
|
+
return {f.name: _resolve_type(hints[f.name]) for f in dataclasses.fields(tp)}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _dc_defaults(tp: Any) -> dict[str, Any]:
|
|
109
|
+
"""Return field names mapped to their default values.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
tp: A dataclass type.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
A dict mapping field names to default values for fields that have them.
|
|
116
|
+
"""
|
|
117
|
+
out: dict[str, Any] = {}
|
|
118
|
+
for f in dataclasses.fields(tp):
|
|
119
|
+
if f.default is not dataclasses.MISSING:
|
|
120
|
+
out[f.name] = f.default
|
|
121
|
+
elif f.default_factory is not dataclasses.MISSING:
|
|
122
|
+
out[f.name] = f.default_factory()
|
|
123
|
+
return out
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _is_none_type(tp: Any) -> bool:
|
|
127
|
+
"""Check whether a type is NoneType.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
tp: The type to check.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
True if tp is NoneType.
|
|
134
|
+
"""
|
|
135
|
+
return _resolve_type(tp) is type(None)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _is_union(tp: Any) -> bool:
|
|
139
|
+
"""Check whether a type is a Union (including X | Y syntax).
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
tp: The type to check.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
True if tp is a Union type.
|
|
146
|
+
"""
|
|
147
|
+
o = get_origin(_resolve_type(tp))
|
|
148
|
+
return o is Union or o is types.UnionType
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _union_args(tp: Any) -> list[Any]:
|
|
152
|
+
"""Return all type arguments of a Union type.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
tp: A Union type.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
A list of the Union's type arguments.
|
|
159
|
+
"""
|
|
160
|
+
return list(get_args(_resolve_type(tp)))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _union_args_no_none(tp: Any) -> list[Any]:
|
|
164
|
+
"""Return Union type arguments excluding NoneType.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
tp: A Union type.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
A list of the Union's type arguments with NoneType filtered out.
|
|
171
|
+
"""
|
|
172
|
+
return [a for a in get_args(_resolve_type(tp)) if a is not type(None)]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _allows_none(tp: Any) -> bool:
|
|
176
|
+
"""Check whether a type accepts None values.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
tp: The type to check.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
True if tp is NoneType or an Optional/Union that includes NoneType.
|
|
183
|
+
"""
|
|
184
|
+
tp = _resolve_type(tp)
|
|
185
|
+
if _is_none_type(tp):
|
|
186
|
+
return True
|
|
187
|
+
if _is_union(tp):
|
|
188
|
+
return type(None) in _union_args(tp)
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _unwrap_optional(tp: Any) -> Any | None:
|
|
193
|
+
"""Unwrap Optional[T] / Union[T, None] to T.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
- T (resolved) if tp is Optional[T] — exactly one non-None union variant.
|
|
197
|
+
- tp (resolved) if tp is not a union at all.
|
|
198
|
+
- None if tp is a multi-variant union (two or more non-None variants).
|
|
199
|
+
This sentinel is Python None, never NoneType; it signals the caller
|
|
200
|
+
must handle the multi-variant case separately.
|
|
201
|
+
"""
|
|
202
|
+
tp = _resolve_type(tp)
|
|
203
|
+
if not _is_union(tp):
|
|
204
|
+
return tp
|
|
205
|
+
non_none = _union_args_no_none(tp)
|
|
206
|
+
if len(non_none) == 1:
|
|
207
|
+
return _resolve_type(non_none[0])
|
|
208
|
+
return None # multi-variant — caller handles
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _is_bool(tp: Any) -> bool:
|
|
212
|
+
"""Check whether a type is bool.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
tp: The type to check.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
True if tp is bool.
|
|
219
|
+
"""
|
|
220
|
+
return _resolve_type(tp) is bool
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _origin(tp: Any) -> Any:
|
|
224
|
+
"""Return the generic origin of a type.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
tp: The type to inspect.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
The origin type (e.g. list for list[int]), or None.
|
|
231
|
+
"""
|
|
232
|
+
return get_origin(_resolve_type(tp))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
_LIST_ORIGINS = frozenset({list, SequenceABC, MutableSequenceABC, IterableABC, CollectionABC})
|
|
236
|
+
_SET_ORIGINS = frozenset({set, SetABC, MutableSetABC})
|
|
237
|
+
_DICT_ORIGINS = frozenset({dict, MappingABC, MutableMappingABC})
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _is_list(tp: Any) -> bool:
|
|
241
|
+
"""Check whether a type is list[...] or an abstract sequence type (Sequence, MutableSequence, Iterable, Collection).
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
tp: The type to check.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
True if tp is a list or sequence type.
|
|
248
|
+
"""
|
|
249
|
+
return _origin(tp) in _LIST_ORIGINS
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _is_set(tp: Any) -> bool:
|
|
253
|
+
"""Check whether a type is set[...] or an abstract set type (AbstractSet, MutableSet).
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
tp: The type to check.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
True if tp is a set type.
|
|
260
|
+
"""
|
|
261
|
+
return _origin(tp) in _SET_ORIGINS
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _is_frozenset(tp: Any) -> bool:
|
|
265
|
+
"""Check whether a type is frozenset[...].
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
tp: The type to check.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
True if tp is a frozenset type.
|
|
272
|
+
"""
|
|
273
|
+
return _origin(tp) is frozenset
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _is_tuple(tp: Any) -> bool:
|
|
277
|
+
"""Check whether a type is tuple[...].
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
tp: The type to check.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
True if tp is a tuple type.
|
|
284
|
+
"""
|
|
285
|
+
return _origin(tp) is tuple
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _is_dict(tp: Any) -> bool:
|
|
289
|
+
"""Check whether a type is dict[...] or an abstract mapping type (Mapping, MutableMapping).
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
tp: The type to check.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
True if tp is a dict or mapping type.
|
|
296
|
+
"""
|
|
297
|
+
return _origin(tp) in _DICT_ORIGINS
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _is_collection(tp: Any) -> bool:
|
|
301
|
+
"""Check whether a type is a list, set, frozenset, or tuple.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
tp: The type to check.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
True if tp is a list, set, frozenset, or tuple type.
|
|
308
|
+
"""
|
|
309
|
+
return _is_list(tp) or _is_set(tp) or _is_frozenset(tp) or _is_tuple(tp)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _is_varlen_collection(tp: Any) -> bool:
|
|
313
|
+
"""Check whether a type is a variable-length collection.
|
|
314
|
+
|
|
315
|
+
Matches list, set, frozenset, or tuple[X, ...].
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
tp: The type to check.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
True if tp is a variable-length collection type.
|
|
322
|
+
"""
|
|
323
|
+
tp = _resolve_type(tp)
|
|
324
|
+
if _is_list(tp) or _is_set(tp) or _is_frozenset(tp):
|
|
325
|
+
return True
|
|
326
|
+
if _is_tuple(tp):
|
|
327
|
+
a = get_args(tp)
|
|
328
|
+
return len(a) == 2 and a[1] is Ellipsis
|
|
329
|
+
return False
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _elem_type(tp: Any) -> Any:
|
|
333
|
+
"""Return the element type of a generic collection.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
tp: A generic collection type (e.g. list[int]).
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
The element type, or Any if not parameterized.
|
|
340
|
+
"""
|
|
341
|
+
a = get_args(_resolve_type(tp))
|
|
342
|
+
return _resolve_type(a[0]) if a else Any
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _tuple_types(tp: Any) -> list[Any] | None:
|
|
346
|
+
"""Return fixed-length element types for a tuple, or None for tuple[X, ...].
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
tp: A tuple type.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
A list of element types for fixed-length tuples, or None for variable-length.
|
|
353
|
+
"""
|
|
354
|
+
a = get_args(_resolve_type(tp))
|
|
355
|
+
if len(a) == 2 and a[1] is Ellipsis:
|
|
356
|
+
return None
|
|
357
|
+
return [_resolve_type(x) for x in a]
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _dict_kv(tp: Any) -> tuple[Any, Any]:
|
|
361
|
+
"""Return the key and value types of a dict type.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
tp: A dict type.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
A (key_type, value_type) tuple, defaulting to (str, Any).
|
|
368
|
+
"""
|
|
369
|
+
a = get_args(_resolve_type(tp))
|
|
370
|
+
return (_resolve_type(a[0]), _resolve_type(a[1])) if a else (str, Any)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _is_literal(tp: Any) -> bool:
|
|
374
|
+
"""Check whether a type is a Literal type.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
tp: The type to check.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
True if tp is a Literal type.
|
|
381
|
+
"""
|
|
382
|
+
from typing import Literal
|
|
383
|
+
|
|
384
|
+
return get_origin(_resolve_type(tp)) is Literal
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _literal_values(tp: Any) -> tuple[Any, ...]:
|
|
388
|
+
"""Return the allowed values of a Literal type.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
tp: A Literal type.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
A tuple of the Literal's allowed values.
|
|
395
|
+
"""
|
|
396
|
+
return get_args(_resolve_type(tp))
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _is_enum(tp: Any) -> bool:
|
|
400
|
+
"""Check whether a type is an Enum subclass.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
tp: The type to check.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
True if tp is an Enum type.
|
|
407
|
+
"""
|
|
408
|
+
tp = _resolve_type(tp)
|
|
409
|
+
return isinstance(tp, type) and issubclass(tp, enum.Enum)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _all_have_defaults(tp: Any) -> bool:
|
|
413
|
+
"""Check whether every field in a struct type has a default value."""
|
|
414
|
+
tp = _resolve_type(tp)
|
|
415
|
+
if not _is_struct(tp):
|
|
416
|
+
return False
|
|
417
|
+
defs = _struct_defaults(tp)
|
|
418
|
+
return all(name in defs for name in _struct_fields(tp))
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
_PLAIN_CLASS_BUILTINS = frozenset(
|
|
422
|
+
{str, int, float, bool, bytes, bytearray, type(None), list, dict, set, frozenset, tuple, CallableABC}
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _is_plain_class(tp: Any) -> bool:
|
|
427
|
+
"""True if tp is a non-dataclass, non-primitive class with __init__ parameters."""
|
|
428
|
+
tp = _resolve_type(tp)
|
|
429
|
+
if not isinstance(tp, type):
|
|
430
|
+
return False
|
|
431
|
+
if _is_dc(tp):
|
|
432
|
+
return False
|
|
433
|
+
if tp in _PLAIN_CLASS_BUILTINS or tp.__module__ == "builtins":
|
|
434
|
+
return False
|
|
435
|
+
if issubclass(tp, enum.Enum) or issubclass(tp, PurePath):
|
|
436
|
+
return False
|
|
437
|
+
try:
|
|
438
|
+
sig = inspect.signature(tp.__init__)
|
|
439
|
+
return any(p.name != "self" for p in sig.parameters.values())
|
|
440
|
+
except (ValueError, TypeError):
|
|
441
|
+
return False
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _init_fields(tp: Any) -> dict[str, Any]:
|
|
445
|
+
"""Return {name: resolved_type} for __init__ parameters of a plain class.
|
|
446
|
+
|
|
447
|
+
VAR_POSITIONAL (*args) becomes List[T] and VAR_KEYWORD (**kwargs) becomes
|
|
448
|
+
Dict[str, T], where T is the annotation on the parameter (Any if absent).
|
|
449
|
+
"""
|
|
450
|
+
try:
|
|
451
|
+
hints = get_type_hints(tp.__init__)
|
|
452
|
+
except Exception:
|
|
453
|
+
hints = {}
|
|
454
|
+
sig = inspect.signature(tp.__init__)
|
|
455
|
+
result: dict[str, Any] = {}
|
|
456
|
+
for name, param in sig.parameters.items():
|
|
457
|
+
if name == "self":
|
|
458
|
+
continue
|
|
459
|
+
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
460
|
+
elem = _resolve_type(hints.get(name, Any))
|
|
461
|
+
result[name] = list[elem]
|
|
462
|
+
elif param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
463
|
+
val = _resolve_type(hints.get(name, Any))
|
|
464
|
+
result[name] = dict[str, val]
|
|
465
|
+
else:
|
|
466
|
+
result[name] = _resolve_type(hints.get(name, Any))
|
|
467
|
+
return result
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _init_defaults(tp: Any) -> dict[str, Any]:
|
|
471
|
+
"""Return {name: default} for __init__ parameters that have defaults.
|
|
472
|
+
|
|
473
|
+
VAR_POSITIONAL and VAR_KEYWORD params always get implicit defaults of [] and {}.
|
|
474
|
+
"""
|
|
475
|
+
sig = inspect.signature(tp.__init__)
|
|
476
|
+
result: dict[str, Any] = {}
|
|
477
|
+
for name, param in sig.parameters.items():
|
|
478
|
+
if name == "self":
|
|
479
|
+
continue
|
|
480
|
+
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
481
|
+
result[name] = []
|
|
482
|
+
elif param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
483
|
+
result[name] = {}
|
|
484
|
+
elif param.default is not inspect.Parameter.empty:
|
|
485
|
+
result[name] = param.default
|
|
486
|
+
return result
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _var_param_names(tp: Any) -> frozenset[str]:
|
|
490
|
+
"""Return names of all *args and **kwargs parameters of tp.__init__."""
|
|
491
|
+
if _is_dc(tp):
|
|
492
|
+
return frozenset()
|
|
493
|
+
try:
|
|
494
|
+
sig = inspect.signature(tp.__init__)
|
|
495
|
+
return frozenset(
|
|
496
|
+
name
|
|
497
|
+
for name, param in sig.parameters.items()
|
|
498
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
|
|
499
|
+
)
|
|
500
|
+
except (ValueError, TypeError):
|
|
501
|
+
return frozenset()
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _var_positional_name(tp: Any) -> str | None:
|
|
505
|
+
"""Return the name of the *args parameter of tp.__init__, or None."""
|
|
506
|
+
if _is_dc(tp):
|
|
507
|
+
return None
|
|
508
|
+
try:
|
|
509
|
+
sig = inspect.signature(tp.__init__)
|
|
510
|
+
for name, param in sig.parameters.items():
|
|
511
|
+
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
512
|
+
return name
|
|
513
|
+
except (ValueError, TypeError):
|
|
514
|
+
pass
|
|
515
|
+
return None
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _var_keyword_name(tp: Any) -> str | None:
|
|
519
|
+
"""Return the name of the **kwargs parameter of tp.__init__, or None."""
|
|
520
|
+
if _is_dc(tp):
|
|
521
|
+
return None
|
|
522
|
+
try:
|
|
523
|
+
sig = inspect.signature(tp.__init__)
|
|
524
|
+
for name, param in sig.parameters.items():
|
|
525
|
+
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
526
|
+
return name
|
|
527
|
+
except (ValueError, TypeError):
|
|
528
|
+
pass
|
|
529
|
+
return None
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _is_struct(tp: Any) -> bool:
|
|
533
|
+
"""True if tp is a dataclass or a plain class with __init__ parameters."""
|
|
534
|
+
return _is_dc(tp) or _is_plain_class(tp)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _struct_fields(tp: Any) -> dict[str, Any]:
|
|
538
|
+
"""Return {name: type} for all fields/parameters of a struct or plain class."""
|
|
539
|
+
return _dc_fields(tp) if _is_dc(tp) else _init_fields(tp)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _struct_defaults(tp: Any) -> dict[str, Any]:
|
|
543
|
+
"""Return {name: default} for fields/parameters that have defaults."""
|
|
544
|
+
return _dc_defaults(tp) if _is_dc(tp) else _init_defaults(tp)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _is_type_ref(tp: Any) -> bool:
|
|
548
|
+
"""True for `type`, `type[X]`, or `Type[X]`."""
|
|
549
|
+
tp = _resolve_type(tp)
|
|
550
|
+
return tp is type or get_origin(tp) is type
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _type_ref_constraint(tp: Any) -> type:
|
|
554
|
+
"""Upper-bound class from `type[X]`; `object` for bare `type`."""
|
|
555
|
+
tp = _resolve_type(tp)
|
|
556
|
+
args = get_args(tp)
|
|
557
|
+
if args:
|
|
558
|
+
c = _resolve_type(args[0])
|
|
559
|
+
return c if isinstance(c, type) else object
|
|
560
|
+
return object
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _is_callable(tp: Any) -> bool:
|
|
564
|
+
"""Check whether a type is Callable[...] or bare Callable."""
|
|
565
|
+
tp = _resolve_type(tp)
|
|
566
|
+
return get_origin(tp) is CallableABC or tp is CallableABC
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _callable_param_types(tp: Any) -> list[Any] | None:
|
|
570
|
+
"""Return the declared parameter types for Callable[[T1, T2], R], or None.
|
|
571
|
+
|
|
572
|
+
Returns None for bare Callable or Callable[..., R] (no param constraint).
|
|
573
|
+
"""
|
|
574
|
+
args = get_args(_resolve_type(tp))
|
|
575
|
+
if not args:
|
|
576
|
+
return None
|
|
577
|
+
params = args[0]
|
|
578
|
+
if params is Ellipsis:
|
|
579
|
+
return None
|
|
580
|
+
return [_resolve_type(p) for p in params]
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _callable_return_type(tp: Any) -> Any | None:
|
|
584
|
+
"""Return the declared return type for Callable[..., R], or None for bare Callable."""
|
|
585
|
+
args = get_args(_resolve_type(tp))
|
|
586
|
+
if len(args) < 2:
|
|
587
|
+
return None
|
|
588
|
+
return _resolve_type(args[1])
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _try_coerce(ft: Any, token: _StrToken) -> Any:
|
|
592
|
+
"""Coerce a string token to the target type if unambiguous.
|
|
593
|
+
|
|
594
|
+
Coerces immediately for concrete leaf types (bool, int, float, Path,
|
|
595
|
+
Literal, Enum) so the merged dict has consistent types regardless of source.
|
|
596
|
+
str tokens are returned unchanged — _StrToken is already a str subclass.
|
|
597
|
+
For multi-variant unions, returns token unchanged for construct() to handle.
|
|
598
|
+
"""
|
|
599
|
+
from confarg.typedload._coerce import _coerce_leaf # lazy — avoids circular import
|
|
600
|
+
|
|
601
|
+
if ft is None:
|
|
602
|
+
return token
|
|
603
|
+
ft = _resolve_type(ft)
|
|
604
|
+
if _is_union(ft):
|
|
605
|
+
non_none = _union_args_no_none(ft)
|
|
606
|
+
if len(non_none) != 1:
|
|
607
|
+
return token
|
|
608
|
+
ft = _resolve_type(non_none[0])
|
|
609
|
+
if not (_is_literal(ft) or _is_enum(ft) or ft in (bool, int, float, Path)):
|
|
610
|
+
return token
|
|
611
|
+
try:
|
|
612
|
+
return _coerce_leaf(ft, token)
|
|
613
|
+
except Exception:
|
|
614
|
+
return token
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
"""confarg.dictexpr — expression interpolation engine for nested dicts.
|
|
6
|
+
|
|
7
|
+
Resolves ``${field.path}`` references and safe Python expressions embedded in
|
|
8
|
+
string values of a nested dict. The engine operates on plain dicts and has no
|
|
9
|
+
dependency on any config-source or dataclass logic.
|
|
10
|
+
|
|
11
|
+
Typical use::
|
|
12
|
+
|
|
13
|
+
from confarg.dictexpr import resolve_expressions
|
|
14
|
+
|
|
15
|
+
data = {"base": "/app", "log": "${base}/logs"}
|
|
16
|
+
resolved = resolve_expressions(data)
|
|
17
|
+
# resolved == {"base": "/app", "log": "/app/logs"}
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from confarg._errors import (
|
|
21
|
+
CircularReferenceError,
|
|
22
|
+
ExpressionEvalError,
|
|
23
|
+
MissingReferenceError,
|
|
24
|
+
UnsafeExpressionError,
|
|
25
|
+
)
|
|
26
|
+
from confarg.dictexpr._expressions import resolve_expressions
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"CircularReferenceError",
|
|
30
|
+
"ExpressionEvalError",
|
|
31
|
+
"MissingReferenceError",
|
|
32
|
+
"UnsafeExpressionError",
|
|
33
|
+
"resolve_expressions",
|
|
34
|
+
]
|