typecraft 0.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.
- typecraft/__init__.py +4 -0
- typecraft/converting.py +308 -0
- typecraft/extras/__init__.py +4 -0
- typecraft/extras/toml.py +428 -0
- typecraft/extras/yaml.py +4 -0
- typecraft/inspecting/__init__.py +3 -0
- typecraft/inspecting/annotations.py +493 -0
- typecraft/inspecting/classes.py +105 -0
- typecraft/inspecting/functions.py +163 -0
- typecraft/models.py +352 -0
- typecraft/serializing.py +414 -0
- typecraft/typedefs.py +37 -0
- typecraft/utils/__init__.py +3 -0
- typecraft/utils/object_collections.py +59 -0
- typecraft/validating.py +575 -0
- typecraft-0.1.0.dist-info/LICENSE +21 -0
- typecraft-0.1.0.dist-info/METADATA +34 -0
- typecraft-0.1.0.dist-info/RECORD +19 -0
- typecraft-0.1.0.dist-info/WHEEL +4 -0
typecraft/__init__.py
ADDED
typecraft/converting.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Low-level conversion capability, agnostic of validation vs serialization.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
from collections.abc import Callable, Sequence
|
|
10
|
+
from typing import Any, cast
|
|
11
|
+
|
|
12
|
+
from .inspecting.annotations import Annotation
|
|
13
|
+
from .inspecting.functions import ParameterInfo, SignatureInfo
|
|
14
|
+
from .typedefs import VarianceType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConverterFunction:
|
|
18
|
+
"""
|
|
19
|
+
Encapsulates a validator or serializer function.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
func: Callable[..., Any]
|
|
23
|
+
"""
|
|
24
|
+
Converter function.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
sig_info: SignatureInfo
|
|
28
|
+
"""
|
|
29
|
+
Function signature.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
obj_param: ParameterInfo
|
|
33
|
+
"""
|
|
34
|
+
Parameter for object to be validated/serialized, must be positional.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
annotation_param: ParameterInfo | None
|
|
38
|
+
"""
|
|
39
|
+
Parameter for annotation, must be keyword.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
context_param: ParameterInfo | None
|
|
43
|
+
"""
|
|
44
|
+
Parameter for context, must be keyword.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self, func: Callable[..., Any], context_cls: type[BaseConversionContext]
|
|
49
|
+
):
|
|
50
|
+
sig_info = SignatureInfo(func)
|
|
51
|
+
|
|
52
|
+
# get object parameter
|
|
53
|
+
obj_param = next(
|
|
54
|
+
(p for p in sig_info.get_params(positional=True)),
|
|
55
|
+
None,
|
|
56
|
+
)
|
|
57
|
+
assert (
|
|
58
|
+
obj_param
|
|
59
|
+
), f"Function {func} does not take any positional params, must take obj as positional"
|
|
60
|
+
|
|
61
|
+
# get annotation parameter
|
|
62
|
+
annotation_param = next(
|
|
63
|
+
(p for p in sig_info.get_params(annotation=Annotation, keyword=True)),
|
|
64
|
+
None,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# get context parameter
|
|
68
|
+
context_param = next(
|
|
69
|
+
(p for p in sig_info.get_params(annotation=context_cls, keyword=True)),
|
|
70
|
+
None,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
expected_param_count = sum(
|
|
74
|
+
int(p is not None) for p in (obj_param, annotation_param, context_param)
|
|
75
|
+
)
|
|
76
|
+
if expected_param_count != len(sig_info.params):
|
|
77
|
+
raise TypeError(
|
|
78
|
+
f"Unexpected param count: expected {expected_param_count}, got {len(sig_info.params)}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
self.func = func
|
|
82
|
+
self.sig_info = sig_info
|
|
83
|
+
self.obj_param = obj_param
|
|
84
|
+
self.annotation_param = annotation_param
|
|
85
|
+
self.context_param = context_param
|
|
86
|
+
|
|
87
|
+
def invoke(
|
|
88
|
+
self, obj: Any, annotation: Annotation, context: BaseConversionContext
|
|
89
|
+
) -> Any:
|
|
90
|
+
kwargs: dict[str, Any] = {}
|
|
91
|
+
|
|
92
|
+
if param := self.annotation_param:
|
|
93
|
+
kwargs[param.parameter.name] = annotation
|
|
94
|
+
if param := self.context_param:
|
|
95
|
+
kwargs[param.parameter.name] = context
|
|
96
|
+
|
|
97
|
+
return self.func(obj, **kwargs)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class BaseTypedConverter[ConverterFuncT: Callable[..., Any]](ABC):
|
|
101
|
+
"""
|
|
102
|
+
Base class for typed converters (validators and serializers).
|
|
103
|
+
|
|
104
|
+
Encapsulates common conversion parameters and logic for type-based
|
|
105
|
+
conversion between source and target annotations.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
_source_annotation: Annotation
|
|
109
|
+
"""
|
|
110
|
+
Annotation specifying type to convert from.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
_target_annotation: Annotation
|
|
114
|
+
"""
|
|
115
|
+
Annotation specifying type to convert to.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
_func: ConverterFunction | None
|
|
119
|
+
"""
|
|
120
|
+
Function taking source type and returning an instance of target type.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
_variance: VarianceType
|
|
124
|
+
"""
|
|
125
|
+
Variance with respect to a reference annotation, either source or target depending
|
|
126
|
+
on serialization vs validation.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
source_annotation: Any,
|
|
132
|
+
target_annotation: Any,
|
|
133
|
+
/,
|
|
134
|
+
*,
|
|
135
|
+
func: ConverterFuncT | None = None,
|
|
136
|
+
variance: VarianceType = "contravariant",
|
|
137
|
+
):
|
|
138
|
+
self._source_annotation = Annotation._normalize(source_annotation)
|
|
139
|
+
self._target_annotation = Annotation._normalize(target_annotation)
|
|
140
|
+
self._func = ConverterFunction(func, self._get_context_cls()) if func else None
|
|
141
|
+
self._variance = variance
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def source_annotation(self) -> Annotation:
|
|
145
|
+
return self._source_annotation
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def target_annotation(self) -> Annotation:
|
|
149
|
+
return self._target_annotation
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def variance(self) -> VarianceType:
|
|
153
|
+
return self._variance
|
|
154
|
+
|
|
155
|
+
@abstractmethod
|
|
156
|
+
def can_convert(self, obj: Any, annotation: Annotation, /) -> bool:
|
|
157
|
+
"""
|
|
158
|
+
Check if this converter can convert the given object with the given annotation.
|
|
159
|
+
|
|
160
|
+
The meaning of 'annotation' depends on the converter type:
|
|
161
|
+
- For validators: target annotation (converting TO)
|
|
162
|
+
- For serializers: source annotation (converting FROM)
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
@abstractmethod
|
|
166
|
+
def _get_context_cls(self) -> type[BaseConversionContext]:
|
|
167
|
+
"""
|
|
168
|
+
Get the context class for this converter.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def _check_variance_match(
|
|
172
|
+
self,
|
|
173
|
+
annotation: Annotation,
|
|
174
|
+
reference_annotation: Annotation,
|
|
175
|
+
) -> bool:
|
|
176
|
+
"""Check if annotation matches reference based on variance."""
|
|
177
|
+
if self._variance == "invariant":
|
|
178
|
+
# exact match only
|
|
179
|
+
return annotation == reference_annotation
|
|
180
|
+
else:
|
|
181
|
+
# contravariant (default): annotation must be a subclass of reference
|
|
182
|
+
return annotation.is_subtype(reference_annotation)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class BaseConverterRegistry[ConverterT: BaseTypedConverter](ABC):
|
|
186
|
+
"""
|
|
187
|
+
Base class for converter registries.
|
|
188
|
+
|
|
189
|
+
Provides efficient lookup of converters based on object type and annotation.
|
|
190
|
+
Converters are indexed by a key type for fast lookup, with fallback to
|
|
191
|
+
sequential search for contravariant matching.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
_converter_map: dict[type, list[ConverterT]]
|
|
195
|
+
"""
|
|
196
|
+
Converters grouped by key type for efficiency.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
_converters: list[ConverterT]
|
|
200
|
+
"""
|
|
201
|
+
List of all converters for fallback/contravariant matching.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
def __init__(self, *converters: ConverterT):
|
|
205
|
+
self._converter_map = defaultdict(list)
|
|
206
|
+
self._converters = []
|
|
207
|
+
self.extend(converters)
|
|
208
|
+
|
|
209
|
+
def __len__(self) -> int:
|
|
210
|
+
return len(self._converters)
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def converters(self) -> list[ConverterT]:
|
|
214
|
+
"""
|
|
215
|
+
Get converters currently registered.
|
|
216
|
+
"""
|
|
217
|
+
return self._converters
|
|
218
|
+
|
|
219
|
+
def find(self, obj: Any, annotation: Annotation) -> ConverterT | None:
|
|
220
|
+
"""
|
|
221
|
+
Find the first converter that can handle the conversion.
|
|
222
|
+
|
|
223
|
+
Searches in order:
|
|
224
|
+
1. Exact key type matches
|
|
225
|
+
2. All converters (for contravariant matching)
|
|
226
|
+
"""
|
|
227
|
+
key_type = annotation.concrete_type
|
|
228
|
+
|
|
229
|
+
# first try converters registered for the exact key type
|
|
230
|
+
if key_type in self._converter_map:
|
|
231
|
+
for converter in self._converter_map[key_type]:
|
|
232
|
+
if converter.can_convert(obj, annotation):
|
|
233
|
+
return converter
|
|
234
|
+
|
|
235
|
+
# then try all converters (handles contravariant, generic cases)
|
|
236
|
+
for converter in self._converters:
|
|
237
|
+
if converter not in self._converter_map.get(key_type, []):
|
|
238
|
+
if converter.can_convert(obj, annotation):
|
|
239
|
+
return converter
|
|
240
|
+
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
def extend(self, converters: Sequence[ConverterT]):
|
|
244
|
+
"""
|
|
245
|
+
Register multiple converters.
|
|
246
|
+
"""
|
|
247
|
+
for converter in converters:
|
|
248
|
+
self._register_converter(converter)
|
|
249
|
+
|
|
250
|
+
@abstractmethod
|
|
251
|
+
def _get_map_key_type(self, converter: ConverterT) -> type:
|
|
252
|
+
"""
|
|
253
|
+
Get the type to use as key in the converter map for this converter.
|
|
254
|
+
|
|
255
|
+
- For validators: target type
|
|
256
|
+
- For serializers: source type
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
def _register_converter(self, converter: ConverterT):
|
|
260
|
+
"""
|
|
261
|
+
Register a converter object.
|
|
262
|
+
"""
|
|
263
|
+
map_key = self._get_map_key_type(converter)
|
|
264
|
+
self._converter_map[map_key].append(converter)
|
|
265
|
+
self._converters.append(converter)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class BaseConversionContext[RegistryT: BaseConverterRegistry](ABC):
|
|
269
|
+
"""
|
|
270
|
+
Base class for conversion contexts.
|
|
271
|
+
|
|
272
|
+
Encapsulates conversion parameters and provides access to the converter
|
|
273
|
+
registry, propagated throughout the conversion process.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
_registry: RegistryT
|
|
277
|
+
|
|
278
|
+
def __init__(self, *, registry: RegistryT | None = None):
|
|
279
|
+
self._registry = registry or self._create_default_registry()
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def registry(self) -> RegistryT:
|
|
283
|
+
return self._registry
|
|
284
|
+
|
|
285
|
+
@abstractmethod
|
|
286
|
+
def _create_default_registry(self) -> RegistryT:
|
|
287
|
+
"""
|
|
288
|
+
Create a default registry.
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def normalize_to_registry[ConverterT, RegistryT](
|
|
293
|
+
converter_cls: type[ConverterT],
|
|
294
|
+
registry_cls: type[RegistryT],
|
|
295
|
+
*converters_or_registry: Any,
|
|
296
|
+
) -> RegistryT:
|
|
297
|
+
"""
|
|
298
|
+
Take converters or registry and return a registry.
|
|
299
|
+
"""
|
|
300
|
+
if len(converters_or_registry) == 1 and isinstance(
|
|
301
|
+
converters_or_registry[0], registry_cls
|
|
302
|
+
):
|
|
303
|
+
registry = cast(RegistryT, converters_or_registry[0])
|
|
304
|
+
else:
|
|
305
|
+
assert all(isinstance(v, converter_cls) for v in converters_or_registry)
|
|
306
|
+
converters = cast(tuple[ConverterT, ...], converters_or_registry)
|
|
307
|
+
registry = registry_cls(*converters)
|
|
308
|
+
return registry
|