statica 1.0.2__tar.gz → 1.1.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statica
3
- Version: 1.0.2
3
+ Version: 1.1.0
4
4
  Summary: A minimalistic data validation library
5
5
  Author-email: Marcel Kröker <kroeker.marcel@gmail.com>
6
6
  License: MIT License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "statica"
3
- version = "1.0.2"
3
+ version = "1.1.0"
4
4
  description = "A minimalistic data validation library"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Marcel Kröker", email = "kroeker.marcel@gmail.com" }]
@@ -59,18 +59,10 @@ ignore = [
59
59
  "ANN202", # Missing return type annotation for private function
60
60
  "ANN401", # Dynamically typed expressions (typing.Any) are disallowed
61
61
  "D", # docstrings
62
- "E501", # line length
63
62
  "ERA001", # Found commented-out code
64
- "FBT001", # Boolean-typed positional argument in function definition
65
- "FIX002", # Line contains TODO, consider resolving the issue
66
63
  "INP001", # File is part of an implicit namespace package. Add an `__init__.py`.
67
64
  "S101", # Use of `assert` detected
68
65
  "T201", # `print` found
69
- "TD002", # Missing author in TODO; try: `# TODO(<author_name>): ...`
70
- "TD003", # Missing issue link on the line following this TODO
71
- "UP006", # use Dict (not possible in python 3.8)
72
- "UP007", # use X | Y for union (not possible in python 3.8)
73
- "UP035", # use typing Dict and List (not possible in python 3.8)
74
66
  "W191", # indentation contains tabs
75
67
  "PLR0913", # Too many arguments in function definition (6 > 5)
76
68
  ]
@@ -18,6 +18,8 @@ from typing import (
18
18
  )
19
19
 
20
20
  from statica.exceptions import ConstraintValidationError, TypeValidationError
21
+ from statica.types_utils import get_expected_type
22
+ from statica.validators import validate_constraints, validate_type
21
23
 
22
24
  if TYPE_CHECKING:
23
25
  from collections.abc import Callable, Mapping
@@ -25,95 +27,6 @@ if TYPE_CHECKING:
25
27
  T = TypeVar("T")
26
28
 
27
29
 
28
- ########################################################################################
29
- #### MARK: Internal functions
30
-
31
-
32
- def _type_allows_none(expected_type: Any) -> bool:
33
- """
34
- Check if the expected type allows None.
35
-
36
- Examples:
37
- .. code-block:: python
38
- _allows_none(int | None) # True
39
- _allows_none(int) # False
40
- """
41
- if isinstance(expected_type, UnionType):
42
- return type(None) in get_args(expected_type)
43
- return expected_type is type(None) or expected_type is Any
44
-
45
-
46
- def _value_matches_type(value: Any | None, expected_type: Any) -> bool:
47
- """
48
- Check if the value matches the expected type.
49
- Handles basic types, Union types, and generic types.
50
-
51
- Examples:
52
- .. code-block:: python
53
- _value_matches_type(1, int) # True
54
- _value_matches_type(None, int | None) # True
55
- _value_matches_type(None, int) # False
56
- """
57
- # If expected_type is e.g. int | None, pass if value is None
58
- if _type_allows_none(expected_type) and value is None:
59
- return True
60
-
61
- # Basic types like int, str, etc.
62
- if (origin := get_origin(expected_type)) is None:
63
- return isinstance(value, expected_type)
64
-
65
- # Handle Union types
66
- if origin is UnionType or origin is type(None) or origin is Any:
67
- types = get_args(expected_type)
68
- return any(_value_matches_type(value, t) for t in types if t is not type(None))
69
-
70
- # Handle generic types
71
- return isinstance(value, origin)
72
-
73
-
74
- def _validate_type(value: Any, expected_type: type | UnionType) -> None:
75
- """
76
- Validate that the value matches the expected type.
77
- Throws TypeValidationError if the type does not match.
78
-
79
- Examples:
80
- .. code-block:: python
81
- _validate_type(1, int) # No exception
82
- _validate_type("abc", str) # No exception
83
- _validate_type(1, str) # Raises TypeValidationError
84
- _validate_type(None, int | None) # No exception
85
- _validate_type(None, int) # Raises TypeValidationError
86
- """
87
- if not _value_matches_type(value, expected_type):
88
- expected_type_str = str(expected_type) if type(expected_type) is UnionType else expected_type.__name__
89
-
90
- msg = f"expected type '{expected_type_str}', got '{type(value).__name__}'"
91
- raise TypeValidationError(msg)
92
-
93
-
94
- def _get_expected_type(cls: type, attr_name: str) -> Any:
95
- """
96
- Get the expected type for a class attribute.
97
- Handles type hints and Field descriptors.
98
-
99
- Examples:
100
- .. code-block:: python
101
- class MyClass(Statica):
102
- age: int | None
103
- name: str = Field()
104
-
105
- _get_expected_type(MyClass, "age") # int | None
106
- _get_expected_type(MyClass, "name") # str
107
- """
108
-
109
- expected_type = get_type_hints(cls).get(attr_name, Any)
110
-
111
- if get_origin(expected_type) is FieldDescriptor:
112
- return get_args(expected_type)[0] # Get type from generic
113
-
114
- return expected_type
115
-
116
-
117
30
  ########################################################################################
