statica 1.0.2__tar.gz → 1.2.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.2.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
@@ -31,7 +31,7 @@ Classifier: Operating System :: OS Independent
31
31
  Classifier: Intended Audience :: Developers
32
32
  Classifier: Programming Language :: Python
33
33
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
34
- Requires-Python: <3.14,>=3.11
34
+ Requires-Python: <3.14,>=3.10
35
35
  Description-Content-Type: text/markdown
36
36
 
37
37
  # Statica
@@ -44,13 +44,13 @@ Statica is a Python library for defining and validating structured data with typ
44
44
 
45
45
  ## Why Statica?
46
46
 
47
- Statica was created to address the need for a lightweight, flexible, and dependency-free alternative to libraries like pydantic.
47
+ Statica was created to address the need for a lightweight, flexible, and dependency-free alternative to libraries like pydantic.
48
48
  While pydantic is a powerful tool for data validation and parsing, Statica offers some distinct advantages in specific situations:
49
49
 
50
50
  1. **Lightweight**: Statica does not rely on any third-party dependencies, making it ideal for projects where minimizing external dependencies is a priority.
51
- 2. **Customizable Validation**: Statica allows fine-grained control over type and constraint validation through customizable field descriptors (`Field`) and error classes.
51
+ 2. **Performance**: For use cases where performance is critical. Pydantic needs `3x` more memory than Statica for the same models.
52
52
  3. **Ease of Use**: With its simple, Pythonic design, Statica is intuitive for developers already familiar with Python's `dataclasses` and type hinting. It avoids much of the magic that pydantic employs.
53
- 4. **Performance**: For use cases where performance, especially memory usage, is critical, Statica avoids some of the overhead introduced by the advanced features of pydantic.
53
+ 4. **Customizable Validation**: Statica allows fine-grained control over type and constraint validation through customizable field descriptors (`Field`) and error classes.
54
54
 
55
55
  ## Features
56
56
 
@@ -8,13 +8,13 @@ Statica is a Python library for defining and validating structured data with typ
8
8
 
9
9
  ## Why Statica?
10
10
 
11
- Statica was created to address the need for a lightweight, flexible, and dependency-free alternative to libraries like pydantic.
11
+ Statica was created to address the need for a lightweight, flexible, and dependency-free alternative to libraries like pydantic.
12
12
  While pydantic is a powerful tool for data validation and parsing, Statica offers some distinct advantages in specific situations:
13
13
 
14
14
  1. **Lightweight**: Statica does not rely on any third-party dependencies, making it ideal for projects where minimizing external dependencies is a priority.
15
- 2. **Customizable Validation**: Statica allows fine-grained control over type and constraint validation through customizable field descriptors (`Field`) and error classes.
15
+ 2. **Performance**: For use cases where performance is critical. Pydantic needs `3x` more memory than Statica for the same models.
16
16
  3. **Ease of Use**: With its simple, Pythonic design, Statica is intuitive for developers already familiar with Python's `dataclasses` and type hinting. It avoids much of the magic that pydantic employs.
17
- 4. **Performance**: For use cases where performance, especially memory usage, is critical, Statica avoids some of the overhead introduced by the advanced features of pydantic.
17
+ 4. **Customizable Validation**: Statica allows fine-grained control over type and constraint validation through customizable field descriptors (`Field`) and error classes.
18
18
 
19
19
  ## Features
20
20
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "statica"
3
- version = "1.0.2"
3
+ version = "1.2.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" }]
@@ -13,7 +13,7 @@ classifiers=[
13
13
  "Programming Language :: Python",
14
14
  "Topic :: Software Development :: Libraries :: Python Modules"
15
15
  ]
16
- requires-python = ">=3.11, <3.14"
16
+ requires-python = ">=3.10, <3.14"
17
17
 
18
18
 
19
19
  ########################################################################################
@@ -26,6 +26,7 @@ dependencies = []
26
26
  [dependency-groups]
27
27
  dev = [
28
28
  "mypy ~= 1.16.0",
29
+ "pyinstrument>=5.0.2",
29
30
  "pytest-cov ~= 6.1.0",
30
31
  "ruff ~= 0.11.0",
31
32
  ]
@@ -59,18 +60,10 @@ ignore = [
59
60
  "ANN202", # Missing return type annotation for private function
60
61
  "ANN401", # Dynamically typed expressions (typing.Any) are disallowed
61
62
  "D", # docstrings
62
- "E501", # line length
63
63
  "ERA001", # Found commented-out code
64
- "FBT001", # Boolean-typed positional argument in function definition
65
- "FIX002", # Line contains TODO, consider resolving the issue
66
64
  "INP001", # File is part of an implicit namespace package. Add an `__init__.py`.
67
65
  "S101", # Use of `assert` detected
68
66
  "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
67
  "W191", # indentation contains tabs
75
68
  "PLR0913", # Too many arguments in function definition (6 > 5)
76
69
  ]
