structtype 0.6.0__tar.gz → 0.8.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.
Files changed (57) hide show
  1. {structtype-0.6.0 → structtype-0.8.0}/Makefile +5 -1
  2. {structtype-0.6.0/src/structtype.egg-info → structtype-0.8.0}/PKG-INFO +1 -1
  3. {structtype-0.6.0 → structtype-0.8.0}/src/structtype/__init__.py +15 -1
  4. {structtype-0.6.0 → structtype-0.8.0}/src/structtype/__init__.pyi +81 -141
  5. {structtype-0.6.0 → structtype-0.8.0}/src/structtype/_adapter.py +15 -14
  6. structtype-0.8.0/src/structtype/_config.py +31 -0
  7. {structtype-0.6.0 → structtype-0.8.0}/src/structtype/_core.c +2983 -825
  8. {structtype-0.6.0 → structtype-0.8.0}/src/structtype/_inspect.py +36 -27
  9. {structtype-0.6.0 → structtype-0.8.0}/src/structtype/_json_schema.py +3 -1
  10. {structtype-0.6.0 → structtype-0.8.0}/src/structtype/_utils.py +26 -0
  11. {structtype-0.6.0 → structtype-0.8.0}/src/structtype/_version.py +2 -2
  12. {structtype-0.6.0 → structtype-0.8.0/src/structtype.egg-info}/PKG-INFO +1 -1
  13. {structtype-0.6.0 → structtype-0.8.0}/src/structtype.egg-info/SOURCES.txt +2 -0
  14. {structtype-0.6.0 → structtype-0.8.0}/tests/test_adapter.py +29 -16
  15. structtype-0.8.0/tests/test_annotations.py +1770 -0
  16. {structtype-0.6.0 → structtype-0.8.0}/tests/test_check.py +108 -7
  17. {structtype-0.6.0 → structtype-0.8.0}/tests/test_constraints.py +220 -146
  18. {structtype-0.6.0 → structtype-0.8.0}/tests/test_free_threading.py +30 -0
  19. {structtype-0.6.0 → structtype-0.8.0}/tests/test_inspect.py +25 -29
  20. {structtype-0.6.0 → structtype-0.8.0}/tests/test_json.py +131 -109
  21. {structtype-0.6.0 → structtype-0.8.0}/tests/test_memory.py +3 -1
  22. {structtype-0.6.0 → structtype-0.8.0}/tests/test_schema.py +51 -34
  23. {structtype-0.6.0 → structtype-0.8.0}/tests/test_struct.py +250 -157
  24. {structtype-0.6.0 → structtype-0.8.0}/tests/test_struct_meta.py +175 -39
  25. {structtype-0.6.0 → structtype-0.8.0}/tests/typecheck/api.py +1 -1
  26. {structtype-0.6.0 → structtype-0.8.0}/tests/typecheck/basic.py +11 -4
  27. {structtype-0.6.0 → structtype-0.8.0}/tests/typecheck/fields.py +17 -8
  28. {structtype-0.6.0 → structtype-0.8.0}/tests/typecheck/types.py +5 -3
  29. {structtype-0.6.0 → structtype-0.8.0}/.opencode/opencode.json +0 -0
  30. {structtype-0.6.0 → structtype-0.8.0}/.opencode/plugins/graphify.js +0 -0
  31. {structtype-0.6.0 → structtype-0.8.0}/AGENTS.md +0 -0
  32. {structtype-0.6.0 → structtype-0.8.0}/LICENSE +0 -0
  33. {structtype-0.6.0 → structtype-0.8.0}/MANIFEST.in +0 -0
  34. {structtype-0.6.0 → structtype-0.8.0}/README.md +0 -0
  35. {structtype-0.6.0 → structtype-0.8.0}/pyproject.toml +0 -0
  36. {structtype-0.6.0 → structtype-0.8.0}/setup.cfg +0 -0
  37. {structtype-0.6.0 → structtype-0.8.0}/setup.py +0 -0
  38. {structtype-0.6.0 → structtype-0.8.0}/src/structtype/atof.h +0 -0
  39. {structtype-0.6.0 → structtype-0.8.0}/src/structtype/atof_consts.h +0 -0
  40. {structtype-0.6.0 → structtype-0.8.0}/src/structtype/common.h +0 -0
  41. {structtype-0.6.0 → structtype-0.8.0}/src/structtype/itoa.h +0 -0
  42. {structtype-0.6.0 → structtype-0.8.0}/src/structtype/py.typed +0 -0
  43. {structtype-0.6.0 → structtype-0.8.0}/src/structtype/ryu.h +0 -0
  44. {structtype-0.6.0 → structtype-0.8.0}/src/structtype.egg-info/dependency_links.txt +0 -0
  45. {structtype-0.6.0 → structtype-0.8.0}/src/structtype.egg-info/top_level.txt +0 -0
  46. {structtype-0.6.0 → structtype-0.8.0}/tests/__init__.py +0 -0
  47. {structtype-0.6.0 → structtype-0.8.0}/tests/conftest.py +0 -0
  48. {structtype-0.6.0 → structtype-0.8.0}/tests/test_JSONTestSuite.py +0 -0
  49. {structtype-0.6.0 → structtype-0.8.0}/tests/test_attrs.py +0 -0
  50. {structtype-0.6.0 → structtype-0.8.0}/tests/test_cpylint.py +0 -0
  51. {structtype-0.6.0 → structtype-0.8.0}/tests/test_msgspec.py +0 -0
  52. {structtype-0.6.0 → structtype-0.8.0}/tests/test_pydantic.py +0 -0
  53. {structtype-0.6.0 → structtype-0.8.0}/tests/test_raw.py +0 -0
  54. {structtype-0.6.0 → structtype-0.8.0}/tests/test_typecheck.py +0 -0
  55. {structtype-0.6.0 → structtype-0.8.0}/tests/test_utils.py +0 -0
  56. {structtype-0.6.0 → structtype-0.8.0}/tests/utils.py +0 -0
  57. {structtype-0.6.0 → structtype-0.8.0}/uv.lock +0 -0