118
31
  #### MARK: Field descriptor
119
32
 
@@ -135,10 +48,14 @@ class FieldDescriptor(Generic[T]):
135
48
  strip_whitespace: bool | None = None
136
49
  cast_to: Callable[..., T] | None = None
137
50
 
51
+ alias: str | None = None
52
+ alias_for_parsing: str | None = None
53
+ alias_for_serialization: str | None = None
54
+
138
55
  def __set_name__(self, owner: Any, name: str) -> None:
139
56
  self.name = name
140
57
  self.owner = owner
141
- self.expected_type = _get_expected_type(owner, name)
58
+ self.expected_type = get_expected_type(owner, name)
142
59
 
143
60
  @overload
144
61
  def __get__(self, instance: None, owner: Any) -> FieldDescriptor[T]: ...
@@ -159,45 +76,49 @@ class FieldDescriptor(Generic[T]):
159
76
  if self.cast_to is not None:
160
77
  value = self.cast_to(value)
161
78
 
162
- _validate_type(value, self.expected_type)
79
+ validate_type(value, self.expected_type)
163
80
 
164
81
  if value is not None:
165
- value = self._validate_constraints(value)
82
+ value = validate_constraints(
83
+ value,
84
+ min_length=self.min_length,
85
+ max_length=self.max_length,
86
+ min_value=self.min_value,
87
+ max_value=self.max_value,
88
+ strip_whitespace=self.strip_whitespace,
89
+ )
166
90
 
167
91
  except TypeValidationError as e:
168
92
  msg = f"{self.name}: {e!s}"
169
93
  error_class = getattr(self.owner, "type_error_class", TypeValidationError)
170
94
  raise error_class(msg) from e
95
+
171
96
  except ConstraintValidationError as e:
172
97
  msg = f"{self.name}: {e!s}"
173
98
  error_class = getattr(self.owner, "constraint_error_class", ConstraintValidationError)
174
99
  raise error_class(msg) from e
100
+
175
101
  except ValueError as e:
176
102
  msg = f"{self.name}: {e!s}"
177
103
  raise TypeValidationError(msg) from e
104
+
178
105
  return value
179
106
 
