haiway 0.24.3__py3-none-any.whl → 0.25.1__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,127 @@
1
+ import inspect
2
+ from types import EllipsisType
3
+ from typing import Any, ClassVar, Self, dataclass_transform, final, get_origin, get_type_hints
4
+
5
+ from haiway.types.default import DefaultValue
6
+
7
+ __all__ = ("Immutable",)
8
+
9
+
10
+ @dataclass_transform(
11
+ kw_only_default=True,
12
+ frozen_default=True,
13
+ field_specifiers=(),
14
+ )
15
+ class ImmutableMeta(type):
16
+ def __new__(
17
+ mcs,
18
+ name: str,
19
+ bases: tuple[type, ...],
20
+ namespace: dict[str, Any],
21
+ **kwargs: Any,
22
+ ) -> type:
23
+ state_type = type.__new__(
24
+ mcs,
25
+ name,
26
+ bases,
27
+ namespace,
28
+ **kwargs,
29
+ )
30
+
31
+ state_type.__ATTRIBUTES__ = _collect_attributes(state_type) # pyright: ignore[reportAttributeAccessIssue]
32
+ state_type.__slots__ = tuple(state_type.__ATTRIBUTES__.keys()) # pyright: ignore[reportAttributeAccessIssue]
33
+ state_type.__match_args__ = state_type.__slots__ # pyright: ignore[reportAttributeAccessIssue]
34
+
35
+ # Only mark subclasses as final (not the base Immutable class itself)
36
+ if name != "Immutable":
37
+ state_type = final(state_type)
38
+
39
+ return state_type
40
+
41
+
42
+ def _collect_attributes(
43
+ cls: type[Any],
44
+ ) -> dict[str, DefaultValue[Any] | None]:
45
+ attributes: dict[str, DefaultValue[Any] | None] = {}
46
+ for key, annotation in get_type_hints(cls, localns={cls.__name__: cls}).items():
47
+ # do not include ClassVars
48
+ if (get_origin(annotation) or annotation) is ClassVar:
49
+ continue
50
+
51
+ field_value: Any = getattr(cls, key, inspect.Parameter.empty)
52
+
53
+ # Create a Field instance with the default value
54
+ if field_value is inspect.Parameter.empty:
55
+ attributes[key] = None
56
+
57
+ elif isinstance(field_value, DefaultValue):
58
+ attributes[key] = field_value
59
+
60
+ else:
61
+ attributes[key] = DefaultValue(field_value)
62
+
63
+ return attributes
64
+
65
+
66
+ class Immutable(metaclass=ImmutableMeta):
67
+ __IMMUTABLE__: ClassVar[EllipsisType] = ...
68
+ __ATTRIBUTES__: ClassVar[dict[str, DefaultValue | None]]
69
+
70
+ def __init__(
71
+ self,
72
+ **kwargs: Any,
73
+ ) -> None:
74
+ for name, default in self.__ATTRIBUTES__.items():
75
+ if name in kwargs:
76
+ object.__setattr__(
77
+ self,
78
+ name,
79
+ kwargs[name],
80
+ )
81
+
82
+ elif default is not None:
83
+ object.__setattr__(
84
+ self,
85
+ name,
86
+ default(),
87
+ )
88
+
89
+ else:
90
+ raise AttributeError(
91
+ f"Missing required attribute: {name}@{self.__class__.__qualname__}"
92
+ )
93
+
94
+ def __setattr__(
95
+ self,
96
+ name: str,
97
+ value: Any,
98
+ ) -> Any:
99
+ raise AttributeError(
100
+ f"Can't modify immutable {self.__class__.__qualname__},"
101
+ f" attribute - '{name}' cannot be modified"
102
+ )
103
+
104
+ def __delattr__(
105
+ self,
106
+ name: str,
107
+ ) -> None:
108
+ raise AttributeError(
109
+ f"Can't modify immutable {self.__class__.__qualname__},"
110
+ f" attribute - '{name}' cannot be deleted"
111
+ )
112
+
113
+ def __str__(self) -> str:
114
+ attributes: str = ", ".join([f"{key}: {value}" for key, value in vars(self).items()])
115
+ return f"{self.__class__.__name__}({attributes})"
116
+
117
+ def __repr__(self) -> str:
118
+ return str(self)
119
+
120
+ def __copy__(self) -> Self:
121
+ return self # Immutable, no need to provide an actual copy
122
+
123
+ def __deepcopy__(
124
+ self,
125
+ memo: dict[int, Any] | None,
126
+ ) -> Self:
127
+ return self # Immutable, no need to provide an actual copy
haiway/state/structure.py CHANGED
@@ -1,4 +1,3 @@
1
- import typing
2
1
  from collections.abc import Mapping