@@ -85,6 +78,9 @@ quote-style = "double"
85
78
  #### MARK: Testing
86
79
 
87
80
  [tool.coverage.report]
81
+ omit = [
82
+ "benchmark_*.py",
83
+ ]
88
84
  exclude_also = [
89
85
  "if TYPE_CHECKING:",
90
86
  'if __name__ == "__main__":',
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from dataclasses import field as dataclass_field
5
- from types import UnionType
6
5
  from typing import (
7
6
  TYPE_CHECKING,
8
7
  Any,
@@ -11,13 +10,12 @@ from typing import (
11
10
  TypeVar,
12
11
  cast,
13
12
  dataclass_transform,
14
- get_args,
15
- get_origin,
16
13
  get_type_hints,
17
14
  overload,
18
15
  )
19
16
 
20
17
  from statica.exceptions import ConstraintValidationError, TypeValidationError
18
+ from statica.validators import validate_constraints, validate_type
21
19
 
22
20
  if TYPE_CHECKING:
23
21
  from collections.abc import Callable, Mapping
@@ -25,95 +23,6 @@ if TYPE_CHECKING:
25
23
  T = TypeVar("T")
26
24
 
27
25
 
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
26
  ########################################################################################
118
27
  #### MARK: Field descriptor
119
28
 
@@ -135,10 +44,14 @@ class FieldDescriptor(Generic[T]):
135
44
  strip_whitespace: bool | None = None
136
45
  cast_to: Callable[..., T] | None = None
137
46
 
47
+ alias: str | None = None
48
+ alias_for_parsing: str | None = None
49
+ alias_for_serialization: str | None = None
50
+
138
51
  def __set_name__(self, owner: Any, name: str) -> None:
139
52
  self.name = name
140
53
  self.owner = owner
141
- self.expected_type = _get_expected_type(owner, name)
54
+ self.expected_type = owner.__annotations__.get(name)
142
55
 
143
56
  @overload
144
57
  def __get__(self, instance: None, owner: Any) -> FieldDescriptor[T]: ...
@@ -159,45 +72,55 @@ class FieldDescriptor(Generic[T]):
159
72
  if self.cast_to is not None:
160
73
  value = self.cast_to(value)
161
74
 
162
- _validate_type(value, self.expected_type)
163
-
164
- if value is not None:
165
- value = self._validate_constraints(value)
75
+ validate_type(value, self.expected_type)
76
+
77
+ if (
78
+ self.min_length is not None
79
+ or self.max_length is not None
80
+ or self.min_value is not None
81
+ or self.max_value is not None
82
+ or self.strip_whitespace is not None
83
+ ):
84
+ value = validate_constraints(
85
+ value,
86
+ min_length=self.min_length,
87
+ max_length=self.max_length,
88
+ min_value=self.min_value,
89
+ max_value=self.max_value,
90
+ strip_whitespace=self.strip_whitespace,
91
+ )
166
92
 
167
93
  except TypeValidationError as e:
168
94
  msg = f"{self.name}: {e!s}"
169
95
  error_class = getattr(self.owner, "type_error_class", TypeValidationError)
170
96
  raise error_class(msg) from e
97
+
171
98
  except ConstraintValidationError as e:
172
99
  msg = f"{self.name}: {e!s}"
173
100
  error_class = getattr(self.owner, "constraint_error_class", ConstraintValidationError)
174
101
  raise error_class(msg) from e
102
+
175
103
  except ValueError as e:
176
104
  msg = f"{self.name}: {e!s}"
177
105
  raise TypeValidationError(msg) from e
106
+
178
107
  return value
179
108
 
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
109
 
200
- return value
110
+ def get_field_descriptors(cls: type[Statica]) -> list[FieldDescriptor]:
111
+ """
112
+ Get all Field descriptors for a class.
113
+ Returns a list of FieldDescriptor instances.
114
+ """
115
+
116
+ descriptors = []
117
+
118
+ for field_name in cls.__annotations__:
119
+ field_descriptor = getattr(cls, field_name)
120
+ assert isinstance(field_descriptor, FieldDescriptor)
121
+ descriptors.append(field_descriptor)
122
+
123
+ return descriptors
201
124
 
202
125
 
203
126
  ########################################################################################
@@ -212,6 +135,9 @@ def Field( # noqa: N802
212
135
  max_value: float | None = None,
213
136
  strip_whitespace: bool | None = None,
214
137
  cast_to: Callable[..., T] | None = None,
138
+ alias: str | None = None,
139
+ alias_for_parsing: str | None = None,
140
+ alias_for_serialization: str | None = None,
215
141
  ) -> Any:
216
142
  """
217
143
  Type-safe field function that returns the correct type for type checkers
@@ -225,6 +151,9 @@ def Field( # noqa: N802
225
151
  max_value=max_value,
226
152
  strip_whitespace=strip_whitespace,
227
153
  cast_to=cast_to,
154
+ alias=alias,
155
+ alias_for_parsing=alias_for_parsing,
156
+ alias_for_serialization=alias_for_serialization,
228
157
  )
229
158
 
230
159
  if TYPE_CHECKING:
@@ -248,8 +177,6 @@ class StaticaMeta(type):
248
177
  already, but only for subclasses of Statica.
249
178
  """
250
179
 
251
- print(f"__new__({name=}, {bases=}, {namespace=})")
252
-
253
180
  if name == "Statica":
254
181
  return super().__new__(cls, name, bases, namespace)
255
182
 
@@ -257,20 +184,15 @@ class StaticaMeta(type):
257
184
 
258
185
  # Generate custom __init__ method
259
186
 
260
- def custom_init(self: Statica, **kwargs: Any) -> None:
187
+ def statica_init(self: Statica, **kwargs: Any) -> None:
261
188
  for field_name, field_type in annotations.items():
262
- # If it is union type with none continuation, skip it
263
- if get_origin(field_type) is UnionType and type(None) in get_args(field_type):
264
- if field_name not in kwargs:
265
- setattr(self, field_name, None)
266
- continue
267
-
268
- if field_name not in kwargs:
189
+ if field_name not in kwargs and not isinstance(None, field_type):
269
190
  msg = f"Missing required field: {field_name}"
270
191
  raise TypeValidationError(msg)
271
- setattr(self, field_name, kwargs[field_name])
272
192
 
273
- namespace["__init__"] = custom_init
193
+ setattr(self, field_name, kwargs.get(field_name))
194
+
195
+ namespace["__init__"] = statica_init
274
196
 
275
197
  # Set up Field descriptors for type-hinted attributes
276
198
 
@@ -296,15 +218,36 @@ class StaticaMeta(type):
296
218
  class Statica(metaclass=StaticaMeta):
297
219
  @classmethod
298
220
  def from_map(cls, mapping: Mapping[str, Any]) -> Self:
299
- instance = cls(**mapping)
221
+ mapping_key_to_field_keys = {} # Maps alias to field name
222
+
223
+ for field_descriptor in get_field_descriptors(cls):
224
+ # Use alias for parsing if it exists
225
+ alias = field_descriptor.alias_for_parsing or field_descriptor.alias or field_descriptor.name
226
+ mapping_key_to_field_keys[alias] = field_descriptor.name
227
+
228
+ parsed_mapping = {mapping_key_to_field_keys[k]: v for k, v in mapping.items()}
229
+
230
+ instance = cls(**parsed_mapping)
300
231
 
301
232
  # Go through type hints and set values
302
233
  for attribute_name in get_type_hints(instance.__class__):
303
- value = mapping.get(attribute_name)
234
+ value = parsed_mapping.get(attribute_name)
304
235
  setattr(instance, attribute_name, value) # Descriptor __set__ validates
305
236
 
306
237
  return instance
307
238
 
239
+ def to_dict(self) -> dict[str, Any]:
240
+ """
241
+ Convert the instance to a dictionary, using the field names as keys.
242
+ """
243
+ result = {}
244
+ for field_descriptor in get_field_descriptors(self.__class__):
245
+ # Use alias for serialization if it exists
246
+ alias = field_descriptor.alias_for_serialization or field_descriptor.alias or field_descriptor.name
247
+ result[alias] = getattr(self, field_descriptor.name)
248
+
249
+ return result
250
+
308
251
 
309
252
  ########################################################################################
310
253
  #### MARK: Main
@@ -317,13 +260,13 @@ if __name__ == "__main__":
317
260
  name: str = Field(min_length=3, max_length=50, strip_whitespace=True)
318
261
  description: str | None = Field(max_length=200)
319
262
  num: int | float
320
- float_num: float | None
263
+ float_num: float | None = Field(alias="floatNum")
321
264
 
322
265
  data = {
323
266
  "name": "Test Payload",
324
267
  "description": "ddf",
325
268
  "num": 5,
326
- "float_num": 5.5,
269
+ "floatNum": 5.5,
327
270
  }
328
271
 
329
272
  payload = Payload.from_map(data)
@@ -334,10 +277,3 @@ if __name__ == "__main__":
334
277
  num=42,
335
278
  float_num=3.14,
336
279
  )
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,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
+
8
+
9
+ def validate_type(value: Any, expected_type: type | UnionType) -> None:
10
+ """
11
+ Validate that the value matches the expected type.
12
+ Throws TypeValidationError if the type does not match.
13
+
14
+ Examples:
15
+ .. code-block:: python
16
+ _validate_type(1, int) # No exception
17
+ _validate_type("abc", str) # No exception
18
+ _validate_type(1, str) # Raises TypeValidationError
19
+ _validate_type(None, int | None) # No exception
20
+ _validate_type(None, int) # Raises TypeValidationError
21
+ """
22
+ if isinstance(value, expected_type):
23
+ return
24
+
25
+ expected_type_str = str(expected_type) if type(expected_type) is UnionType else expected_type.__name__
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.2.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
@@ -31,7 +31,7 @@ Classifier: Operating System :: OS Independent
31
31
  Classifier: Intended Audience :: Developers
32
32
  Classifier: Programming Language :: Python
33
33
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
34
- Requires-Python: <3.14,>=3.11
34
+ Requires-Python: <3.14,>=3.10
35
35
  Description-Content-Type: text/markdown
36
36
 
37
37
  # Statica
@@ -44,13 +44,13 @@ Statica is a Python library for defining and validating structured data with typ
44
44
 
45
45
  ## Why Statica?
46
46
 
47
- Statica was created to address the need for a lightweight, flexible, and dependency-free alternative to libraries like pydantic.
47
+ Statica was created to address the need for a lightweight, flexible, and dependency-free alternative to libraries like pydantic.
48
48
  While pydantic is a powerful tool for data validation and parsing, Statica offers some distinct advantages in specific situations:
49
49
 
50
50
  1. **Lightweight**: Statica does not rely on any third-party dependencies, making it ideal for projects where minimizing external dependencies is a priority.
51
- 2. **Customizable Validation**: Statica allows fine-grained control over type and constraint validation through customizable field descriptors (`Field`) and error classes.
51
+ 2. **Performance**: For use cases where performance is critical. Pydantic needs `3x` more memory than Statica for the same models.
52
52
  3. **Ease of Use**: With its simple, Pythonic design, Statica is intuitive for developers already familiar with Python's `dataclasses` and type hinting. It avoids much of the magic that pydantic employs.
53
- 4. **Performance**: For use cases where performance, especially memory usage, is critical, Statica avoids some of the overhead introduced by the advanced features of pydantic.
53
+ 4. **Customizable Validation**: Statica allows fine-grained control over type and constraint validation through customizable field descriptors (`Field`) and error classes.
54
54
 
55
55
  ## Features
56
56
 
@@ -4,8 +4,11 @@ pyproject.toml
4
4
  statica/__init__.py
5
5
  statica/core.py
6
6
  statica/exceptions.py
7
+ statica/validators.py
7
8
  statica.egg-info/PKG-INFO
8
9
  statica.egg-info/SOURCES.txt
9
10
  statica.egg-info/dependency_links.txt
10
11
  statica.egg-info/top_level.txt
11
- tests/test_statica.py
12
+ tests/test_aliasing.py
13
+ tests/test_statica.py
14
+ 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,63 +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)
48
-
49
-
50
- def test_get_expected_type() -> None:
51
- class TestClass(Statica):
52
- required_field: str
53
- optional_field: str | None
54
- required_with_field: str = Field()
55
- optional_with_field: str | None = Field()
56
-
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
3
+ from statica.core import ConstraintValidationError, Field, Statica, TypeValidationError
61
4
 
62
5
 
63
6
  def test_basic_syntax() -> None:
@@ -231,7 +174,3 @@ def test_cast() -> None:
231
174
 
232
175
  with pytest.raises(TypeValidationError):
233
176
  IntTest.from_map({"num": "5.5"})
234
-
235
-
236
- if __name__ == "__main__":
237
- pytest.main([__file__])
@@ -0,0 +1,24 @@
1
+ import pytest
2
+
3
+ from statica.core import TypeValidationError
4
+ from statica.validators import validate_type
5
+
6
+
7
+ def test_validate_type() -> None:
8
+ # No exceptions should be raised for valid types
9
+
10
+ validate_type(1, int)
11
+ validate_type("abc", str)
12
+ validate_type(None, int | None)
13
+ validate_type(1, int | None)
14
+
15
+ # Exceptions should be raised for invalid types
16
+
17
+ with pytest.raises(TypeValidationError):
18
+ validate_type(1, str)
19
+ with pytest.raises(TypeValidationError):
20
+ validate_type("abc", int)
21
+ with pytest.raises(TypeValidationError):
22
+ validate_type(None, int)
23
+ with pytest.raises(TypeValidationError):
24
+ validate_type("abc", int | None)
File without changes
File without changes
File without changes
File without changes