180
- def _validate_constraints(self, value: Any) -> Any:
181
- if self.strip_whitespace and isinstance(value, str):
182
- value = value.strip()
183
-
184
- if isinstance(value, str | list | tuple | dict):
185
- if self.min_length is not None and len(value) < self.min_length:
186
- msg = f"length must be at least {self.min_length}"
187
- raise ConstraintValidationError(msg)
188
- if self.max_length is not None and len(value) > self.max_length:
189
- msg = f"length must be at most {self.max_length}"
190
- raise ConstraintValidationError(msg)
191
-
192
- if isinstance(value, int | float):
193
- if self.min_value is not None and value < self.min_value:
194
- msg = f"must be at least {self.min_value}"
195
- raise ConstraintValidationError(msg)
196
- if self.max_value is not None and value > self.max_value:
197
- msg = f"must be at most {self.max_value}"
198
- raise ConstraintValidationError(msg)
199
107
 
200
- return value
108
+ def get_field_descriptors(cls: type[Statica]) -> list[FieldDescriptor]:
109
+ """
110
+ Get all Field descriptors for a class.
111
+ Returns a list of FieldDescriptor instances.
112
+ """
113
+
114
+ descriptors = []
115
+
116
+ for field_name in cls.__annotations__:
117
+ field_descriptor = getattr(cls, field_name)
118
+ assert isinstance(field_descriptor, FieldDescriptor)
119
+ descriptors.append(field_descriptor)
120
+
121
+ return descriptors
201
122
 
202
123
 
203
124
  ########################################################################################
@@ -212,6 +133,9 @@ def Field( # noqa: N802
212
133
  max_value: float | None = None,
213
134
  strip_whitespace: bool | None = None,
214
135
  cast_to: Callable[..., T] | None = None,
136
+ alias: str | None = None,
137
+ alias_for_parsing: str | None = None,
138
+ alias_for_serialization: str | None = None,
215
139
  ) -> Any:
216
140
  """
217
141
  Type-safe field function that returns the correct type for type checkers
@@ -225,6 +149,9 @@ def Field( # noqa: N802
225
149
  max_value=max_value,
226
150
  strip_whitespace=strip_whitespace,
227
151
  cast_to=cast_to,
152
+ alias=alias,
153
+ alias_for_parsing=alias_for_parsing,
154
+ alias_for_serialization=alias_for_serialization,
228
155
  )
229
156
 
230
157
  if TYPE_CHECKING:
@@ -248,8 +175,6 @@ class StaticaMeta(type):
248
175
  already, but only for subclasses of Statica.
249
176
  """
250
177
 
251
- print(f"__new__({name=}, {bases=}, {namespace=})")
252
-
253
178
  if name == "Statica":
254
179
  return super().__new__(cls, name, bases, namespace)
255
180
 
@@ -296,15 +221,36 @@ class StaticaMeta(type):
296
221
  class Statica(metaclass=StaticaMeta):
297
222
  @classmethod
298
223
  def from_map(cls, mapping: Mapping[str, Any]) -> Self:
299
- instance = cls(**mapping)
224
+ mapping_key_to_field_keys = {} # Maps alias to field name
225
+
226
+ for field_descriptor in get_field_descriptors(cls):
227
+ # Use alias for parsing if it exists
228
+ alias = field_descriptor.alias_for_parsing or field_descriptor.alias or field_descriptor.name
229
+ mapping_key_to_field_keys[alias] = field_descriptor.name
230
+
231
+ parsed_mapping = {mapping_key_to_field_keys[k]: v for k, v in mapping.items()}
232
+
233
+ instance = cls(**parsed_mapping)
300
234
 
301
235
  # Go through type hints and set values
302
236
  for attribute_name in get_type_hints(instance.__class__):
303
- value = mapping.get(attribute_name)
237
+ value = parsed_mapping.get(attribute_name)
304
238
  setattr(instance, attribute_name, value) # Descriptor __set__ validates
305
239
 
306
240
  return instance
307
241
 
