haiway 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.
@@ -0,0 +1,360 @@
1
+ import sys
2
+ import types
3
+ import typing
4
+ from collections.abc import Mapping
5
+ from types import NoneType, UnionType
6
+ from typing import (
7
+ Any,
8
+ ClassVar,
9
+ ForwardRef,
10
+ Generic,
11
+ Literal,
12
+ TypeAliasType,
13
+ TypeVar,
14
+ get_args,
15
+ get_origin,
16
+ get_type_hints,
17
+ )
18
+
19
+ __all__ = [
20
+ "attribute_annotations",
21
+ "AttributeAnnotation",
22
+ ]
23
+
24
+
25
+ class AttributeAnnotation:
26
+ def __init__(
27
+ self,
28
+ *,
29
+ origin: Any,
30
+ arguments: list[Any],
31
+ ) -> None:
32
+ self.origin: Any = origin
33
+ self.arguments: list[Any] = arguments
34
+
35
+ def __eq__(
36
+ self,
37
+ other: Any,
38
+ ) -> bool:
39
+ return self is other or (
40
+ isinstance(other, self.__class__)
41
+ and self.origin == other.origin
42
+ and self.arguments == other.arguments
43
+ )
44
+
45
+
46
+ def attribute_annotations(
47
+ cls: type[Any],
48
+ /,
49
+ type_parameters: dict[str, Any] | None = None,
50
+ ) -> dict[str, AttributeAnnotation]:
51
+ type_parameters = type_parameters or {}
52
+
53
+ self_annotation = AttributeAnnotation(
54
+ origin=cls,
55
+ arguments=[], # ignore self arguments here, Structure will have them resolved at this stage
56
+ )
57
+ localns: dict[str, Any] = {cls.__name__: cls}
58
+ recursion_guard: dict[Any, AttributeAnnotation] = {cls: self_annotation}
59
+ attributes: dict[str, AttributeAnnotation] = {}
60
+
61
+ for key, annotation in get_type_hints(cls, localns=localns).items():
62
+ # do not include ClassVars, private or dunder items
63
+ if ((get_origin(annotation) or annotation) is ClassVar) or key.startswith("_"):
64
+ continue
65
+
66
+ attributes[key] = _resolve_attribute_annotation(
67
+ annotation,
68
+ self_annotation=self_annotation,
69
+ type_parameters=type_parameters,
70
+ module=cls.__module__,
71
+ localns=localns,
72
+ recursion_guard=recursion_guard,
73
+ )
74
+
75
+ return attributes
76
+
77
+
78
+ def _resolve_attribute_annotation( # noqa: C901, PLR0911, PLR0912, PLR0913
79
+ annotation: Any,
80
+ /,
81
+ self_annotation: AttributeAnnotation | None,
82
+ type_parameters: dict[str, Any],
83
+ module: str,
84
+ localns: dict[str, Any],
85
+ recursion_guard: Mapping[Any, AttributeAnnotation], # TODO: verify recursion!
86
+ ) -> AttributeAnnotation:
87
+ # resolve annotation directly if able
88
+ match annotation:
89
+ # None
90
+ case types.NoneType | types.NoneType():
91
+ return AttributeAnnotation(
92
+ origin=NoneType,
93
+ arguments=[],
94
+ )
95
+
96
+ # forward reference through string
97
+ case str() as forward_ref:
98
+ return _resolve_attribute_annotation(
99
+ ForwardRef(forward_ref, module=module)._evaluate(
100
+ globalns=None,
101
+ localns=localns,
102
+ recursive_guard=frozenset(),
103
+ ),
104
+ self_annotation=self_annotation,
105
+ type_parameters=type_parameters,
106
+ module=module,
107
+ localns=localns,
108
+ recursion_guard=recursion_guard, # we might need to update it somehow?
109
+ )
110
+
111
+ # forward reference directly
112
+ case typing.ForwardRef() as reference:
113
+ return _resolve_attribute_annotation(
114
+ reference._evaluate(
115
+ globalns=None,
116
+ localns=localns,
117
+ recursive_guard=frozenset(),
118
+ ),
119
+ self_annotation=self_annotation,
120
+ type_parameters=type_parameters,
121
+ module=module,
122
+ localns=localns,
123
+ recursion_guard=recursion_guard, # we might need to update it somehow?
124
+ )
125
+
126
+ # generic alias aka parametrized type
127
+ case types.GenericAlias() as generic_alias:
128
+ match get_origin(generic_alias):
129
+ # check for an alias with parameters
130
+ case typing.TypeAliasType() as alias: # pyright: ignore[reportUnnecessaryComparison]
131
+ type_alias: AttributeAnnotation = AttributeAnnotation(
132
+ origin=TypeAliasType,
133
+ arguments=[],
134
+ )
135
+ resolved: AttributeAnnotation = _resolve_attribute_annotation(
136
+ alias.__value__,
137
+ self_annotation=None,
138
+ type_parameters=type_parameters,
139
+ module=module,
140
+ localns=localns,
141
+ recursion_guard=recursion_guard,
142
+ )
143
+ type_alias.origin = resolved.origin
144
+ type_alias.arguments = resolved.arguments
145
+ return type_alias
146
+
147
+ # check if we can resolve it as generic
148
+ case parametrized if issubclass(parametrized, Generic):
149
+ parametrized_type: Any = parametrized.__class_getitem__( # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
150
+ *(
151
+ type_parameters.get(
152
+ arg.__name__,
153
+ arg.__bound__ or Any,
154
+ )
155
+ if isinstance(arg, TypeVar)
156
+ else arg
157
+ for arg in get_args(generic_alias)
158
+ )
159
+ )
160
+
161
+ match parametrized_type:
162
+ # verify if we got any specific type or generic alias again
163
+ case types.GenericAlias():
164
+ return AttributeAnnotation(
165
+ origin=parametrized,
166
+ arguments=[
167
+ _resolve_attribute_annotation(
168
+ argument,
169
+ self_annotation=self_annotation,
170
+ type_parameters=type_parameters,
171
+ module=module,
172
+ localns=localns,
173
+ recursion_guard=recursion_guard,
174
+ )
175
+ for argument in get_args(generic_alias)
176
+ ],
177
+ )
178
+
179
+ # use resolved type if it is not an alias again
180
+ case _:
181
+ return AttributeAnnotation(
182
+ origin=parametrized_type,
183
+ arguments=[],
184
+ )
185
+
186
+ # anything else - try to resolve a concrete type or use as is
187
+ case origin:
188
+ return AttributeAnnotation(
189
+ origin=origin,
190
+ arguments=[
191
+ _resolve_attribute_annotation(
192
+ argument,
193
+ self_annotation=self_annotation,
194
+ type_parameters=type_parameters,
195
+ module=module,
196
+ localns=localns,
197
+ recursion_guard=recursion_guard,
198
+ )
199
+ for argument in get_args(generic_alias)
200
+ ],
201
+ )
202
+
203
+ # type alias
204
+ case typing.TypeAliasType() as alias:
205
+ type_alias: AttributeAnnotation = AttributeAnnotation(
206
+ origin=TypeAliasType,
207
+ arguments=[],
208
+ )
209
+ resolved: AttributeAnnotation = _resolve_attribute_annotation(
210
+ alias.__value__,
211
+ self_annotation=None,
212
+ type_parameters=type_parameters,
213
+ module=module,
214
+ localns=localns,
215
+ recursion_guard=recursion_guard,
216
+ )
217
+ type_alias.origin = resolved.origin
218
+ type_alias.arguments = resolved.arguments
219
+ return type_alias
220
+
221
+ # type parameter
222
+ case typing.TypeVar():
223
+ return _resolve_attribute_annotation(
224
+ # try to resolve it from current parameters if able
225
+ type_parameters.get(
226
+ annotation.__name__,
227
+ # use bound as default or Any otherwise
228
+ annotation.__bound__ or Any,
229
+ ),
230
+ self_annotation=None,
231
+ type_parameters=type_parameters,
232
+ module=module,
233
+ localns=localns,
234
+ recursion_guard=recursion_guard,
235
+ )
236
+
237
+ case typing.ParamSpec():
238
+ sys.stderr.write(
239
+ "ParamSpec is not supported for attribute annotations,"
240
+ " ignoring with Any type - it might incorrectly validate types\n"
241
+ )
242
+ return AttributeAnnotation(
243
+ origin=Any,
244
+ arguments=[],
245
+ )
246
+
247
+ case typing.TypeVarTuple():
248
+ sys.stderr.write(
249
+ "TypeVarTuple is not supported for attribute annotations,"
250
+ " ignoring with Any type - it might incorrectly validate types\n"
251
+ )
252
+ return AttributeAnnotation(
253
+ origin=Any,
254
+ arguments=[],
255
+ )
256
+
257
+ case _:
258
+ pass # proceed to resolving based on origin
259
+
260
+ # resolve based on origin if any
261
+ match get_origin(annotation) or annotation:
262
+ case types.UnionType | typing.Union:
263
+ return AttributeAnnotation(
264
+ origin=UnionType, # pyright: ignore[reportArgumentType]
265
+ arguments=[
266
+ recursion_guard.get(
267
+ argument,
268
+ _resolve_attribute_annotation(
269
+ argument,
270
+ self_annotation=self_annotation,
271
+ type_parameters=type_parameters,
272
+ module=module,
273
+ localns=localns,
274
+ recursion_guard=recursion_guard,
275
+ ),
276
+ )
277
+ for argument in get_args(annotation)
278
+ ],
279
+ )
280
+
281
+ case typing.Callable: # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
282
+ return AttributeAnnotation(
283
+ origin=typing.Callable,
284
+ arguments=[
285
+ _resolve_attribute_annotation(
286
+ argument,
287
+ self_annotation=self_annotation,
288
+ type_parameters=type_parameters,
289
+ module=module,
290
+ localns=localns,
291
+ recursion_guard=recursion_guard,
292
+ )
293
+ for argument in get_args(annotation)
294
+ ],
295
+ )
296
+
297
+ case typing.Self: # pyright: ignore[reportUnknownMemberType]
298
+ if not self_annotation:
299
+ sys.stderr.write(
300
+ "Unresolved Self attribute annotation,"
301
+ " ignoring with Any type - it might incorrectly validate types\n"
302
+ )
303
+ return AttributeAnnotation(
304
+ origin=Any,
305
+ arguments=[],
306
+ )
307
+
308
+ return self_annotation
309
+
310
+ # unwrap from irrelevant type wrappers
311
+ case typing.Annotated | typing.Final | typing.Required | typing.NotRequired:
312
+ return _resolve_attribute_annotation(
313
+ get_args(annotation)[0],
314
+ self_annotation=self_annotation,
315
+ type_parameters=type_parameters,
316
+ module=module,
317
+ localns=localns,
318
+ recursion_guard=recursion_guard,
319
+ )
320
+
321
+ case typing.Optional: # optional is a Union[Value, None]
322
+ return AttributeAnnotation(
323
+ origin=UnionType, # pyright: ignore[reportArgumentType]
324
+ arguments=[
325
+ _resolve_attribute_annotation(
326
+ get_args(annotation)[0],
327
+ self_annotation=self_annotation,
328
+ type_parameters=type_parameters,
329
+ module=module,
330
+ localns=localns,
331
+ recursion_guard=recursion_guard,
332
+ ),
333
+ AttributeAnnotation(
334
+ origin=NoneType,
335
+ arguments=[],
336
+ ),
337
+ ],
338
+ )
339
+
340
+ case typing.Literal:
341
+ return AttributeAnnotation(
342
+ origin=Literal,
343
+ arguments=list(get_args(annotation)),
344
+ )
345
+
346
+ case other: # finally use whatever there was
347
+ return AttributeAnnotation(
348
+ origin=other,
349
+ arguments=[
350
+ _resolve_attribute_annotation(
351
+ argument,
352
+ self_annotation=self_annotation,
353
+ type_parameters=type_parameters,
354
+ module=module,
355
+ localns=localns,
356
+ recursion_guard=recursion_guard,
357
+ )
358
+ for argument in get_args(other)
359
+ ],
360
+ )
@@ -0,0 +1,254 @@
1
+ from collections.abc import Callable
2
+ from copy import deepcopy
3
+ from types import GenericAlias
4
+ from typing import (
5
+ Any,
6
+ ClassVar,
7
+ Generic,
8
+ Self,
9
+ TypeVar,
10
+ cast,
11
+ dataclass_transform,
12
+ final,
13
+ get_origin,
14
+ )
15
+ from weakref import WeakValueDictionary
16
+
17
+ from haiway.state.attributes import AttributeAnnotation, attribute_annotations
18
+ from haiway.state.validation import attribute_type_validator
19
+ from haiway.types.missing import MISSING, Missing
20
+
21
+ __all__ = [
22
+ "Structure",
23
+ ]
24
+
25
+
26
+ @final
27
+ class StructureAttribute[Value]:
28
+ def __init__(
29
+ self,
30
+ annotation: AttributeAnnotation,
31
+ default: Value | Missing,
32
+ validator: Callable[[Any], Value],
33
+ ) -> None:
34
+ self.annotation: AttributeAnnotation = annotation
35
+ self.default: Value | Missing = default
36
+ self.validator: Callable[[Any], Value] = validator
37
+
38
+ def validated(
39
+ self,
40
+ value: Any | Missing,
41
+ /,
42
+ ) -> Value:
43
+ return self.validator(self.default if value is MISSING else value)
44
+
45
+
46
+ @dataclass_transform(
47
+ kw_only_default=True,
48
+ frozen_default=True,
49
+ field_specifiers=(),
50
+ )
51
+ class StructureMeta(type):
52
+ def __new__(
53
+ cls,
54
+ /,
55
+ name: str,
56
+ bases: tuple[type, ...],
57
+ namespace: dict[str, Any],
58
+ type_parameters: dict[str, Any] | None = None,
59
+ **kwargs: Any,
60
+ ) -> Any:
61
+ structure_type = type.__new__(
62
+ cls,
63
+ name,
64
+ bases,
65
+ namespace,
66
+ **kwargs,
67
+ )
68
+
69
+ attributes: dict[str, StructureAttribute[Any]] = {}
70
+
71
+ if bases: # handle base class
72
+ for key, annotation in attribute_annotations(
73
+ structure_type,
74
+ type_parameters=type_parameters,
75
+ ).items():
76
+ # do not include ClassVars and dunder items
77
+ if ((get_origin(annotation) or annotation) is ClassVar) or key.startswith("__"):
78
+ continue
79
+
80
+ attributes[key] = StructureAttribute(
81
+ annotation=annotation,
82
+ default=getattr(structure_type, key, MISSING),
83
+ validator=attribute_type_validator(annotation),
84
+ )
85
+
86
+ structure_type.__ATTRIBUTES__ = attributes # pyright: ignore[reportAttributeAccessIssue]
87
+ structure_type.__slots__ = frozenset(attributes.keys()) # pyright: ignore[reportAttributeAccessIssue]
88
+ structure_type.__match_args__ = structure_type.__slots__ # pyright: ignore[reportAttributeAccessIssue]
89
+
90
+ return structure_type
91
+
92
+
93
+ _types_cache: WeakValueDictionary[
94
+ tuple[
95
+ Any,
96
+ tuple[Any, ...],
97
+ ],
98
+ Any,
99
+ ] = WeakValueDictionary()
100
+
101
+
102
+ class Structure(metaclass=StructureMeta):
103
+ """
104
+ Base class for immutable data structures.
105
+ """
106
+
107
+ __ATTRIBUTES__: ClassVar[dict[str, StructureAttribute[Any]]]
108
+
109
+ def __class_getitem__(
110
+ cls,
111
+ type_argument: tuple[type[Any], ...] | type[Any],
112
+ ) -> type[Self]:
113
+ assert Generic in cls.__bases__, "Can't specialize non generic type!" # nosec: B101
114
+
115
+ type_arguments: tuple[type[Any], ...]
116
+ match type_argument:
117
+ case [*arguments]:
118
+ type_arguments = tuple(arguments)
119
+
120
+ case argument:
121
+ type_arguments = (argument,)
122
+
123
+ if any(isinstance(argument, TypeVar) for argument in type_arguments): # pyright: ignore[reportUnnecessaryIsInstance]
124
+ # if we got unfinished type treat it as an alias instead of resolving
125
+ return cast(type[Self], GenericAlias(cls, type_arguments))
126
+
127
+ assert len(type_arguments) == len( # nosec: B101
128
+ cls.__type_params__
129
+ ), "Type arguments count has to match type parameters count"
130
+
131
+ if cached := _types_cache.get((cls, type_arguments)):
132
+ return cached
133
+
134
+ type_parameters: dict[str, Any] = {
135
+ parameter.__name__: argument
136
+ for (parameter, argument) in zip(
137
+ cls.__type_params__ or (),
138
+ type_arguments or (),
139
+ strict=False,
140
+ )
141
+ }
142
+
143
+ parameter_names: str = ",".join(
144
+ getattr(
145
+ argument,
146
+ "__name__",
147
+ str(argument),
148
+ )
149
+ for argument in type_arguments
150
+ )
151
+ name: str = f"{cls.__name__}[{parameter_names}]"
152
+ bases: tuple[type[Self]] = (cls,)
153
+
154
+ parametrized_type: type[Self] = StructureMeta.__new__(
155
+ cls.__class__,
156
+ name=name,
157
+ bases=bases,
158
+ namespace={"__module__": cls.__module__},
159
+ type_parameters=type_parameters,
160
+ )
161
+ _types_cache[(cls, type_arguments)] = parametrized_type
162
+ return parametrized_type
163
+
164
+ def __init__(
165
+ self,
166
+ **kwargs: Any,
167
+ ) -> None:
168
+ for name, attribute in self.__ATTRIBUTES__.items():
169
+ object.__setattr__(
170
+ self, # pyright: ignore[reportUnknownArgumentType]
171
+ name,
172
+ attribute.validated(
173
+ kwargs.get(
174
+ name,
175
+ MISSING,
176
+ ),
177
+ ),
178
+ )
179
+
180
+ def updated(
181
+ self,
182
+ **kwargs: Any,
183
+ ) -> Self:
184
+ return self.__replace__(**kwargs)
185
+
186
+ def as_dict(self) -> dict[str, Any]:
187
+ return vars(self)
188
+
189
+ def __str__(self) -> str:
190
+ attributes: str = ", ".join([f"{key}: {value}" for key, value in vars(self).items()])
191
+ return f"{self.__class__.__name__}({attributes})"
192
+
193
+ def __repr__(self) -> str:
194
+ return str(self)
195
+
196
+ def __eq__(
197
+ self,
198
+ other: Any,
199
+ ) -> bool:
200
+ if not issubclass(other.__class__, self.__class__):
201
+ return False
202
+
203
+ return all(
204
+ getattr(self, key, MISSING) == getattr(other, key, MISSING)
205
+ for key in self.__ATTRIBUTES__.keys()
206
+ )
207
+
208
+ def __setattr__(
209
+ self,
210
+ name: str,
211
+ value: Any,
212
+ ) -> Any:
213
+ raise AttributeError(
214
+ f"Can't modify immutable structure {self.__class__.__qualname__},"
215
+ f" attribute - '{name}' cannot be modified"
216
+ )
217
+
218
+ def __delattr__(
219
+ self,
220
+ name: str,
221
+ ) -> None:
222
+ raise AttributeError(
223
+ f"Can't modify immutable structure {self.__class__.__qualname__},"
224
+ f" attribute - '{name}' cannot be deleted"
225
+ )
226
+
227
+ def __copy__(self) -> Self:
228
+ return self.__class__(**vars(self))
229
+
230
+ def __deepcopy__(
231
+ self,
232
+ memo: dict[int, Any] | None,
233
+ ) -> Self:
234
+ copy: Self = self.__class__(
235
+ **{
236
+ key: deepcopy(
237
+ value,
238
+ memo,
239
+ )
240
+ for key, value in vars(self).items()
241
+ }
242
+ )
243
+ return copy
244
+
245
+ def __replace__(
246
+ self,
247
+ **kwargs: Any,
248
+ ) -> Self:
249
+ return self.__class__(
250
+ **{
251
+ **vars(self),
252
+ **kwargs,
253
+ }
254
+ )