3
2
  from types import EllipsisType, GenericAlias
4
3
  from typing import (
@@ -131,7 +130,7 @@ class StateAttribute[Value]:
131
130
  @dataclass_transform(
132
131
  kw_only_default=True,
133
132
  frozen_default=True,
134
- field_specifiers=(DefaultValue,),
133
+ field_specifiers=(),
135
134
  )
136
135
  class StateMeta(type):
137
136
  """
@@ -150,7 +149,7 @@ class StateMeta(type):
150
149
  """
151
150
 
152
151
  def __new__(
153
- cls,
152
+ mcs,
154
153
  /,
155
154
  name: str,
156
155
  bases: tuple[type, ...],
@@ -158,29 +157,8 @@ class StateMeta(type):
158
157
  type_parameters: dict[str, Any] | None = None,
159
158
  **kwargs: Any,
160
159
  ) -> Any:
161
- """
162
- Create a new State class with processed attributes and validation.
163
-
164
- Parameters
165
- ----------
166
- name : str
167
- The name of the new class
168
- bases : tuple[type, ...]
169
- The base classes
170
- namespace : dict[str, Any]
171
- The class namespace (attributes and methods)
172
- type_parameters : dict[str, Any] | None
173
- Type parameters for generic specialization
174
- **kwargs : Any
175
- Additional arguments for class creation
176
-
177
- Returns
178
- -------
179
- Any
180
- The new class object
181
- """
182
- state_type = type.__new__(
183
- cls,
160
+ cls = type.__new__(
161
+ mcs,
184
162
  name,
185
163
  bases,
186
164
  namespace,
@@ -190,29 +168,27 @@ class StateMeta(type):
190
168
  attributes: dict[str, StateAttribute[Any]] = {}
191
169
 
192
170
  for key, annotation in attribute_annotations(
193
- state_type,
171
+ cls,
194
172
  type_parameters=type_parameters or {},
195
173
  ).items():
196
- default: Any = getattr(state_type, key, MISSING)
174
+ default: Any = getattr(cls, key, MISSING)
197
175
  attributes[key] = StateAttribute(
198
176
  name=key,
199
177
  annotation=annotation.update_required(default is MISSING),
200
178
  default=_resolve_default(default),
201
179
  validator=AttributeValidator.of(
202
180
  annotation,
203
- recursion_guard={
204
- str(AttributeAnnotation(origin=state_type)): state_type.validator
205
- },
181
+ recursion_guard={str(AttributeAnnotation(origin=cls)): cls.validator},
206
182
  ),
207
183
  )
208
184
 
209
- state_type.__TYPE_PARAMETERS__ = type_parameters # pyright: ignore[reportAttributeAccessIssue]
210
- state_type.__ATTRIBUTES__ = attributes # pyright: ignore[reportAttributeAccessIssue]
211
- state_type.__slots__ = frozenset(attributes.keys()) # pyright: ignore[reportAttributeAccessIssue]
212
- state_type.__match_args__ = state_type.__slots__ # pyright: ignore[reportAttributeAccessIssue]
213
- state_type._ = AttributePath(state_type, attribute=state_type) # pyright: ignore[reportCallIssue, reportUnknownMemberType, reportAttributeAccessIssue]
185
+ cls.__TYPE_PARAMETERS__ = type_parameters # pyright: ignore[reportAttributeAccessIssue]
186
+ cls.__ATTRIBUTES__ = attributes # pyright: ignore[reportAttributeAccessIssue]
187
+ cls.__slots__ = frozenset(attributes.keys()) # pyright: ignore[reportAttributeAccessIssue]
188
+ cls.__match_args__ = cls.__slots__ # pyright: ignore[reportAttributeAccessIssue]
189
+ cls._ = AttributePath(cls, attribute=cls) # pyright: ignore[reportCallIssue, reportUnknownMemberType, reportAttributeAccessIssue]
214
190
 
215
- return state_type
191
+ return cls
216
192
 
217
193
  def validator(
218
194
  cls,
@@ -241,24 +217,8 @@ class StateMeta(type):
241
217
  self,
242
218
  instance: Any,
243
219
  ) -> bool:
244
- """
245
- Check if an instance is an instance of this class.
246
-
247
- Implements isinstance() behavior for State classes, with special handling
248
- for generic type parameters and validation.
249
-
250
- Parameters
251
- ----------
252
- instance : Any
253
- The instance to check
254
-
255
- Returns
256
- -------
257
- bool
258
- True if the instance is an instance of this class, False otherwise
259
- """
260
220
  # check for type match
261
- if self.__subclasscheck__(type(instance)): # pyright: ignore[reportUnknownArgumentType]
221
+ if self.__subclasscheck__(type(instance)):
262
222
  return True
263
223
 
264
224
  # otherwise check if we are dealing with unparametrized base
@@ -276,85 +236,69 @@ class StateMeta(type):
276
236
  else:
277
237
  return True
278
238
 
279
- def __subclasscheck__( # noqa: C901, PLR0911, PLR0912
239
+ def __subclasscheck__(
280
240
  self,
281
241
  subclass: type[Any],
282
242
  ) -> bool:
283
- """
284
- Check if a class is a subclass of this class.
285
-
286
- Implements issubclass() behavior for State classes, with special handling
287
- for generic type parameters.
243
+ if self is subclass:
244
+ return True
288
245
 
289
- Parameters
290
- ----------
291
- subclass : type[Any]
292
- The class to check
246
+ self_origin: type[Any] = getattr(self, "__origin__", self)
247
+ subclass_origin: type[Any] = getattr(subclass, "__origin__", subclass)
293
248
 
294
- Returns
295
- -------
296
- bool
297
- True if the class is a subclass of this class, False otherwise
249
+ # Handle case where we're checking a parameterized type against unparameterized
250
+ if self_origin is self:
251
+ return type.__subclasscheck__(self, subclass)
298
252
 
299
- Raises
300
- ------
301
- RuntimeError
302
- If there is an issue with type parametrization
303
- """
304
- # check if we are the same class for early exit
305
- if self == subclass:
306
- return True
253
+ # Both must be based on the same generic class
254
+ if not issubclass(subclass_origin, self_origin):
255
+ return False
307
256
 
308
- # then check if we are parametrized
309
- checked_parameters: Mapping[str, Any] | None = getattr(
310
- self,
311
- "__TYPE_PARAMETERS__",
312
- None,
313
- )
314
- if checked_parameters is None:
315
- # if we are not parametrized allow any subclass
316
- return self in subclass.__bases__
317
-
318
- # verify if we have common base next - our generic subtypes have the same base
319
- if self.__bases__ == subclass.__bases__:
320
- # if we have the same bases we have different generic subtypes
321
- # we can verify all of the attributes to check if we have common base
322
- available_parameters: Mapping[str, Any] | None = getattr(
323
- subclass,
324
- "__TYPE_PARAMETERS__",
325
- None,
326
- )
257
+ return self._check_type_parameters(subclass)
327
258
 
328
- if available_parameters is None:
329
- # if we have no parameters at this stage this is a serious bug
330
- raise RuntimeError("Invalid type parametrization for %s", subclass)
259
+ def _check_type_parameters(
260
+ self,
261
+ subclass: type[Any],
262
+ ) -> bool:
263
+ self_params: Mapping[str, Any] | None = getattr(self, "__TYPE_PARAMETERS__", None)
264
+ subclass_params: Mapping[str, Any] | None = getattr(subclass, "__TYPE_PARAMETERS__", None)
331
265
 
332
- for key, param in checked_parameters.items():
333
- match available_parameters.get(key):
334
- case None: # if any parameter is missing we should not be there already
335
- return False
266
+ if self_params is None:
267
+ return True
336
268
 
337
- case typing.Any:
338
- continue # Any ignores type checks
269
+ # If subclass doesn't have type parameters, look in the MRO for a parametrized base
270
+ if subclass_params is None:
271
+ subclass_params = self._find_parametrized_base(subclass)
272
+ if subclass_params is None:
273
+ return False
339
274
 
340
- case checked:
341
- if param is Any:
342
- continue # Any ignores type checks
275
+ # Check if the type parameters are compatible (covariant)
276
+ for key, self_param in self_params.items():
277
+ subclass_param: type[Any] = subclass_params.get(key, Any)
278
+ if self_param is Any:
279
+ continue
343
280
 
344
- elif issubclass(checked, param):
345
- continue # if we have matching type we are fine
281
+ # For covariance: GenericState[Child] should be subclass of GenericState[Parent]
282
+ # This means subclass_param should be a subclass of self_param
283
+ if not issubclass(subclass_param, self_param):
284
+ return False
346
285
 
347
- else:
348
- return False # types are not matching
286
+ return True
349
287
 
350
- return True # when all parameters were matching we have matching subclass
288
+ def _find_parametrized_base(
289
+ self,
290
+ subclass: type[Any],
291
+ ) -> Mapping[str, Any] | None:
292
+ self_origin: type[Any] = getattr(self, "__origin__", self)
293
+ for base in getattr(subclass, "__mro__", ()):
294
+ if getattr(base, "__origin__", None) is not self_origin:
295
+ continue
351
296
 
352
- elif subclass in self.__bases__: # our generic subtypes have base of unparametrized type
353
- # if subclass parameters were not provided then we can be valid ony if all were Any
354
- return all(param is Any for param in checked_parameters.values())
297
+ subclass_params: Mapping[str, Any] | None = getattr(base, "__TYPE_PARAMETERS__", None)
298
+ if subclass_params is not None:
299
+ return subclass_params
355
300
 
356
- else:
357
- return False # we have different base / comparing to not parametrized
301
+ return None
358
302
 
359
303
 
360
304
  def _resolve_default[Value](
@@ -514,6 +458,8 @@ class State(metaclass=StateMeta):
514
458
  namespace={"__module__": cls.__module__},
515
459
  type_parameters=type_parameters,
516
460
  )
461
+ # Set origin for subclass checks
462
+ parametrized_type.__origin__ = cls # pyright: ignore[reportAttributeAccessIssue]
517
463
  _types_cache[(cls, type_arguments)] = parametrized_type
518
464
  return parametrized_type
519
465
 
@@ -1,4 +1,4 @@
1
- from collections.abc import Callable, Mapping, MutableMapping, Sequence, Set
1
+ from collections.abc import Callable, Collection, Mapping, MutableMapping, Sequence, Set
2
2
  from datetime import date, datetime, time, timedelta, timezone
3
3
  from enum import Enum
4
4
  from pathlib import Path
@@ -132,6 +132,14 @@ class AttributeValidator[Type]:
132
132
  _prepare_validator_of_type(annotation, recursion_guard),
133
133
  )
134
134
 
135
+ elif isinstance(annotation.origin, type):
136
+ # Handle arbitrary types as valid type annotations
137
+ object.__setattr__(
138
+ validator,
139
+ "validation",
140
+ _prepare_validator_of_type(annotation, recursion_guard),
141
+ )
142
+
135
143
  else:
136
144
  raise TypeError(f"Unsupported type annotation: {annotation}")
137
145
 
@@ -397,12 +405,11 @@ def _prepare_validator_of_type(
397
405
  value: Any,
398
406
  /,
399
407
  ) -> Any:
400
- match value:
401
- case value if isinstance(value, validated_type):
402
- return value
408
+ if isinstance(value, validated_type):
409
+ return value
403
410
 
404
- case _:
405
- raise TypeError(f"'{value}' is not matching expected type of '{formatted_type}'")
411
+ else:
412
+ raise TypeError(f"'{value}' is not matching expected type of '{formatted_type}'")
406
413
 
407
414
  return validator
408
415
 
@@ -440,8 +447,8 @@ def _prepare_validator_of_set(
440
447
  value: Any,
441
448
  /,
442
449
  ) -> Any:
443
- if isinstance(value, set):
444
- return frozenset(element_validator(element) for element in value) # pyright: ignore[reportUnknownVariableType]
450
+ if isinstance(value, Set):
451
+ return frozenset(element_validator(element) for element in value)
445
452
 
446
453
  else:
447
454
  raise TypeError(f"'{value}' is not matching expected type of '{formatted_type}'")
@@ -482,12 +489,53 @@ def _prepare_validator_of_sequence(
482
489
  value: Any,
483
490
  /,
484
491
  ) -> Any:
485
- match value:
486
- case [*elements]:
487
- return tuple(element_validator(element) for element in elements)
492
+ if isinstance(value, Sequence) and not isinstance(value, str | bytes):
493
+ return tuple(element_validator(element) for element in value)
488
494
 
489
- case _:
490
- raise TypeError(f"'{value}' is not matching expected type of '{formatted_type}'")
495
+ else:
496
+ raise TypeError(f"'{value}' is not matching expected type of '{formatted_type}'")
497
+
498
+ return validator
499
+
500
+
501
+ def _prepare_validator_of_collection(
502
+ annotation: AttributeAnnotation,
503
+ /,
504
+ recursion_guard: MutableMapping[str, AttributeValidation[Any]],
505
+ ) -> AttributeValidation[Any]:
506
+ """
507
+ Create a validator for collection types.
508
+
509
+ This validator checks if the value is a collection and validates each element
510
+ according to the collection's element type. Collections are converted to tuples.
511
+
512
+ Parameters
513
+ ----------
514
+ annotation : AttributeAnnotation
515
+ The collection type annotation
516
+ recursion_guard : MutableMapping[str, AttributeValidation[Any]]
517
+ Mapping to prevent infinite recursion for recursive types
518
+
519
+ Returns
520
+ -------
521
+ AttributeValidation[Any]
522
+ A validator that validates collections and their elements
523
+ """
524
+ element_validator: AttributeValidation[Any] = AttributeValidator.of(
525
+ annotation.arguments[0],
526
+ recursion_guard=recursion_guard,
527
+ )
528
+ formatted_type: str = str(annotation)
529
+
530
+ def validator(
531
+ value: Any,
532
+ /,
533
+ ) -> Any:
534
+ if isinstance(value, Collection) and not isinstance(value, str | bytes):
535
+ return tuple(element_validator(element) for element in value)
536
+
537
+ else:
538
+ raise TypeError(f"'{value}' is not matching expected type of '{formatted_type}'")
491
539
 
492
540
  return validator
493
541
 
@@ -529,16 +577,12 @@ def _prepare_validator_of_mapping(
529
577
  value: Any,
530
578
  /,
531
579
  ) -> Any:
532
- match value:
533
- case {**elements}:
534
- # TODO: make sure dict is not mutable with MappingProxyType?
535
- return {
536
- key_validator(key): value_validator(element)
537
- for key, element in elements.items()
538
- }
539
-
540
- case _:
541
- raise TypeError(f"'{value}' is not matching expected type of '{formatted_type}'")
580
+ if isinstance(value, Mapping):
581
+ # TODO: make sure dict is not mutable with MappingProxyType?
582
+ return {key_validator(key): value_validator(element) for key, element in value.items()}
583
+
584
+ else:
585
+ raise TypeError(f"'{value}' is not matching expected type of '{formatted_type}'")
542
586
 
543
587
  return validator
544
588
 
@@ -580,14 +624,11 @@ def _prepare_validator_of_tuple(
580
624
  value: Any,
581
625
  /,
582
626
  ) -> Any:
583
- match value:
584
- case [*elements]:
585
- return tuple(element_validator(element) for element in elements)
627
+ if isinstance(value, Collection) and not isinstance(value, str | bytes):
628
+ return tuple(element_validator(element) for element in value)
586
629
 
587
- case _:
588
- raise TypeError(
589
- f"'{value}' is not matching expected type of '{formatted_type}'"
590
- )
630
+ else:
631
+ raise TypeError(f"'{value}' is not matching expected type of '{formatted_type}'")
591
632
 
592
633
  return validator
593
634
 
@@ -603,22 +644,17 @@ def _prepare_validator_of_tuple(
603
644
  value: Any,
604
645
  /,
605
646
  ) -> Any:
606
- match value:
607
- case [*elements]:
608
- if len(elements) != elements_count:
609
- raise ValueError(
610
- f"'{value}' is not matching expected type of '{formatted_type}'"
611
- )
612
-
613
- return tuple(
614
- element_validators[idx](element) for idx, element in enumerate(elements)
615
- )
616
-
617
- case _:
618
- raise TypeError(
647
+ if isinstance(value, Sequence):
648
+ if len(value) != elements_count:
649
+ raise ValueError(
619
650
  f"'{value}' is not matching expected type of '{formatted_type}'"
620
651
  )
621
652
 
653
+ return tuple(element_validators[idx](element) for idx, element in enumerate(value))
654
+
655
+ else:
656
+ raise TypeError(f"'{value}' is not matching expected type of '{formatted_type}'")
657
+
622
658
  return validator
623
659
 
624
660
 
@@ -738,12 +774,11 @@ def _prepare_validator_of_typed_dict(
738
774
  value: Any,
739
775
  /,
740
776
  ) -> Any:
741
- match value:
742
- case value if isinstance(value, str):
743
- return value
777
+ if isinstance(value, str):
778
+ return value
744
779
 
745
- case _:
746
- raise TypeError(f"'{value}' is not matching expected type of 'str'")
780
+ else:
781
+ raise TypeError(f"'{value}' is not matching expected type of 'str'")
747
782
 
748
783
  formatted_type: str = str(annotation)
749
784
  values_validators: dict[str, AttributeValidation[Any]] = {
@@ -756,21 +791,20 @@ def _prepare_validator_of_typed_dict(
756
791
  value: Any,
757
792
  /,
758
793
  ) -> Any:
759
- match value:
760
- case {**elements}:
761
- validated: MutableMapping[str, Any] = {}
762
- for key, validator in values_validators.items():
763
- element: Any = elements.get(key, MISSING)
764
- if element is MISSING and key not in required_values:
765
- continue # skip missing and not required
794
+ if isinstance(value, Mapping):
795
+ validated: MutableMapping[str, Any] = {}
796
+ for key, validator in values_validators.items():
797
+ element: Any = value.get(key, MISSING)
798
+ if element is MISSING and key not in required_values:
799
+ continue # skip missing and not required
766
800
 
767
- validated[key_validator(key)] = validator(element)
801
+ validated[key_validator(key)] = validator(element)
768
802
 
769
- # TODO: make sure dict is not mutable with MappingProxyType?
770
- return validated
803
+ # TODO: make sure dict is not mutable with MappingProxyType?
804
+ return validated
771
805
 
772
- case _:
773
- raise TypeError(f"'{value}' is not matching expected type of '{formatted_type}'")
806
+ else:
807
+ raise TypeError(f"'{value}' is not matching expected type of '{formatted_type}'")
774
808
 
775
809
  return validator
776
810
 
@@ -799,6 +833,7 @@ VALIDATORS: Mapping[
799
833
  frozenset: _prepare_validator_of_set,
800
834
  Set: _prepare_validator_of_set,
801
835
  Sequence: _prepare_validator_of_sequence,
836
+ Collection: _prepare_validator_of_collection,
802
837
  Mapping: _prepare_validator_of_mapping,
803
838
  Literal: _prepare_validator_of_literal,
804
839
  range: _prepare_validator_of_type,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haiway
3
- Version: 0.24.3
3
+ Version: 0.25.1
4
4
  Summary: Framework for dependency injection and state management within structured concurrency model.
5
5
  Project-URL: Homepage, https://miquido.com
6
6
  Project-URL: Repository, https://github.com/miquido/haiway.git