242
+ def to_dict(self) -> dict[str, Any]:
243
+ """
244
+ Convert the instance to a dictionary, using the field names as keys.
245
+ """
246
+ result = {}
247
+ for field_descriptor in get_field_descriptors(self.__class__):
248
+ # Use alias for serialization if it exists
249
+ alias = field_descriptor.alias_for_serialization or field_descriptor.alias or field_descriptor.name
250
+ result[alias] = getattr(self, field_descriptor.name)
251
+
252
+ return result
253
+
308
254
 
309
255
  ########################################################################################
310
256
  #### MARK: Main
@@ -317,13 +263,13 @@ if __name__ == "__main__":
317
263
  name: str = Field(min_length=3, max_length=50, strip_whitespace=True)
318
264
  description: str | None = Field(max_length=200)
319
265
  num: int | float
320
- float_num: float | None
266
+ float_num: float | None = Field(alias="floatNum")
321
267
 
322
268
  data = {
323
269
  "name": "Test Payload",
324
270
  "description": "ddf",
325
271
  "num": 5,
326
- "float_num": 5.5,
272
+ "floatNum": 5.5,
327
273
  }
328
274
 
329
275
  payload = Payload.from_map(data)
@@ -334,10 +280,3 @@ if __name__ == "__main__":
334
280
  num=42,
335
281
  float_num=3.14,
336
282
  )
337
-
338
- class ShortSyntax(Statica):
339
- name: str = Field(min_length=3, max_length=5, strip_whitespace=True)
340
-
341
- print("Testing ShortSyntax...")
342
- short = ShortSyntax(name="test")
343
- print(f"short.name = {short.name}")
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ from types import UnionType
4
+ from typing import Any, get_args, get_origin, get_type_hints
5
+
6
+
7
+ def type_allows_none(expected_type: Any) -> bool:
8
+ """
9
+ Check if the expected type allows None.
10
+
11
+ Examples:
12
+ .. code-block:: python
13
+ _allows_none(int | None) # True
14
+ _allows_none(int) # False
15
+ """
16
+ if isinstance(expected_type, UnionType):
17
+ return type(None) in get_args(expected_type)
18
+ return expected_type is type(None) or expected_type is Any
19
+
20
+
21
+ def value_matches_type(value: Any | None, expected_type: Any) -> bool:
22
+ """
23
+ Check if the value matches the expected type.
24
+ Handles basic types, Union types, and generic types.
25
+
26
+ Examples:
27
+ .. code-block:: python
28
+ _value_matches_type(1, int) # True
29
+ _value_matches_type(None, int | None) # True
30
+ _value_matches_type(None, int) # False
31
+ """
32
+ # If expected_type is e.g. int | None, pass if value is None
33
+ if type_allows_none(expected_type) and value is None:
34
+ return True
35
+
36
+ # Basic types like int, str, etc.
37
+ if (origin := get_origin(expected_type)) is None:
38
+ return isinstance(value, expected_type)
39
+
40
+ # Handle Union types
41
+ if origin is UnionType or origin is type(None) or origin is Any:
42
+ types = get_args(expected_type)
43
+ return any(value_matches_type(value, t) for t in types if t is not type(None))
44
+
45
+ # Handle generic types
46
+ return isinstance(value, origin)
47
+
48
+
49
+ def get_expected_type(cls: type, attr_name: str) -> Any:
50
+ """
51
+ Get the expected type for a class attribute.
52
+ Handles type hints and Field descriptors.
53
+
54
+ Examples:
55
+ .. code-block:: python
56
+ class MyClass(Statica):
57
+ age: int | None
58
+ name: str = Field()
59
+
60
+ _get_expected_type(MyClass, "age") # int | None
61
+ _get_expected_type(MyClass, "name") # str
62
+ """
63
+
64
+ return get_type_hints(cls).get(attr_name, Any)
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ from types import UnionType
4
+ from typing import Any
5
+
6
+ from statica.exceptions import ConstraintValidationError, TypeValidationError
7
+ from statica.types_utils import value_matches_type
8
+
9
+
10
+ def validate_type(value: Any, expected_type: type | UnionType) -> None:
11
+ """
12
+ Validate that the value matches the expected type.
13
+ Throws TypeValidationError if the type does not match.
14
+
15
+ Examples:
16
+ .. code-block:: python
17
+ _validate_type(1, int) # No exception
18
+ _validate_type("abc", str) # No exception
19
+ _validate_type(1, str) # Raises TypeValidationError
20
+ _validate_type(None, int | None) # No exception
21
+ _validate_type(None, int) # Raises TypeValidationError
22
+ """
23
+ if not value_matches_type(value, expected_type):
24
+ expected_type_str = str(expected_type) if type(expected_type) is UnionType else expected_type.__name__
25
+
26
+ msg = f"expected type '{expected_type_str}', got '{type(value).__name__}'"
27
+ raise TypeValidationError(msg)
28
+
29
+
30
+ def validate_constraints(
31
+ value: Any,
32
+ min_length: int | None = None,
33
+ max_length: int | None = None,
34
+ min_value: float | None = None,
35
+ max_value: float | None = None,
36
+ strip_whitespace: bool | None = None,
37
+ ) -> Any:
38
+ """
39
+ If the value is a string, strip the whitespace if `strip_whitespace` is True.
40
+
41
+ If the value is a string, list, tuple, or dict, check its length against
42
+ the `min_length` and `max_length` constraints.
43
+
44
+ If the value is an int or float, check its value against the `min_value`
45
+ and `max_value` constraints.
46
+
47
+ Throws ConstraintValidationError if any constraints are violated.
48
+ """
49
+
50
+ if strip_whitespace and isinstance(value, str):
51
+ value = value.strip()
52
+
53
+ if isinstance(value, str | list | tuple | dict):
54
+ if min_length is not None and len(value) < min_length:
55
+ msg = f"length must be at least {min_length}"
56
+ raise ConstraintValidationError(msg)
57
+ if max_length is not None and len(value) > max_length:
58
+ msg = f"length must be at most {max_length}"
59
+ raise ConstraintValidationError(msg)
60
+
61
+ if isinstance(value, int | float):
62
+ if min_value is not None and value < min_value:
63
+ msg = f"must be at least {min_value}"
64
+ raise ConstraintValidationError(msg)
65
+ if max_value is not None and value > max_value:
66
+ msg = f"must be at most {max_value}"
67
+ raise ConstraintValidationError(msg)
68
+
69
+ return value
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statica
3
- Version: 1.0.2
3
+ Version: 1.1.0
4
4
  Summary: A minimalistic data validation library
