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 ADDED
@@ -0,0 +1,4 @@
1
+ """
2
+ TypeCraft: Annotation-native toolkit for data validation, transformation, and
3
+ type inspection
4
+ """
@@ -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
@@ -0,0 +1,4 @@
1
+ """
2
+ Specific models accommodating different formats. To use, install this package with the
3
+ respective extras group, e.g. `typecraft[toml]`.
4
+ """