haiway 0.6.2__py3-none-any.whl → 0.6.4__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.
haiway/__init__.py CHANGED
@@ -17,7 +17,7 @@ from haiway.helpers import (
17
17
  traced,
18
18
  wrap_async,
19
19
  )
20
- from haiway.state import State
20
+ from haiway.state import AttributePath, AttributeRequirement, State
21
21
  from haiway.types import (
22
22
  MISSING,
23
23
  Missing,
@@ -46,6 +46,8 @@ __all__ = [
46
46
  "MISSING",
47
47
  "ArgumentsTrace",
48
48
  "AsyncQueue",
49
+ "AttributePath",
50
+ "AttributeRequirement",
49
51
  "Disposable",
50
52
  "Disposables",
51
53
  "Missing",
haiway/state/__init__.py CHANGED
@@ -1,8 +1,12 @@
1
1
  from haiway.state.attributes import AttributeAnnotation, attribute_annotations
2
+ from haiway.state.path import AttributePath
3
+ from haiway.state.requirement import AttributeRequirement
2
4
  from haiway.state.structure import State
3
5
 
4
6
  __all__ = [
5
7
  "AttributeAnnotation",
8
+ "AttributePath",
9
+ "AttributeRequirement",
6
10
  "State",
7
11
  "attribute_annotations",
8
12
  ]
@@ -19,6 +19,7 @@ from typing import (
19
19
  __all__ = [
20
20
  "AttributeAnnotation",
21
21
  "attribute_annotations",
22
+ "resolve_attribute_annotation",
22
23
  ]
23
24
 
24
25
 
@@ -68,7 +69,7 @@ def attribute_annotations(
68
69
  if ((get_origin(annotation) or annotation) is ClassVar) or key.startswith("_"):
69
70
  continue
70
71
 
71
- attributes[key] = _resolve_attribute_annotation(
72
+ attributes[key] = resolve_attribute_annotation(
72
73
  annotation,
73
74
  self_annotation=self_annotation,
74
75
  type_parameters=type_parameters,
@@ -80,7 +81,7 @@ def attribute_annotations(
80
81
  return attributes
81
82
 
82
83
 
83
- def _resolve_attribute_annotation( # noqa: C901, PLR0911, PLR0912, PLR0913
84
+ def resolve_attribute_annotation( # noqa: C901, PLR0911, PLR0912, PLR0913
84
85
  annotation: Any,
85
86
  /,
86
87
  self_annotation: AttributeAnnotation | None,
@@ -100,7 +101,7 @@ def _resolve_attribute_annotation( # noqa: C901, PLR0911, PLR0912, PLR0913
100
101
 
101
102
  # forward reference through string
102
103
  case str() as forward_ref:
103
- return _resolve_attribute_annotation(
104
+ return resolve_attribute_annotation(
104
105
  ForwardRef(forward_ref, module=module)._evaluate(
105
106
  globalns=None,
106
107
  localns=localns,
@@ -115,7 +116,7 @@ def _resolve_attribute_annotation( # noqa: C901, PLR0911, PLR0912, PLR0913
115
116
 
116
117
  # forward reference directly
117
118
  case typing.ForwardRef() as reference:
118
- return _resolve_attribute_annotation(
119
+ return resolve_attribute_annotation(
119
120
  reference._evaluate(
120
121
  globalns=None,
121
122
  localns=localns,
@@ -137,7 +138,7 @@ def _resolve_attribute_annotation( # noqa: C901, PLR0911, PLR0912, PLR0913
137
138
  origin=TypeAliasType,
138
139
  arguments=[],
139
140
  )
140
- resolved: AttributeAnnotation = _resolve_attribute_annotation(
141
+ resolved: AttributeAnnotation = resolve_attribute_annotation(
141
142
  alias.__value__,
142
143
  self_annotation=None,
143
144
  type_parameters=type_parameters,
@@ -169,7 +170,7 @@ def _resolve_attribute_annotation( # noqa: C901, PLR0911, PLR0912, PLR0913
169
170
  return AttributeAnnotation(
170
171
  origin=parametrized,
171
172
  arguments=[
172
- _resolve_attribute_annotation(
173
+ resolve_attribute_annotation(
173
174
  argument,
174
175
  self_annotation=self_annotation,
175
176
  type_parameters=type_parameters,
@@ -193,7 +194,7 @@ def _resolve_attribute_annotation( # noqa: C901, PLR0911, PLR0912, PLR0913
193
194
  return AttributeAnnotation(
194
195
  origin=origin,
195
196
  arguments=[
196
- _resolve_attribute_annotation(
197
+ resolve_attribute_annotation(
197
198
  argument,
198
199
  self_annotation=self_annotation,
199
200
  type_parameters=type_parameters,
@@ -211,7 +212,7 @@ def _resolve_attribute_annotation( # noqa: C901, PLR0911, PLR0912, PLR0913
211
212
  origin=TypeAliasType,
212
213
  arguments=[],
213
214
  )
214
- resolved: AttributeAnnotation = _resolve_attribute_annotation(
215
+ resolved: AttributeAnnotation = resolve_attribute_annotation(
215
216
  alias.__value__,
216
217
  self_annotation=None,
217
218
  type_parameters=type_parameters,
@@ -225,7 +226,7 @@ def _resolve_attribute_annotation( # noqa: C901, PLR0911, PLR0912, PLR0913
225
226
 
226
227
  # type parameter
227
228
  case typing.TypeVar():
228
- return _resolve_attribute_annotation(
229
+ return resolve_attribute_annotation(
229
230
  # try to resolve it from current parameters if able
230
231
  type_parameters.get(
231
232
  annotation.__name__,
@@ -270,7 +271,7 @@ def _resolve_attribute_annotation( # noqa: C901, PLR0911, PLR0912, PLR0913
270
271
  arguments=[
271
272
  recursion_guard.get(
272
273
  argument,
273
- _resolve_attribute_annotation(
274
+ resolve_attribute_annotation(
274
275
  argument,
275
276
  self_annotation=self_annotation,
276
277
  type_parameters=type_parameters,
@@ -287,7 +288,7 @@ def _resolve_attribute_annotation( # noqa: C901, PLR0911, PLR0912, PLR0913
287
288
  return AttributeAnnotation(
288
289
  origin=typing.Callable,
289
290
  arguments=[
290
- _resolve_attribute_annotation(
291
+ resolve_attribute_annotation(
291
292
  argument,
292
293
  self_annotation=self_annotation,
293
294
  type_parameters=type_parameters,
@@ -314,7 +315,7 @@ def _resolve_attribute_annotation( # noqa: C901, PLR0911, PLR0912, PLR0913
314
315
 
315
316
  # unwrap from irrelevant type wrappers
316
317
  case typing.Annotated | typing.Final | typing.Required | typing.NotRequired:
317
- return _resolve_attribute_annotation(
318
+ return resolve_attribute_annotation(
318
319
  get_args(annotation)[0],
319
320
  self_annotation=self_annotation,
320
321
  type_parameters=type_parameters,
@@ -327,7 +328,7 @@ def _resolve_attribute_annotation( # noqa: C901, PLR0911, PLR0912, PLR0913
327
328
  return AttributeAnnotation(
328
329
  origin=UnionType, # pyright: ignore[reportArgumentType]
329
330
  arguments=[
330
- _resolve_attribute_annotation(
331
+ resolve_attribute_annotation(
331
332
  get_args(annotation)[0],
332
333
  self_annotation=self_annotation,
333
334
  type_parameters=type_parameters,
@@ -352,7 +353,7 @@ def _resolve_attribute_annotation( # noqa: C901, PLR0911, PLR0912, PLR0913
352
353
  return AttributeAnnotation(
353
354
  origin=other,
354
355
  arguments=[
355
- _resolve_attribute_annotation(
356
+ resolve_attribute_annotation(
356
357
  argument,
357
358
  self_annotation=self_annotation,
358
359
  type_parameters=type_parameters,
haiway/state/path.py ADDED
@@ -0,0 +1,524 @@
1
+ import builtins
2
+ import types
3
+ import typing
4
+ from abc import ABC, abstractmethod
5
+ from collections import abc as collections_abc
6
+ from collections import deque
7
+ from collections.abc import Callable, Mapping, Sequence
8
+ from copy import copy
9
+ from typing import Any, final, get_args, get_origin, overload
10
+
11
+ from haiway.types import MISSING, Missing, not_missing
12
+
13
+ __all__ = [
14
+ "AttributePath",
15
+ ]
16
+
17
+
18
+ class AttributePathComponent(ABC):
19
+ @abstractmethod
20
+ def path_str(
21
+ self,
22
+ current: str | None = None,
23
+ /,
24
+ ) -> str: ...
25
+
26
+ @abstractmethod
27
+ def access(
28
+ self,
29
+ subject: Any,
30
+ /,
31
+ ) -> Any: ...
32
+
33
+ @abstractmethod
34
+ def assigning(
35
+ self,
36
+ subject: Any,
37
+ /,
38
+ value: Any,
39
+ ) -> Any: ...
40
+
41
+
42
+ @final
43
+ class PropertyAttributePathComponent(AttributePathComponent):
44
+ def __init__[Root, Attribute](
45
+ self,
46
+ root: type[Root],
47
+ *,
48
+ attribute: type[Attribute],
49
+ name: str,
50
+ ) -> None:
51
+ root_origin: Any = get_origin(root) or root
52
+ attribute_origin: Any = get_origin(attribute) or attribute
53
+
54
+ def access(
55
+ subject: Root,
56
+ /,
57
+ ) -> Attribute:
58
+ assert isinstance(subject, root_origin), ( # nosec: B101
59
+ f"AttributePath used on unexpected subject of"
60
+ f" '{type(subject).__name__}' instead of '{root.__name__}' for '{name}'"
61
+ )
62
+
63
+ assert hasattr(subject, name), ( # nosec: B101
64
+ f"AttributePath pointing to attribute '{name}'"
65
+ f" which is not available in subject '{type(subject).__name__}'"
66
+ )
67
+
68
+ resolved: Any = getattr(subject, name)
69
+
70
+ assert isinstance(resolved, attribute_origin), ( # nosec: B101
71
+ f"AttributePath pointing to unexpected value of"
72
+ f" '{type(resolved).__name__}' instead of '{attribute.__name__}' for '{name}'"
73
+ )
74
+ return resolved
75
+
76
+ def assigning(
77
+ subject: Root,
78
+ /,
79
+ value: Attribute,
80
+ ) -> Root:
81
+ assert isinstance(subject, root_origin), ( # nosec: B101
82
+ f"AttributePath used on unexpected subject of"
83
+ f" '{type(subject).__name__}' instead of '{root.__name__}' for '{name}'"
84
+ )
85
+
86
+ assert hasattr(subject, name), ( # nosec: B101
87
+ f"AttributePath pointing to attribute '{name}'"
88
+ f" which is not available in subject '{type(subject).__name__}'"
89
+ )
90
+
91
+ assert isinstance(value, attribute_origin), ( # nosec: B101
92
+ f"AttributePath assigning unexpected value of "
93
+ f"'{type(value).__name__}' instead of '{attribute.__name__}' for '{name}'"
94
+ )
95
+
96
+ updated: Root
97
+ # python 3.13 introduces __replace__, we are already implementing it for our types
98
+ if hasattr(subject, "__replace__"): # can't check full type here
99
+ updated = subject.__replace__(**{name: value}) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType, reportAttributeAccessIssue]
100
+
101
+ else:
102
+ updated = copy(subject)
103
+ setattr(updated, name, value)
104
+
105
+ return updated # pyright: ignore[reportUnknownVariableType]
106
+
107
+ self._access: Callable[[Any], Any] = access
108
+ self._assigning: Callable[[Any, Any], Any] = assigning
109
+ self._name: str = name
110
+
111
+ def path_str(
112
+ self,
113
+ current: str | None = None,
114
+ /,
115
+ ) -> str:
116
+ if current:
117
+ return f"{current}.{self._name}"
118
+
119
+ else:
120
+ return self._name
121
+
122
+ def access(
123
+ self,
124
+ subject: Any,
125
+ /,
126
+ ) -> Any:
127
+ return self._access(subject)
128
+
129
+ def assigning(
130
+ self,
131
+ subject: Any,
132
+ /,
133
+ value: Any,
134
+ ) -> Any:
135
+ return self._assigning(subject, value)
136
+
137
+
138
+ @final
139
+ class SequenceItemAttributePathComponent(AttributePathComponent):
140
+ def __init__[Root: Sequence[Any], Attribute](
141
+ self,
142
+ root: type[Root],
143
+ *,
144
+ attribute: type[Attribute],
145
+ index: int,
146
+ ) -> None:
147
+ root_origin: Any = get_origin(root) or root
148
+ attribute_origin: Any = get_origin(attribute) or attribute
149
+
150
+ def access(
151
+ subject: Root,
152
+ /,
153
+ ) -> Attribute:
154
+ assert isinstance(subject, root_origin), ( # nosec: B101
155
+ f"AttributePathComponent used on unexpected root of "
156
+ f"'{type(root).__name__}' instead of '{root.__name__}' for '{index}'"
157
+ )
158
+
159
+ resolved: Any = subject.__getitem__(index) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
160
+
161
+ assert isinstance(resolved, attribute_origin), ( # nosec: B101
162
+ f"AttributePath pointing to unexpected value of "
163
+ f"'{type(resolved).__name__}' instead of '{attribute.__name__}' for '{index}'"
164
+ )
165
+ return resolved
166
+
167
+ def assigning(
168
+ subject: Root,
169
+ /,
170
+ value: Attribute,
171
+ ) -> Root:
172
+ assert isinstance(subject, root_origin), ( # nosec: B101
173
+ f"AttributePath used on unexpected root of "
174
+ f"'{type(subject).__name__}' instead of '{root.__name__}' for '{index}'"
175
+ )
176
+ assert isinstance(value, attribute_origin), ( # nosec: B101
177
+ f"AttributePath assigning to unexpected value of "
178
+ f"'{type(value).__name__}' instead of '{attribute.__name__}' for '{index}'"
179
+ )
180
+
181
+ temp_list: list[Any] = list(subject) # pyright: ignore[reportUnknownArgumentType]
182
+ temp_list[index] = value
183
+ return subject.__class__(temp_list) # pyright: ignore[reportCallIssue, reportUnknownVariableType, reportUnknownMemberType]
184
+
185
+ self._access: Callable[[Any], Any] = access
186
+ self._assigning: Callable[[Any, Any], Any] = assigning
187
+ self._index: Any = index
188
+
189
+ def path_str(
190
+ self,
191
+ current: str | None = None,
192
+ /,
193
+ ) -> str:
194
+ return f"{current or ''}[{self._index}]"
195
+
196
+ def access(
197
+ self,
198
+ subject: Any,
199
+ /,
200
+ ) -> Any:
201
+ return self._access(subject)
202
+
203
+ def assigning(
204
+ self,
205
+ subject: Any,
206
+ /,
207
+ value: Any,
208
+ ) -> Any:
209
+ return self._assigning(subject, value)
210
+
211
+
212
+ @final
213
+ class MappingItemAttributePathComponent(AttributePathComponent):
214
+ def __init__[Root: Mapping[Any, Any], Attribute](
215
+ self,
216
+ root: type[Root],
217
+ *,
218
+ attribute: type[Attribute],
219
+ key: str | int,
220
+ ) -> None:
221
+ root_origin: Any = get_origin(root) or root
222
+ attribute_origin: Any = get_origin(attribute) or attribute
223
+
224
+ def access(
225
+ subject: Root,
226
+ /,
227
+ ) -> Attribute:
228
+ assert isinstance(subject, root_origin), ( # nosec: B101
229
+ f"AttributePathComponent used on unexpected root of "
230
+ f"'{type(root).__name__}' instead of '{root.__name__}' for '{key}'"
231
+ )
232
+
233
+ resolved: Any = subject.__getitem__(key) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
234
+
235
+ assert isinstance(resolved, attribute_origin), ( # nosec: B101
236
+ f"AttributePath pointing to unexpected value of "
237
+ f"'{type(resolved).__name__}' instead of '{attribute.__name__}' for '{key}'"
238
+ )
239
+ return resolved
240
+
241
+ def assigning(
242
+ subject: Root,
243
+ /,
244
+ value: Attribute,
245
+ ) -> Root:
246
+ assert isinstance(subject, root_origin), ( # nosec: B101
247
+ f"AttributePath used on unexpected root of "
248
+ f"'{type(subject).__name__}' instead of '{root.__name__}' for '{key}'"
249
+ )
250
+ assert isinstance(value, attribute_origin), ( # nosec: B101
251
+ f"AttributePath assigning to unexpected value of "
252
+ f"'{type(value).__name__}' instead of '{attribute.__name__}' for '{key}'"
253
+ )
254
+
255
+ temp_dict: dict[Any, Any] = dict(subject) # pyright: ignore[reportUnknownArgumentType]
256
+ temp_dict[key] = value
257
+ return subject.__class__(temp_dict) # pyright: ignore[reportCallIssue, reportUnknownVariableType, reportUnknownMemberType]
258
+
259
+ self._access: Callable[[Any], Any] = access
260
+ self._assigning: Callable[[Any, Any], Any] = assigning
261
+ self._key: Any = key
262
+
263
+ def path_str(
264
+ self,
265
+ current: str | None = None,
266
+ /,
267
+ ) -> str:
268
+ return f"{current or ''}[{self._key}]"
269
+
270
+ def access(
271
+ self,
272
+ subject: Any,
273
+ /,
274
+ ) -> Any:
275
+ return self._access(subject)
276
+
277
+ def assigning(
278
+ self,
279
+ subject: Any,
280
+ /,
281
+ value: Any,
282
+ ) -> Any:
283
+ return self._assigning(subject, value)
284
+
285
+
286
+ @final
287
+ class AttributePath[Root, Attribute]:
288
+ @overload
289
+ def __init__(
290
+ self,
291
+ root: type[Root],
292
+ /,
293
+ *,
294
+ attribute: type[Root],
295
+ ) -> None: ...
296
+
297
+ @overload
298
+ def __init__(
299
+ self,
300
+ root: type[Root],
301
+ /,
302
+ *components: AttributePathComponent,
303
+ attribute: type[Attribute],
304
+ ) -> None: ...
305
+
306
+ def __init__(
307
+ self,
308
+ root: type[Root],
309
+ /,
310
+ *components: AttributePathComponent,
311
+ attribute: type[Attribute],
312
+ ) -> None:
313
+ assert components or root == attribute # nosec: B101
314
+ self.__root__: type[Root] = root
315
+ self.__attribute__: type[Attribute] = attribute
316
+ self.__components__: tuple[AttributePathComponent, ...] = components
317
+
318
+ @property
319
+ def components(self) -> Sequence[str]:
320
+ return tuple(component.path_str() for component in self.__components__)
321
+
322
+ def __str__(self) -> str:
323
+ path: str = ""
324
+ for component in self.__components__:
325
+ path = component.path_str(path)
326
+
327
+ return path
328
+
329
+ def __repr__(self) -> str:
330
+ path: str = self.__root__.__name__
331
+ for component in self.__components__:
332
+ path = component.path_str(path)
333
+
334
+ return path
335
+
336
+ def __getattr__(
337
+ self,
338
+ name: str,
339
+ ) -> Any:
340
+ try:
341
+ return object.__getattribute__(self, name)
342
+
343
+ except (AttributeError, KeyError):
344
+ pass # continue
345
+
346
+ assert not name.startswith( # nosec: B101
347
+ "_"
348
+ ), f"Accessing private/special attribute paths ({name}) is forbidden"
349
+
350
+ try:
351
+ annotation: Any = self.__attribute__.__annotations__[name]
352
+
353
+ except Exception as exc:
354
+ raise AttributeError(
355
+ f"Failed to prepare AttributePath caused by inaccessible"
356
+ f" type annotation for '{name}' within '{self.__attribute__.__name__}'"
357
+ ) from exc
358
+
359
+ return AttributePath[Root, Any](
360
+ self.__root__,
361
+ *(
362
+ *self.__components__,
363
+ PropertyAttributePathComponent(
364
+ root=self.__attribute__,
365
+ attribute=annotation,
366
+ name=name,
367
+ ),
368
+ ),
369
+ attribute=annotation,
370
+ )
371
+
372
+ def __getitem__(
373
+ self,
374
+ key: str | int,
375
+ ) -> Any:
376
+ match get_origin(self.__attribute__) or self.__attribute__:
377
+ case collections_abc.Mapping | typing.Mapping | builtins.dict:
378
+ match get_args(self.__attribute__):
379
+ case (builtins.str | builtins.int, element_annotation):
380
+ return AttributePath[Root, Any](
381
+ self.__root__,
382
+ *(
383
+ *self.__components__,
384
+ MappingItemAttributePathComponent(
385
+ root=self.__attribute__, # pyright: ignore[reportArgumentType]
386
+ attribute=element_annotation,
387
+ key=key,
388
+ ),
389
+ ),
390
+ attribute=element_annotation,
391
+ )
392
+
393
+ case other:
394
+ raise TypeError(
395
+ "Unsupported Mapping type annotation",
396
+ self.__attribute__.__name__,
397
+ )
398
+
399
+ case builtins.tuple:
400
+ if not isinstance(key, int):
401
+ raise TypeError(
402
+ "Unsupported tuple type annotation",
403
+ self.__attribute__.__name__,
404
+ )
405
+
406
+ match get_args(self.__attribute__):
407
+ case (element_annotation, builtins.Ellipsis | types.EllipsisType):
408
+ return AttributePath[Root, Any](
409
+ self.__root__,
410
+ *(
411
+ *self.__components__,
412
+ SequenceItemAttributePathComponent(
413
+ root=self.__attribute__, # pyright: ignore[reportArgumentType]
414
+ attribute=element_annotation,
415
+ index=key,
416
+ ),
417
+ ),
418
+ attribute=element_annotation,
419
+ )
420
+
421
+ case other:
422
+ return AttributePath[Root, Any](
423
+ self.__root__,
424
+ *(
425
+ *self.__components__,
426
+ SequenceItemAttributePathComponent(
427
+ root=self.__attribute__, # pyright: ignore[reportArgumentType]
428
+ attribute=other[key],
429
+ index=key,
430
+ ),
431
+ ),
432
+ attribute=other[key],
433
+ )
434
+
435
+ case collections_abc.Sequence | typing.Sequence | builtins.list:
436
+ if not isinstance(key, int):
437
+ raise TypeError(
438
+ "Unsupported Sequence type annotation",
439
+ self.__attribute__.__name__,
440
+ )
441
+
442
+ match get_args(self.__attribute__):
443
+ case (element_annotation,):
444
+ return AttributePath[Root, Any](
445
+ self.__root__,
446
+ *(
447
+ *self.__components__,
448
+ SequenceItemAttributePathComponent(
449
+ root=self.__attribute__, # pyright: ignore[reportArgumentType]
450
+ attribute=element_annotation,
451
+ index=key,
452
+ ),
453
+ ),
454
+ attribute=element_annotation,
455
+ )
456
+
457
+ case other:
458
+ raise TypeError(
459
+ "Unsupported Seqence type annotation",
460
+ self.__attribute__.__name__,
461
+ )
462
+
463
+ case other:
464
+ raise TypeError("Unsupported type annotation", other)
465
+
466
+ @overload
467
+ def __call__(
468
+ self,
469
+ root: Root,
470
+ /,
471
+ ) -> Attribute: ...
472
+
473
+ @overload
474
+ def __call__(
475
+ self,
476
+ root: Root,
477
+ /,
478
+ updated: Attribute,
479
+ ) -> Root: ...
480
+
481
+ def __call__(
482
+ self,
483
+ root: Root,
484
+ /,
485
+ updated: Attribute | Missing = MISSING,
486
+ ) -> Root | Attribute:
487
+ assert isinstance(root, get_origin(self.__root__) or self.__root__), ( # nosec: B101
488
+ f"AttributePath '{self.__repr__()}' used on unexpected root of "
489
+ f"'{type(root).__name__}' instead of '{self.__root__.__name__}'"
490
+ )
491
+
492
+ if not_missing(updated):
493
+ assert isinstance(updated, get_origin(self.__attribute__) or self.__attribute__), ( # nosec: B101
494
+ f"AttributePath '{self.__repr__()}' assigning to unexpected value of "
495
+ f"'{type(updated).__name__}' instead of '{self.__attribute__.__name__}'"
496
+ )
497
+
498
+ resolved: Any = root
499
+ updates_stack: deque[tuple[Any, AttributePathComponent]] = deque()
500
+ for component in self.__components__:
501
+ updates_stack.append((resolved, component))
502
+ resolved = component.access(resolved)
503
+
504
+ updated_value: Any = updated
505
+ while updates_stack:
506
+ subject, component = updates_stack.pop()
507
+ updated_value = component.assigning(
508
+ subject,
509
+ value=updated_value,
510
+ )
511
+
512
+ return updated_value
513
+
514
+ else:
515
+ resolved: Any = root
516
+ for component in self.__components__:
517
+ resolved = component.access(resolved)
518
+
519
+ assert isinstance(resolved, get_origin(self.__attribute__) or self.__attribute__), ( # nosec: B101
520
+ f"AttributePath '{self.__repr__()}' pointing to unexpected value of "
521
+ f"'{type(resolved).__name__}' instead of '{self.__attribute__.__name__}'"
522
+ )
523
+
524
+ return resolved
@@ -0,0 +1,229 @@
1
+ from collections.abc import Callable, Collection, Iterable
2
+ from typing import Any, Literal, Self, cast, final
3
+
4
+ from haiway.state.path import AttributePath
5
+ from haiway.utils import freeze
6
+
7
+ __all__ = [
8
+ "AttributeRequirement",
9
+ ]
10
+
11
+
12
+ @final
13
+ class AttributeRequirement[Root]:
14
+ @classmethod
15
+ def equal[Parameter](
16
+ cls,
17
+ value: Parameter,
18
+ /,
19
+ path: AttributePath[Root, Parameter] | Parameter,
20
+ ) -> Self:
21
+ assert isinstance( # nosec: B101
22
+ path, AttributePath
23
+ ), "Prepare attribute path by using Self._.path.to.property or explicitly"
24
+
25
+ def check_equal(root: Root) -> None:
26
+ checked: Any = cast(AttributePath[Root, Parameter], path)(root)
27
+ if checked != value:
28
+ raise ValueError(f"{checked} is not equal {value} for '{path.__repr__()}'")
29
+
30
+ return cls(
31
+ path,
32
+ "equal",
33
+ value,
34
+ check=check_equal,
35
+ )
36
+
37
+ @classmethod
38
+ def not_equal[Parameter](
39
+ cls,
40
+ value: Parameter,
41
+ /,
42
+ path: AttributePath[Root, Parameter] | Parameter,
43
+ ) -> Self:
44
+ assert isinstance( # nosec: B101
45
+ path, AttributePath
46
+ ), "Prepare attribute path by using Self._.path.to.property or explicitly"
47
+
48
+ def check_not_equal(root: Root) -> None:
49
+ checked: Any = cast(AttributePath[Root, Parameter], path)(root)
50
+ if checked == value:
51
+ raise ValueError(f"{checked} is equal {value} for '{path.__repr__()}'")
52
+
53
+ return cls(
54
+ path,
55
+ "not_equal",
56
+ value,
57
+ check=check_not_equal,
58
+ )
59
+
60
+ @classmethod
61
+ def contains[Parameter](
62
+ cls,
63
+ value: Parameter,
64
+ /,
65
+ path: AttributePath[
66
+ Root,
67
+ Collection[Parameter] | tuple[Parameter, ...] | list[Parameter] | set[Parameter],
68
+ ]
69
+ | Collection[Parameter]
70
+ | tuple[Parameter, ...]
71
+ | list[Parameter]
72
+ | set[Parameter],
73
+ ) -> Self:
74
+ assert isinstance( # nosec: B101
75
+ path, AttributePath
76
+ ), "Prepare attribute path by using Self._.path.to.property or explicitly"
77
+
78
+ def check_contains(root: Root) -> None:
79
+ checked: Any = cast(AttributePath[Root, Parameter], path)(root)
80
+ if value not in checked:
81
+ raise ValueError(f"{checked} does not contain {value} for '{path.__repr__()}'")
82
+
83
+ return cls(
84
+ path,
85
+ "contains",
86
+ value,
87
+ check=check_contains,
88
+ )
89
+
90
+ @classmethod
91
+ def contains_any[Parameter](
92
+ cls,
93
+ value: Collection[Parameter],
94
+ /,
95
+ path: AttributePath[
96
+ Root,
97
+ Collection[Parameter] | tuple[Parameter, ...] | list[Parameter] | set[Parameter],
98
+ ]
99
+ | Collection[Parameter]
100
+ | tuple[Parameter, ...]
101
+ | list[Parameter]
102
+ | set[Parameter],
103
+ ) -> Self:
104
+ assert isinstance( # nosec: B101
105
+ path, AttributePath
106
+ ), "Prepare attribute path by using Self._.path.to.property or explicitly"
107
+
108
+ def check_contains_any(root: Root) -> None:
109
+ checked: Any = cast(AttributePath[Root, Parameter], path)(root)
110
+ if any(element in checked for element in value):
111
+ raise ValueError(
112
+ f"{checked} does not contain any of {value} for '{path.__repr__()}'"
113
+ )
114
+
115
+ return cls(
116
+ path,
117
+ "contains_any",
118
+ value,
119
+ check=check_contains_any,
120
+ )
121
+
122
+ @classmethod
123
+ def contained_in[Parameter](
124
+ cls,
125
+ value: Collection[Parameter],
126
+ /,
127
+ path: AttributePath[Root, Parameter] | Parameter,
128
+ ) -> Self:
129
+ assert isinstance( # nosec: B101
130
+ path, AttributePath
131
+ ), "Prepare attribute path by using Self._.path.to.property or explicitly"
132
+
133
+ def check_contained_in(root: Root) -> None:
134
+ checked: Any = cast(AttributePath[Root, Parameter], path)(root)
135
+ if checked not in value:
136
+ raise ValueError(f"{value} does not contain {checked} for '{path.__repr__()}'")
137
+
138
+ return cls(
139
+ value,
140
+ "contained_in",
141
+ path,
142
+ check=check_contained_in,
143
+ )
144
+
145
+ def __init__(
146
+ self,
147
+ lhs: Any,
148
+ operator: Literal[
149
+ "equal",
150
+ "not_equal",
151
+ "contains",
152
+ "contains_any",
153
+ "contained_in",
154
+ "and",
155
+ "or",
156
+ ],
157
+ rhs: Any,
158
+ check: Callable[[Root], None],
159
+ ) -> None:
160
+ self.lhs: Any = lhs
161
+ self.operator: Literal[
162
+ "equal",
163
+ "not_equal",
164
+ "contains",
165
+ "contains_any",
166
+ "contained_in",
167
+ "and",
168
+ "or",
169
+ ] = operator
170
+ self.rhs: Any = rhs
171
+ self._check: Callable[[Root], None] = check
172
+
173
+ freeze(self)
174
+
175
+ def __and__(
176
+ self,
177
+ other: Self,
178
+ ) -> Self:
179
+ def check_and(root: Root) -> None:
180
+ self.check(root)
181
+ other.check(root)
182
+
183
+ return self.__class__(
184
+ self,
185
+ "and",
186
+ other,
187
+ check=check_and,
188
+ )
189
+
190
+ def __or__(
191
+ self,
192
+ other: Self,
193
+ ) -> Self:
194
+ def check_or(root: Root) -> None:
195
+ try:
196
+ self.check(root)
197
+ except ValueError:
198
+ other.check(root)
199
+
200
+ return self.__class__(
201
+ self,
202
+ "or",
203
+ other,
204
+ check=check_or,
205
+ )
206
+
207
+ def check(
208
+ self,
209
+ root: Root,
210
+ /,
211
+ *,
212
+ raise_exception: bool = True,
213
+ ) -> bool:
214
+ try:
215
+ self._check(root)
216
+ return True
217
+
218
+ except Exception as exc:
219
+ if raise_exception:
220
+ raise exc
221
+
222
+ else:
223
+ return False
224
+
225
+ def filter(
226
+ self,
227
+ values: Iterable[Root],
228
+ ) -> list[Root]:
229
+ return [value for value in values if self.check(value, raise_exception=False)]
haiway/state/structure.py CHANGED
@@ -15,6 +15,7 @@ from typing import (
15
15
  from weakref import WeakValueDictionary
16
16
 
17
17
  from haiway.state.attributes import AttributeAnnotation, attribute_annotations
18
+ from haiway.state.path import AttributePath
18
19
  from haiway.state.validation import attribute_validator
19
20
  from haiway.types import MISSING, Missing, not_missing
20
21
 
@@ -86,6 +87,7 @@ class StateMeta(type):
86
87
  state_type.__ATTRIBUTES__ = attributes # pyright: ignore[reportAttributeAccessIssue]
87
88
  state_type.__slots__ = frozenset(attributes.keys()) # pyright: ignore[reportAttributeAccessIssue]
88
89
  state_type.__match_args__ = state_type.__slots__ # pyright: ignore[reportAttributeAccessIssue]
90
+ state_type._ = AttributePath(state_type, attribute=state_type) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
89
91
 
90
92
  return state_type
91
93
 
@@ -104,6 +106,7 @@ class State(metaclass=StateMeta):
104
106
  Base class for immutable data structures.
105
107
  """
106
108
 
109
+ _: ClassVar[Self]
107
110
  __IMMUTABLE__: ClassVar[EllipsisType] = ...
108
111
  __ATTRIBUTES__: ClassVar[dict[str, StateAttribute[Any]]]
109
112
 
@@ -178,6 +181,18 @@ class State(metaclass=StateMeta):
178
181
  ),
179
182
  )
180
183
 
184
+ def updating[Value](
185
+ self,
186
+ path: AttributePath[Self, Value] | Value,
187
+ /,
188
+ value: Value,
189
+ ) -> Self:
190
+ assert isinstance( # nosec: B101
191
+ path, AttributePath
192
+ ), "Prepare parameter path by using Self._.path.to.property or explicitly"
193
+
194
+ return cast(AttributePath[Self, Value], path)(self, updated=value)
195
+
181
196
  def updated(
182
197
  self,
183
198
  **kwargs: Any,
@@ -5,6 +5,9 @@ from pathlib import Path
5
5
  from re import Pattern
6
6
  from types import MappingProxyType, NoneType, UnionType
7
7
  from typing import Any, Literal, Protocol, Union
8
+ from typing import Mapping as MappingType # noqa: UP035
9
+ from typing import Sequence as SequenceType # noqa: UP035
10
+ from typing import Sequence as SetType # noqa: UP035
8
11
  from uuid import UUID
9
12
 
10
13
  from haiway.state.attributes import AttributeAnnotation
@@ -128,7 +131,7 @@ def _prepare_validator_of_set(
128
131
  def validator(
129
132
  value: Any,
130
133
  ) -> Any:
131
- if isinstance(value, Set):
134
+ if isinstance(value, set):
132
135
  return frozenset(element_validator(element) for element in value) # pyright: ignore[reportUnknownVariableType]
133
136
 
134
137
  else:
@@ -171,7 +174,7 @@ def _prepare_validator_of_mapping(
171
174
  match value:
172
175
  case {**elements}:
173
176
  return MappingProxyType(
174
- {key_validator(key): value_validator(value) for key, value in elements}
177
+ {key_validator(key): value_validator(value) for key, value in elements.items()}
175
178
  )
176
179
 
177
180
  case _:
@@ -291,9 +294,13 @@ VALIDATORS: Mapping[Any, Callable[[AttributeAnnotation], Callable[[Any], Any]]]
291
294
  tuple: _prepare_validator_of_tuple,
292
295
  frozenset: _prepare_validator_of_set,
293
296
  Literal: _prepare_validator_of_literal,
297
+ set: _prepare_validator_of_set,
294
298
  Set: _prepare_validator_of_set,
299
+ SetType: _prepare_validator_of_set,
295
300
  Sequence: _prepare_validator_of_sequence,
301
+ SequenceType: _prepare_validator_of_sequence,
296
302
  Mapping: _prepare_validator_of_mapping,
303
+ MappingType: _prepare_validator_of_mapping,
297
304
  range: _prepare_validator_of_type,
298
305
  UUID: _prepare_validator_of_type,
299
306
  date: _prepare_validator_of_type,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: haiway
3
- Version: 0.6.2
3
+ Version: 0.6.4
4
4
  Summary: Framework for dependency injection and state management within structured concurrency model.
5
5
  Maintainer-email: Kacper Kaliński <kacper.kalinski@miquido.com>
6
6
  License: MIT License
@@ -1,4 +1,4 @@
1
- haiway/__init__.py,sha256=QksN9EkQrmsYuOhkrKNYr-RMLDT7hixuplkLmJntIfc,1301
1
+ haiway/__init__.py,sha256=GslXcxpiKSKmcWlsvj25X7uDZzuUDUUL8pxAKFqbvIs,1387
2
2
  haiway/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  haiway/context/__init__.py,sha256=ZgQoQFUqfPDqeIbhS898C3dP02QzOCRmClVQpHaPTBA,336
4
4
  haiway/context/access.py,sha256=CB-F9Yd6EAoJIqzMQGid9szww7aFiti5z6x0T79LV7k,14098
@@ -14,10 +14,12 @@ haiway/helpers/retries.py,sha256=gIkyUlqJLDYaxIZd3qzeqGFY9y5Gp8dgZLlZ6hs8hoc,753
14
14
  haiway/helpers/throttling.py,sha256=zo0OwFq64si5KUwhd58cFHLmGAmYwRbFRJMbv9suhPs,3844
15
15
  haiway/helpers/timeouted.py,sha256=1xU09hQnFdj6p48BwZl5xUvtIr3zC0ZUXehkdrduCjs,3074
16
16
  haiway/helpers/tracing.py,sha256=eQpkIoGSB51jRF8RcLaihvHX3VzJIRdyRxTx3I14Pzg,3346
17
- haiway/state/__init__.py,sha256=nPVHBySLuOdIL1VSIs-mo64s53xYHIbieELhw8mQgyc,204
18
- haiway/state/attributes.py,sha256=gS4sEp50bDLAb3Y477BvagobvgMekUkiZZ64ZX6Avac,13613
19
- haiway/state/structure.py,sha256=r8q2d3ro3C0kKYrdx9IE-bY2mKMVRwYC7d5Oeazj83Y,7289
20
- haiway/state/validation.py,sha256=dC4m3a8HfC7c3slBpFbyYfIRnzl0QrmUSUb6MokAajY,8737
17
+ haiway/state/__init__.py,sha256=emTuwGFn7HyjyTJ_ass69J5jQIA7_WHO4teZz_dR05Y,355
18
+ haiway/state/attributes.py,sha256=OVUHp0_OwDwqJa-4Rk_diQhIpBYg0PW9APU3p_UTjd0,13635
19
+ haiway/state/path.py,sha256=4vh-fYQv8_xRWjS0ErMQslKDWRI6-KVECAr8JhYk0UY,17503
20
+ haiway/state/requirement.py,sha256=3iQqzp5Q7w6y5uClamJGH7S5Hib9pciuTAV27PP5lS8,6161
21
+ haiway/state/structure.py,sha256=_1K_RSqA20ufcMay6i2CthMkgJeF1gZi7Hr4rUiCJs0,7869
22
+ haiway/state/validation.py,sha256=l3NHHYVEr1M9F3HrV4hXA1gCibp7Uj5_nZWXMuy2tsU,9089
21
23
  haiway/types/__init__.py,sha256=00Ulp2BxcIWm9vWXKQPodpFEwE8hpqj6OYgrNxelp5s,252
22
24
  haiway/types/frozen.py,sha256=CZhFCXnWAKEhuWSfILxA8smfdpMd5Ku694ycfLh98R8,76
23
25
  haiway/types/missing.py,sha256=JiXo5xdi7H-PbIJr0fuK5wpOuQZhjrDYUkMlfIFcsaE,1705
@@ -29,8 +31,8 @@ haiway/utils/logs.py,sha256=oDsc1ZdqKDjlTlctLbDcp9iX98Acr-1tdw-Pyg3DElo,1577
29
31
  haiway/utils/mimic.py,sha256=BkVjTVP2TxxC8GChPGyDV6UXVwJmiRiSWeOYZNZFHxs,1828
30
32
  haiway/utils/noop.py,sha256=qgbZlOKWY6_23Zs43OLukK2HagIQKRyR04zrFVm5rWI,344
31
33
  haiway/utils/queue.py,sha256=oQ3GXCJ-PGNtMEr6EPdgqAvYZoj8lAa7Z2drBKBEoBM,2345
32
- haiway-0.6.2.dist-info/LICENSE,sha256=GehQEW_I1pkmxkkj3NEa7rCTQKYBn7vTPabpDYJlRuo,1063
33
- haiway-0.6.2.dist-info/METADATA,sha256=pHxz4V97cT3so3DTs5fNhVrKjP7L2_63UsPX6GK7aSk,3898
34
- haiway-0.6.2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
35
- haiway-0.6.2.dist-info/top_level.txt,sha256=_LdXVLzUzgkvAGQnQJj5kQfoFhpPW6EF4Kj9NapniLg,7
36
- haiway-0.6.2.dist-info/RECORD,,
34
+ haiway-0.6.4.dist-info/LICENSE,sha256=GehQEW_I1pkmxkkj3NEa7rCTQKYBn7vTPabpDYJlRuo,1063
35
+ haiway-0.6.4.dist-info/METADATA,sha256=Vx_AwXB2HZvP8BIN3EdA9l4gk7YdeILRy72qZ3yPgf8,3898
36
+ haiway-0.6.4.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
37
+ haiway-0.6.4.dist-info/top_level.txt,sha256=_LdXVLzUzgkvAGQnQJj5kQfoFhpPW6EF4Kj9NapniLg,7
38
+ haiway-0.6.4.dist-info/RECORD,,
File without changes