5
5
  Author-email: Marcel Kröker <kroeker.marcel@gmail.com>
6
6
  License: MIT License
@@ -4,8 +4,13 @@ pyproject.toml
4
4
  statica/__init__.py
5
5
  statica/core.py
6
6
  statica/exceptions.py
7
+ statica/types_utils.py
8
+ statica/validators.py
7
9
  statica.egg-info/PKG-INFO
8
10
  statica.egg-info/SOURCES.txt
9
11
  statica.egg-info/dependency_links.txt
10
12
  statica.egg-info/top_level.txt
11
- tests/test_statica.py
13
+ tests/test_aliasing.py
14
+ tests/test_statica.py
15
+ tests/test_types_utils.py
16
+ tests/test_validators.py
@@ -0,0 +1,163 @@
1
+ import pytest
2
+
3
+ from statica.core import Field, Statica, TypeValidationError
4
+ from statica.exceptions import ConstraintValidationError
5
+
6
+ INTEGER = 42
7
+ FLOAT = 19.99
8
+ STRING = "test string"
9
+
10
+
11
+ def test_basic_alias() -> None:
12
+ class AliasTest(Statica):
13
+ full_name: str = Field(alias="fullName")
14
+ age: int = Field(alias="userAge")
15
+
16
+ # Test parsing with alias
17
+ data = {"fullName": "John Doe", "userAge": INTEGER}
18
+ instance = AliasTest.from_map(data)
19
+ assert instance.full_name == "John Doe"
20
+ assert instance.age == INTEGER
21
+ assert instance.to_dict() == data
22
+
23
+ # Test that original field names don't work when alias is used
24
+ with pytest.raises(KeyError):
25
+ AliasTest.from_map({"full_name": "John Doe", "age": INTEGER})
26
+
27
+
28
+ def test_alias_for_parsing() -> None:
29
+ """Test alias_for_parsing specifically."""
30
+
31
+ class ParseAliasTest(Statica):
32
+ user_name: str = Field(alias_for_parsing="userName")
33
+ user_id: int = Field(alias_for_parsing="userId")
34
+
35
+ # Test parsing with parsing alias
36
+ instance = ParseAliasTest.from_map({"userName": "alice", "userId": INTEGER})
37
+ assert instance.user_name == "alice"
38
+ assert instance.user_id == INTEGER
39
+
40
+
41
+ def test_alias_priority() -> None:
42
+ """Test that alias_for_parsing takes priority over general alias."""
43
+
44
+ class PriorityTest(Statica):
45
+ field_name: str = Field(alias="generalAlias", alias_for_parsing="parseAlias")
46
+
47
+ # Should use alias_for_parsing
48
+ instance = PriorityTest.from_map({"parseAlias": STRING})
49
+ assert instance.field_name == STRING
50
+
51
+ # General alias should not work when alias_for_parsing exists
52
+ with pytest.raises(KeyError):
53
+ PriorityTest.from_map({"generalAlias": STRING})
54
+
55
+
56
+ def test_alias_with_optional_fields() -> None:
57
+ """Test aliases with optional fields."""
58
+
59
+ class OptionalAliasTest(Statica):
60
+ required_field: str = Field(alias="reqField")
61
+ optional_field: str | None = Field(alias="optField")
62
+
63
+ # Test with both fields provided
64
+ instance = OptionalAliasTest.from_map({"reqField": "required", "optField": "optional"})
65
+ assert instance.required_field == "required"
66
+ assert instance.optional_field == "optional"
67
+
68
+ # Test with only required field
69
+ instance = OptionalAliasTest.from_map({"reqField": "required"})
70
+ assert instance.required_field == "required"
71
+ assert instance.optional_field is None
72
+
73
+ # Test missing required field should still raise error
74
+ with pytest.raises(TypeValidationError):
75
+ OptionalAliasTest.from_map({"optField": "optional"})
76
+
77
+
78
+ def test_alias_with_constraints() -> None:
79
+ """Test that aliases work with field constraints."""
80
+
81
+ class ConstraintAliasTest(Statica):
82
+ name: str = Field(alias="userName", min_length=3, max_length=10, strip_whitespace=True)
83
+ score: int = Field(alias="userScore", min_value=0, max_value=100)
84
+
85
+ # Test valid values
86
+ instance = ConstraintAliasTest.from_map({"userName": " Alice ", "userScore": INTEGER})
87
+ assert instance.name == "Alice" # Whitespace should be stripped
88
+ assert instance.score == INTEGER
89
+
90
+ # Test constraint violations with aliases
91
+ with pytest.raises(ConstraintValidationError): # Should be ConstraintValidationError
92
+ ConstraintAliasTest.from_map({"userName": "Al", "userScore": INTEGER}) # Too short
93
+
94
+ with pytest.raises(ConstraintValidationError): # Should be ConstraintValidationError
95
+ ConstraintAliasTest.from_map({"userName": "Alice", "userScore": 150}) # Too high
96
+
97
+
98
+ def test_alias_with_casting() -> None:
99
+ """Test that aliases work with type casting."""
100
+
101
+ class CastAliasTest(Statica):
102
+ number: int = Field(alias="numStr", cast_to=int)
103
+ price: float = Field(alias="priceStr", cast_to=float)
104
+
105
+ instance = CastAliasTest.from_map({"numStr": "42", "priceStr": "19.99"})
106
+ assert instance.number == INTEGER
107
+ assert instance.price == FLOAT
108
+
109
+
110
+ def test_mixed_alias_and_no_alias() -> None:
111
+ """Test mixing fields with and without aliases."""
112
+
113
+ class MixedTest(Statica):
114
+ normal_field: str
115
+ aliased_field: str = Field(alias="aliasedField")
116
+ another_normal: int
117
+
118
+ instance = MixedTest.from_map(
119
+ {
120
+ "normal_field": "normal",
121
+ "aliasedField": "aliased",
122
+ "another_normal": INTEGER,
123
+ },
124
+ )
125
+ assert instance.normal_field == "normal"
126
+ assert instance.aliased_field == "aliased"
127
+ assert instance.another_normal == INTEGER
128
+
129
+
130
+ def test_alias_serialization() -> None:
131
+ class SerializationTest(Statica):
132
+ field_name: str = Field(alias_for_serialization="serialAlias")
133
+
134
+ instance = SerializationTest(field_name=STRING)
135
+
136
+ assert instance.to_dict() == {"serialAlias": STRING}
137
+
138
+
139
+ def test_all_alias_types_together() -> None:
140
+ """Test a field with all three alias types."""
141
+
142
+ class AllAliasTest(Statica):
143
+ field_name: str = Field(
144
+ alias="generalAlias",
145
+ alias_for_parsing="parseAlias",
146
+ alias_for_serialization="serializeAlias",
147
+ )
148
+
149
+ # Should use alias_for_parsing for parsing
150
+ instance = AllAliasTest.from_map({"parseAlias": STRING})
151
+ assert instance.field_name == STRING
152
+ assert instance.to_dict() == {"serializeAlias": STRING}
153
+
154
+
155
+ def test_empty_alias_mapping() -> None:
156
+ """Test behavior when no matching aliases are found in the mapping."""
157
+
158
+ class EmptyMappingTest(Statica):
159
+ field_name: str = Field(alias="expectedAlias")
160
+
161
+ # Should raise KeyError when the aliased field is missing
162
+ with pytest.raises(KeyError):
163
+ EmptyMappingTest.from_map({"wrongAlias": "value"})
@@ -1,50 +1,6 @@
1
1
  import pytest
