1
1
"""
2
2
The backus naur grammar for types is as follows:
3
- T ::= Statica | int | float | str | None | (T1 | T2) | list[T] | set[T] | dict[T1, T2]
3
+ T ::= Statica
4
+ | int
5
+ | float
6
+ | str
7
+ | None
8
+ | (T1 | T2)
9
+ | list[T]
10
+ | set[T]
11
+ | dict[T1, T2]
12
+ | Literal[V1, ...]
13
+
4
14
5
15
Where:
6
16
- Statica: A class that inherits from Statica
16
26
from __future__ import annotations
17
27
18
28
from types import GenericAlias , UnionType
19
- from typing import Any
29
+ from typing import Any , Literal , TypeGuard , Union
20
30
21
31
from statica .config import StaticaConfig , default_config
22
32
from statica .exceptions import ConstraintValidationError , TypeValidationError
23
33
34
+ ########################################################################################
35
+ #### MARK: Types
36
+
37
+
38
+ class LiteralGenericAlias :
39
+ """A type used in place of typing._LiteralGenericAlias to avoid private imports."""
40
+
41
+ __origin__ = Literal
42
+ __args__ : tuple [Any , ...]
43
+
44
+
45
+ def is_literal_generic_alias (expected_type : Any ) -> TypeGuard [LiteralGenericAlias ]:
46
+ return hasattr (expected_type , "__origin__" ) and expected_type .__origin__ is Literal
47
+
48
+
49
+ class UnionGenericAlias :
50
+ """A type used in place of typing._UnionGenericAlias to avoid private imports."""
51
+
52
+ __origin__ = Union
53
+ __args__ : tuple [Any , ...]
54
+
55
+
56
+ def is_union_generic_alias (expected_type : Any ) -> TypeGuard [UnionGenericAlias ]:
57
+ return hasattr (expected_type , "__origin__" ) and expected_type .__origin__ is Union
58
+
59
+
24
60
########################################################################################
25
61
#### MARK: Type Validation
26
62
@@ -35,16 +71,28 @@ def validate_or_raise(
35
71
are already initialized Statica objects.
36
72
"""
37
73
38
- # Handle union types
74
+ # Handle generic aliases if native python types, e.g. list[int], dict[str, int]
39
75
40
- if isinstance (expected_type , UnionType ):
41
- validate_type_union (value , expected_type , config )
76
+ if isinstance (expected_type , GenericAlias ):
77
+ validate_type_generic_alias (value , expected_type , config )
42
78
return
43
79
44
- # Handle generic aliases
80
+ # Handle parameterized generic types
45
81
46
- if isinstance (expected_type , GenericAlias ):
47
- validate_type_generic_alias (value , expected_type , config )
82
+ if is_union_generic_alias (expected_type ):
83
+ validate_type_union_generic_alias (value , expected_type , config )
84
+ return
85
+
86
+ # Handle Literal (e.g. Literal["a", "b"], with any number and type of values)
87
+
88
+ if is_literal_generic_alias (expected_type ):
89
+ validate_literal (value , expected_type )
90
+ return
91
+
92
+ # Handle union types
93
+
94
+ if isinstance (expected_type , UnionType ):
95
+ validate_type_union (value , expected_type , config )
48
96
return
49
97
50
98
# Handle all other types
@@ -59,6 +107,19 @@ def validate_or_raise(
59
107
raise TypeValidationError (msg )
60
108
61
109
110
+ def validate_literal (
111
+ value : Any ,
112
+ expected_type : LiteralGenericAlias ,
113
+ ) -> None :
114
+ """
115
+ Validate that the value matches one of the literals in the expected_type.
116
+ Throws TypeValidationError if the value is not one of the literals.
117
+ """
118
+ if value not in expected_type .__args__ :
119
+ msg = f"expected one of { expected_type .__args__ } , got '{ value } '"
120
+ raise TypeValidationError (msg )
121
+
122
+
62
123
def validate_type_union (
63
124
value : Any ,
64
125
expected_type : UnionType ,
@@ -83,6 +144,30 @@ def validate_type_union(
83
144
raise TypeValidationError (msg )
84
145
85
146
147
+ def validate_type_union_generic_alias (
148
+ value : Any ,
149
+ expected_type : UnionGenericAlias ,
150
+ config : StaticaConfig = default_config ,
151
+ ) -> None :
152
+ """
153
+ Validate that the value matches one of the types in the UnionGenericAlias.
154
+ Throws TypeValidationError if the type does not match any of the union types.
155
+ """
156
+ for sub_type in expected_type .__args__ :
157
+ try :
158
+ validate_or_raise (value , sub_type , config )
159
+ except TypeValidationError :
160
+ continue # Try the next sub-type
161
+ else :
162
+ return # Exit if one of the sub-types matches
163
+
164
+ msg = config .type_error_message .format (
165
+ expected_type = expected_type .__args__ ,
166
+ found_type = type (value ).__name__ ,
167
+ )
168
+ raise TypeValidationError (msg )
169
+
170
+
86
171
def validate_type_generic_alias (
87
172
value : Any ,
88
173
expected_type : GenericAlias ,
0 commit comments