@@ -93,7 +93,11 @@ format: ## Format files using ruff format
93
93
  ##@ Benchmark
94
94
  .PHONY: bench
95
95
  bench: ## run benchmarks
96
- uv run benchmarks/bench_libs.py
96
+ uv run -p 3.15 benchmarks/bench_libs.py
97
+
98
+ .PHONY: bench-validators
99
+ bench-validators: ## run Serializer/Validator benchmarks
100
+ uv run -p 3.15 benchmarks/bench_validators.py
97
101
 
98
102
  ##@ Utility
99
103
  .PHONY: clean
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structtype
3
- Version: 0.6.0
3
+ Version: 0.8.0
4
4
  Summary: Fast Struct type with validation and JSON serialization for Python.
5
5
  Maintainer-email: Wolfgang Langner <tds333@mailbox.org>
6
6
  License-Expression: BSD-3-Clause
@@ -1,18 +1,25 @@
1
1
  from ._adapter import StructAdapter
2
+ from ._config import StructConfig
2
3
  from ._core import ( # type: ignore
3
4
  ALL_BUILTIN_TYPES,
4
5
  NODEFAULT,
5
6
  UNSET,
7
+ BytesValidator,
8
+ CollectionValidator,
6
9
  DecodeError,
7
10
  EncodeError,
8
11
  Factory,
9
12
  Field,
13
+ NumericValidator,
10
14
  Raw,
15
+ Serializer,
11
16
  Struct,
12
- StructConfig,
13
17
  StructMeta,
18
+ StrValidator,
19
+ TimezoneValidator,
14
20
  UnsetType,
15
21
  ValidationError,
22
+ Validator,
16
23
  )
17
24
  from ._inspect import FieldInfo, fields
18
25
  from ._json_schema import json_schema, json_schema_components, json_schema_dump