2
2
 
3
- from statica.core import (
4
- ConstraintValidationError,
5
- Field,
6
- Statica,
7
- TypeValidationError,
8
- _get_expected_type,
9
- _type_allows_none,
10
- _validate_type,
11
- _value_matches_type,
12
- )
13
-
14
-
15
- def test_allows_none() -> None:
16
- assert _type_allows_none(type(None))
17
- assert _type_allows_none(int | None)
18
- assert not _type_allows_none(int)
19
-
20
-
21
- def test_value_matches_type() -> None:
22
- assert _value_matches_type(1, int)
23
- assert _value_matches_type("abc", str)
24
- assert not _value_matches_type(1, str)
25
- assert _value_matches_type(None, int | None)
26
- assert _value_matches_type(1, int | None)
27
- assert not _value_matches_type(None, int)
28
- assert _value_matches_type(1, int | None)
29
- assert not _value_matches_type("abc", int | None)
30
-
31
-
32
- def test_validate_type() -> None:
33
- # No exceptions should be raised for valid types
34
- _validate_type(1, int)
35
- _validate_type("abc", str)
36
- _validate_type(None, int | None)
37
- _validate_type(1, int | None)
38
-
39
- # Exceptions should be raised for invalid types
40
- with pytest.raises(TypeValidationError):
41
- _validate_type(1, str)
42
- with pytest.raises(TypeValidationError):
43
- _validate_type("abc", int)
44
- with pytest.raises(TypeValidationError):
45
- _validate_type(None, int)
46
- with pytest.raises(TypeValidationError):
47
- _validate_type("abc", int | None)
3
+ from statica.core import ConstraintValidationError, Field, Statica, TypeValidationError, get_expected_type
48
4
 
49
5
 
50
6
  def test_get_expected_type() -> None:
@@ -54,10 +10,10 @@ def test_get_expected_type() -> None:
54
10
  required_with_field: str = Field()
55
11
  optional_with_field: str | None = Field()
56
12
 
57
- assert _get_expected_type(TestClass, "required_field") is str
58
- assert _get_expected_type(TestClass, "optional_field") == str | None
59
- assert _get_expected_type(TestClass, "required_with_field") is str
60
- assert _get_expected_type(TestClass, "optional_with_field") == str | None
13
+ assert get_expected_type(TestClass, "required_field") is str
14
+ assert get_expected_type(TestClass, "optional_field") == str | None
15
+ assert get_expected_type(TestClass, "required_with_field") is str
16
+ assert get_expected_type(TestClass, "optional_with_field") == str | None
61
17
 
62
18
 
63
19
  def test_basic_syntax() -> None:
@@ -231,7 +187,3 @@ def test_cast() -> None:
231
187
 
232
188
  with pytest.raises(TypeValidationError):
233
189
  IntTest.from_map({"num": "5.5"})
234
-
235
-
236
- if __name__ == "__main__":
237
- pytest.main([__file__])
@@ -0,0 +1,33 @@
1
+ from typing import Generic, TypeVar
2
+
3
+ from statica.types_utils import type_allows_none, value_matches_type
4
+
5
+
6
+ def test_allows_none() -> None:
7
+ assert type_allows_none(type(None))
8
+ assert type_allows_none(int | None)
9
+ assert not type_allows_none(int)
10
+
11
+
12
+ def test_value_matches_type() -> None:
13
+ assert value_matches_type(1, int)
14
+ assert value_matches_type("abc", str)
15
+ assert not value_matches_type(1, str)
16
+ assert value_matches_type(None, int | None)
17
+ assert value_matches_type(1, int | None)
18
+ assert not value_matches_type(None, int)
19
+ assert value_matches_type(1, int | None)
20
+ assert not value_matches_type("abc", int | None)
21
+
22
+ # Test with generics
23
+
24
+ T = TypeVar("T")
25
+
26
+ class GenericTest(Generic[T]):
27
+ value: T
28
+
29
+ def __init__(self, value: T) -> None:
30
+ self.value = value
31
+
32
+ assert value_matches_type(GenericTest[int](1), GenericTest[int])
33
+ assert value_matches_type(GenericTest[int](1), GenericTest[str]) # Is this correct?
@@ -0,0 +1,37 @@
1
+ from typing import Generic, TypeVar
2
+
3
+ import pytest
4
+
5
+ from statica.core import TypeValidationError
6
+ from statica.validators import validate_type
7
+
8
+
9
+ def test_validate_type() -> None:
10
+ # No exceptions should be raised for valid types
11
+
12
+ validate_type(1, int)
13
+ validate_type("abc", str)
14
+ validate_type(None, int | None)
15
+ validate_type(1, int | None)
16
+
17
+ # Exceptions should be raised for invalid types
18
+
19
+ with pytest.raises(TypeValidationError):
20
+ validate_type(1, str)
21
+ with pytest.raises(TypeValidationError):
22
+ validate_type("abc", int)
23
+ with pytest.raises(TypeValidationError):
24
+ validate_type(None, int)
25
+ with pytest.raises(TypeValidationError):
26
+ validate_type("abc", int | None)
27
+
28
+ # Test with generics
29
+
30
+ T = TypeVar("T")
31
+
32
+ class GenericTest(Generic[T]):
33
+ value: T
34
+
35
+ validate_type(GenericTest[int](), GenericTest[int])
36
+
37
+ validate_type(GenericTest[str](), GenericTest[str]) # Is this correct?
File without changes
File without changes
File without changes
File without changes
File without changes