@@ -22,18 +29,25 @@ __all__ = [
22
29
  "ALL_BUILTIN_TYPES",
23
30
  "NODEFAULT",
24
31
  "UNSET",
32
+ "BytesValidator",
33
+ "CollectionValidator",
25
34
  "DecodeError",
26
35
  "EncodeError",
27
36
  "Factory",
28
37
  "Field",
29
38
  "FieldInfo",
39
+ "NumericValidator",
30
40
  "Raw",
41
+ "Serializer",
42
+ "StrValidator",
31
43
  "Struct",
32
44
  "StructAdapter",
33
45
  "StructConfig",
34
46
  "StructMeta",
47
+ "TimezoneValidator",
35
48
  "UnsetType",
36
49
  "ValidationError",
50
+ "Validator",
37
51
  "__version__",
38
52
  "fields",
39
53
  "json_schema",
@@ -8,17 +8,31 @@ from typing import (
8
8
  Final,
9
9
  Literal,
10
10
  TypeAlias,
11
- TypeVar,
11
+ TypedDict,
12
12
  final,
13
13
  overload,
14
14
  )
15
15
 
16
16
  from typing_extensions import Buffer, Self, dataclass_transform
17
17
 
18
- class StructConfig:
18
+ @final
19
+ class UnsetType(enum.Enum):
20
+ UNSET = "UNSET"
21
+ def __bool__(self) -> Literal[False]: ...
22
+
23
+ UNSET: Final = UnsetType.UNSET
24
+
25
+ @final
26
+ class _NoDefault(enum.Enum):
27
+ NODEFAULT = "NODEFAULT"
28
+
29
+ NODEFAULT: Final = _NoDefault.NODEFAULT
30
+
31
+ class StructConfig(TypedDict, total=False):
19
32
  frozen: bool
20
33
  eq: bool
21
34
  order: bool
35
+ kw_only: bool
22
36
  array_like: bool
23
37
  repr_omit_defaults: bool
24
38
  omit_defaults: bool
@@ -27,15 +41,14 @@ class StructConfig:
27
41
  weakref: bool
28
42
  dict: bool
29
43
  cache_hash: bool
30
- tag: str | int | None
44
+ tag: bool | str | int | Callable[[str], str | int] | None
31
45
  tag_field: str | None
32
-
33
- # PEP 673 explicitly rejects using Self in metaclass definitions:
34
- # https://peps.python.org/pep-0673/#valid-locations-for-self
35
- #
36
- # Typeshed works around this by using a type variable as well:
37
- # https://github.com/python/typeshed/blob/17bde1bd5e556de001adde3c2f340ba1c3581bd2/stdlib/abc.pyi#L14-L19
38
- _SM = TypeVar("_SM", bound="StructMeta")
46
+ rename: (
47
+ None
48
+ | Literal["lower", "upper", "camel", "pascal", "kebab"]
49
+ | Callable[[str], str | None]
50
+ | Mapping[str, str]
51
+ )
39
52
 
40
53
  class StructMeta(type):
41
54
  __struct_fields__: ClassVar[tuple[str, ...]]
@@ -46,81 +59,20 @@ class StructMeta(type):
46
59
  def __signature__(self) -> Signature: ...
47
60
  @property
48
61
  def __struct_config__(self) -> StructConfig: ...
49
- def __new__(
50
- mcls: type[_SM],
51
- name: str,
52
- bases: tuple[type, ...],
53
- namespace: dict[str, Any],
54
- /,
55
- *,
56
- tag: bool | str | int | Callable[[str], str | int] | None = None,
57
- tag_field: str | None = None,
58
- rename: (
59
- None
60
- | Literal["lower", "upper", "camel", "pascal", "kebab"]
61
- | Callable[[str], str | None]
62
- | Mapping[str, str]
63
- ) = None,
64
- omit_defaults: bool = False,
65
- forbid_unknown_fields: bool = False,
66
- frozen: bool = False,
67
- eq: bool = True,
68
- order: bool = False,
69
- kw_only: bool = False,
70
- repr_omit_defaults: bool = False,
71
- array_like: bool = False,
72
- weakref: bool = False,
73
- dict: bool = False,
74
- cache_hash: bool = False,
75
- validate_on_init: bool = False,
76
- ) -> _SM: ...
77
-
78
- @final
79
- class UnsetType(enum.Enum):
80
- UNSET = "UNSET"
81
- def __bool__(self) -> Literal[False]: ...
82
-
83
- UNSET: Final = UnsetType.UNSET
84
-
85
- @final
86
- class _NoDefault(enum.Enum):
87
- NODEFAULT = "NODEFAULT"
88
-
89
- NODEFAULT: Final = _NoDefault.NODEFAULT
62
+ @property
63
+ def struct_config(self) -> StructConfig: ...
90
64
 
91
65
  @dataclass_transform(field_specifiers=("Field",)) # type: ignore
92
66
  class Struct(metaclass=StructMeta):
93
67
  __struct_fields__: ClassVar[tuple[str, ...]]
94
68
  __struct_config__: ClassVar[StructConfig]
69
+ struct_config: ClassVar[StructConfig]
95
70
  __struct_alias_fields__: ClassVar[tuple[str, ...]]
96
71
  __struct_defaults__: ClassVar[tuple[Any, ...]]
97
72
  __match_args__: ClassVar[tuple[str, ...]] = ...
98
73
  # A default __init__ so that Structs with unknown field types
99
74
  # won't error on every call to `__init__`
100
75
  def __init__(self, *args: Any, **kwargs: Any) -> None: ...
101
- def __init_subclass__(
102
- cls,
103
- tag: bool | str | int | Callable[[str], str | int] | None = None,
104
- tag_field: str | None = None,
105
- rename: (
106
- None
107
- | Literal["lower", "upper", "camel", "pascal", "kebab"]
108
- | Callable[[str], str | None]
109
- | Mapping[str, str]
110
- ) = None,
111
- omit_defaults: bool = False,
112
- forbid_unknown_fields: bool = False,
113
- frozen: bool = False,
114
- eq: bool = True,
115
- order: bool = False,
116
- kw_only: bool = False,
117
- repr_omit_defaults: bool = False,
118
- array_like: bool = False,
119
- weakref: bool = False,
120
- dict: bool = False,
121
- cache_hash: bool = False,
122
- validate_on_init: bool = False,
123
- ) -> None: ...
124
76
  def __rich_repr__(self) -> list[tuple[str, Any]]: ...
125
77
  def __copy__(self) -> Self: ...
126
78
  def __reduce__(self) -> tuple: ...
@@ -179,107 +131,95 @@ class Factory:
179
131
 
180
132
  @final
181
133
  class Field:
182
- # Numeric:
183
- @overload
184
134
  def __init__(
185
135
  self,
186
136
  *,
187
- gt: int | float | None = None,
188
- lt: int | float | None = None,
189
- multiple_of: int | float | None = None,
190
137
  alias: str | None = None,
191
138
  title: str | None = None,
192
139
  description: str | None = None,
193
140
  json_schema_extra: dict[str, Any] | None = None,
194
141
  examples: list[Any] | None = None,
195
142
  deprecated: bool | None = None,
196
- dump: Callable[[Any], Any] | None = None,
197
- validate: Callable[[Any], Any] | None = None,
198
143
  ) -> None: ...
199
- @overload
144
+ alias: Final[str | None]
145
+ title: Final[str | None]
146
+ description: Final[str | None]
147
+ examples: Final[list[Any] | None]
148
+ deprecated: Final[bool | None]
149
+ json_schema_extra: Final[dict[str, Any] | None]
150
+ def __rich_repr__(self) -> list[tuple[str, Any]]: ...
151
+
152
+ @final
153
+ class Serializer:
200
154
  def __init__(
201
155
  self,
202
156
  *,
203
- gt: int | float | None = None,
204
- le: int | float | None = None,
205
- multiple_of: int | float | None = None,
206
- alias: str | None = None,
207
- title: str | None = None,
208
- description: str | None = None,
209
- json_schema_extra: dict[str, Any] | None = None,
210
- examples: list[Any] | None = None,
211
- deprecated: bool | None = None,
157
+ load: Callable[[Any], Any] | None = None,
212
158
  dump: Callable[[Any], Any] | None = None,
213
- validate: Callable[[Any], Any] | None = None,
214
159
  ) -> None: ...
215
- @overload
160
+ load: Final[Callable[[Any], Any] | None]
161
+ dump: Final[Callable[[Any], Any] | None]
162
+
163
+ class Validator:
164
+ def __init__(self, fn: Callable[[Any], Any] | None = None) -> None: ...
165
+ def __call__(self, value: Any) -> None: ...
166
+
167
+ @final
168
+ class NumericValidator(Validator):
216
169
  def __init__(
217
170
  self,
218
171
  *,
172
+ gt: int | float | None = None,
219
173
  ge: int | float | None = None,
220
174
  lt: int | float | None = None,
175
+ le: int | float | None = None,
221
176
  multiple_of: int | float | None = None,
222
- alias: str | None = None,
223
- title: str | None = None,
224
- description: str | None = None,
225
- json_schema_extra: dict[str, Any] | None = None,
226
- examples: list[Any] | None = None,
227
- deprecated: bool | None = None,
228
- dump: Callable[[Any], Any] | None = None,
229
- validate: Callable[[Any], Any] | None = None,
230
177
  ) -> None: ...
231
- @overload
178
+ gt: Final[int | float | None]
179
+ ge: Final[int | float | None]
180
+ lt: Final[int | float | None]
181
+ le: Final[int | float | None]
182
+ multiple_of: Final[int | float | None]
183
+
184
+ @final
185
+ class StrValidator(Validator):
232
186
  def __init__(
233
187
  self,
234
188
  *,
235
- ge: int | float | None = None,
236
- le: int | float | None = None,
237
- multiple_of: int | float | None = None,
238
- alias: str | None = None,
239
- title: str | None = None,
240
- description: str | None = None,
241
- json_schema_extra: dict[str, Any] | None = None,
242
- examples: list[Any] | None = None,
243
- deprecated: bool | None = None,
244
- dump: Callable[[Any], Any] | None = None,
245
- validate: Callable[[Any], Any] | None = None,
189
+ pattern: str | None = None,
190
+ min_length: _NonNegativeInt | None = None,
191
+ max_length: _NonNegativeInt | None = None,
246
192
  ) -> None: ...
247
- # Other (string/datetime):
248
- @overload
193
+ pattern: Final[str | None]
194
+ min_length: Final[int | None]
195
+ max_length: Final[int | None]
196
+
197
+ @final
198
+ class BytesValidator(Validator):
249
199
  def __init__(
250
200
  self,
251
201
  *,
252
- pattern: str | None = None,
253
202
  min_length: _NonNegativeInt | None = None,
254
203
  max_length: _NonNegativeInt | None = None,
255
- tz: bool | None = None,
256
- alias: str | None = None,
257
- title: str | None = None,
258
- description: str | None = None,
259
- json_schema_extra: dict[str, Any] | None = None,
260
- examples: list[Any] | None = None,
261
- deprecated: bool | None = None,
262
- dump: Callable[[Any], Any] | None = None,
263
- validate: Callable[[Any], Any] | None = None,
264
204
  ) -> None: ...
265
- alias: Final[str | None]
266
- gt: Final[int | float | None]
267
- ge: Final[int | float | None]
268
- lt: Final[int | float | None]
269
- le: Final[int | float | None]
270
- multiple_of: Final[int | float | None]
271
- pattern: Final[str | None]
272
205
  min_length: Final[int | None]
273
206
  max_length: Final[int | None]
274
- tz: Final[int | None]
275
- title: Final[str | None]
276
- description: Final[str | None]
277
- examples: Final[list[Any] | None]
278
- deprecated: Final[bool | None]
279
- json_schema_extra: Final[dict[str, Any] | None]
280
- dump: Final[Callable[[Any], Any] | None]
281
- validate: Final[Callable[[Any], Any] | None]
282
- def __rich_repr__(self) -> list[tuple[str, Any]]: ...
207
+
208
+ @final
209
+ class CollectionValidator(Validator):
210
+ def __init__(
211
+ self,
212
+ *,
213
+ min_length: _NonNegativeInt | None = None,
214
+ max_length: _NonNegativeInt | None = None,
215
+ ) -> None: ...
216
+ min_length: Final[int | None]
217
+ max_length: Final[int | None]
218
+
219
+ @final
220
+ class TimezoneValidator(Validator):
221
+ def __init__(self, *, tz: bool) -> None: ...
222
+ tz: Final[bool]
283
223
 
284
224
  class FieldInfo(Struct):
285
225
  name: str
@@ -1,7 +1,7 @@
1
1
  from typing import Any, get_args
2
2
 
3
3
  from ._core import ( # type: ignore
4
- Field as _Field,
4
+ Serializer as _Serializer,
5
5
  )
6
6
  from ._core import ( # type: ignore
7
7
  _dump,
@@ -11,22 +11,22 @@ from ._core import ( # type: ignore
11
11
  )
12
12
 
13
13
 
14
- def _has_codec(ann):
15
- """True if the annotation carries a ``Field`` with ``dump``/``validate``."""
14
+ def _has_serializer(ann):
15
+ """True if the annotation carries a ``Serializer`` with ``load``/``dump``."""
16
16
  metadata = getattr(ann, "__metadata__", None)
17
17
  if metadata is not None:
18
18
  for meta in metadata:
19
- if isinstance(meta, _Field) and (
20
- meta.dump is not None or meta.validate is not None
19
+ if isinstance(meta, _Serializer) and (
20
+ meta.load is not None or meta.dump is not None
21
21
  ):
22
22
  return True
23
23
  supertype = getattr(ann, "__supertype__", None) # NewType
24
- if supertype is not None and _has_codec(supertype):
24
+ if supertype is not None and _has_serializer(supertype):
25
25
  return True
26
26
  value = getattr(ann, "__value__", None) # PEP 695 type alias
27
- if value is not None and _has_codec(value):
27
+ if value is not None and _has_serializer(value):
28
28
  return True
29
- return any(_has_codec(arg) for arg in get_args(ann))
29
+ return any(_has_serializer(arg) for arg in get_args(ann))
30
30
 
31
31
 
32
32
  class StructAdapter:
@@ -35,8 +35,8 @@ class StructAdapter:
35
35
  Useful when you want to validate or serialize plain Python types
36
36
  (e.g. ``list[int]``) without defining a full ``Struct`` subclass.
37
37
 
38
- ``Field(dump=...)`` / ``Field(validate=...)`` codecs are not supported on
39
- ``StructAdapter`` — annotations carrying one are rejected. Implement the
38
+ ``Serializer(load=...)`` / ``Serializer(dump=...)`` codecs are not supported
39
+ on ``StructAdapter`` — annotations carrying one are rejected. Implement the
40
40
  ``struct_dump`` / ``struct_validate`` protocol methods on the custom type,
41
41
  or use a ``Struct``.
42
42
 
@@ -49,11 +49,12 @@ class StructAdapter:
49
49
  __slots__ = ("_type",)
50
50
 
51
51
  def __init__(self, type: Any):
52
- if _has_codec(type):
52
+ if _has_serializer(type):
53
53
  raise TypeError(
54
- "`Field(dump=...)`/`Field(validate=...)` codecs are not supported "
55
- "on StructAdapter; define `struct_dump`/`struct_validate` methods "
56
- "on the custom type, or use a `Struct` instead"
54
+ "`Serializer(load=...)`/`Serializer(dump=...)` codecs are not "
55
+ "supported on StructAdapter; define `struct_dump`/"
56
+ "`struct_validate` methods on the custom type, or use a "
57
+ "`Struct` instead"
57
58
  )
58
59
  self._type = type
59
60
 
@@ -0,0 +1,31 @@
1
+ from collections.abc import Callable, Mapping
2
+ from typing import Literal, TypedDict
3
+
4
+
5
+ class StructConfig(TypedDict, total=False):
6
+ """Configuration for a Struct type, mirroring the Struct config options.
7
+
8
+ Used as a class-body ``struct_config`` attribute. Keys not present inherit
9
+ from the base class; ``__struct_config__`` returns the fully-resolved dict.
10
+ """
11
+
12
+ frozen: bool
13
+ eq: bool
14
+ order: bool
15
+ kw_only: bool
16
+ repr_omit_defaults: bool
17
+ array_like: bool
18
+ omit_defaults: bool
19
+ forbid_unknown_fields: bool
20
+ validate_on_init: bool
21
+ weakref: bool
22
+ dict: bool
23
+ cache_hash: bool
24
+ tag: bool | str | int | Callable[[str], str | int] | None
25
+ tag_field: str | None
26
+ rename: (
27
+ None
28
+ | Literal["lower", "upper", "camel", "pascal", "kebab"]
29
+ | Callable[[str], str | None]
30
+ | Mapping[str, str]
31
+ )