structtype 0.14.0__tar.gz → 0.15.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 (63) hide show
  1. {structtype-0.14.0 → structtype-0.15.0}/AGENTS.md +8 -2
  2. {structtype-0.14.0 → structtype-0.15.0}/MANIFEST.in +0 -1
  3. {structtype-0.14.0 → structtype-0.15.0}/Makefile +19 -12
  4. {structtype-0.14.0 → structtype-0.15.0}/PKG-INFO +1 -1
  5. {structtype-0.14.0 → structtype-0.15.0}/src/structtype/__init__.pyi +21 -12
  6. {structtype-0.14.0 → structtype-0.15.0}/src/structtype/_adapter.py +20 -0
  7. {structtype-0.14.0 → structtype-0.15.0}/src/structtype/_config.py +2 -1
  8. {structtype-0.14.0 → structtype-0.15.0}/src/structtype/_core.c +523 -183
  9. {structtype-0.14.0 → structtype-0.15.0}/src/structtype/_inspect.py +12 -4
  10. {structtype-0.14.0 → structtype-0.15.0}/src/structtype/_json_schema.py +8 -4
  11. {structtype-0.14.0 → structtype-0.15.0}/src/structtype/_version.py +2 -2
  12. {structtype-0.14.0 → structtype-0.15.0}/src/structtype/atof.h +5 -0
  13. {structtype-0.14.0 → structtype-0.15.0}/src/structtype.egg-info/PKG-INFO +1 -1
  14. {structtype-0.14.0 → structtype-0.15.0}/src/structtype.egg-info/SOURCES.txt +2 -1
  15. structtype-0.15.0/src/structtype.egg-info/scm_file_list.json +97 -0
  16. {structtype-0.14.0 → structtype-0.15.0}/tests/test_adapter.py +17 -0
  17. {structtype-0.14.0 → structtype-0.15.0}/tests/test_annotations.py +13 -11
  18. {structtype-0.14.0 → structtype-0.15.0}/tests/test_check.py +68 -0
  19. {structtype-0.14.0 → structtype-0.15.0}/tests/test_constraints.py +1 -1
  20. {structtype-0.14.0 → structtype-0.15.0}/tests/test_csv.py +23 -0
  21. {structtype-0.14.0 → structtype-0.15.0}/tests/test_free_threading.py +9 -72
  22. {structtype-0.14.0 → structtype-0.15.0}/tests/test_json.py +95 -0
  23. structtype-0.15.0/tests/test_protocols.py +181 -0
  24. {structtype-0.14.0 → structtype-0.15.0}/tests/test_schema.py +39 -0
  25. {structtype-0.14.0 → structtype-0.15.0}/tests/test_struct.py +93 -19
  26. {structtype-0.14.0 → structtype-0.15.0}/tests/test_struct_meta.py +170 -7
  27. {structtype-0.14.0 → structtype-0.15.0}/tests/typecheck/basic.py +15 -0
  28. {structtype-0.14.0 → structtype-0.15.0}/tests/typecheck/fields.py +21 -0
  29. {structtype-0.14.0 → structtype-0.15.0}/uv.lock +401 -349
  30. structtype-0.14.0/.opencode/opencode.json +0 -4
  31. {structtype-0.14.0 → structtype-0.15.0}/LICENSE +0 -0
  32. {structtype-0.14.0 → structtype-0.15.0}/README.md +0 -0
  33. {structtype-0.14.0 → structtype-0.15.0}/pyproject.toml +0 -0
  34. {structtype-0.14.0 → structtype-0.15.0}/setup.cfg +0 -0
  35. {structtype-0.14.0 → structtype-0.15.0}/setup.py +0 -0
  36. {structtype-0.14.0 → structtype-0.15.0}/src/structtype/__init__.py +0 -0
  37. {structtype-0.14.0 → structtype-0.15.0}/src/structtype/_utils.py +0 -0
  38. {structtype-0.14.0 → structtype-0.15.0}/src/structtype/atof_consts.h +0 -0
  39. {structtype-0.14.0 → structtype-0.15.0}/src/structtype/common.h +0 -0
  40. {structtype-0.14.0 → structtype-0.15.0}/src/structtype/itoa.h +0 -0
  41. {structtype-0.14.0 → structtype-0.15.0}/src/structtype/py.typed +0 -0
  42. {structtype-0.14.0 → structtype-0.15.0}/src/structtype/ryu.h +0 -0
  43. {structtype-0.14.0 → structtype-0.15.0}/src/structtype.egg-info/dependency_links.txt +0 -0
  44. {structtype-0.14.0 → structtype-0.15.0}/src/structtype.egg-info/top_level.txt +0 -0
  45. {structtype-0.14.0 → structtype-0.15.0}/tests/__init__.py +0 -0
  46. {structtype-0.14.0 → structtype-0.15.0}/tests/conftest.py +0 -0
  47. {structtype-0.14.0 → structtype-0.15.0}/tests/test_JSONTestSuite.py +0 -0
  48. {structtype-0.14.0 → structtype-0.15.0}/tests/test_attrs.py +0 -0
  49. {structtype-0.14.0 → structtype-0.15.0}/tests/test_cpylint.py +0 -0
  50. {structtype-0.14.0 → structtype-0.15.0}/tests/test_frozendict.py +0 -0
  51. {structtype-0.14.0 → structtype-0.15.0}/tests/test_inspect.py +0 -0
  52. {structtype-0.14.0 → structtype-0.15.0}/tests/test_ipnetwork.py +0 -0
  53. {structtype-0.14.0 → structtype-0.15.0}/tests/test_memory.py +0 -0
  54. {structtype-0.14.0 → structtype-0.15.0}/tests/test_msgspec.py +0 -0
  55. {structtype-0.14.0 → structtype-0.15.0}/tests/test_purepath.py +0 -0
  56. {structtype-0.14.0 → structtype-0.15.0}/tests/test_pydantic.py +0 -0
  57. {structtype-0.14.0 → structtype-0.15.0}/tests/test_roundtrip.py +0 -0
  58. {structtype-0.14.0 → structtype-0.15.0}/tests/test_threading.py +0 -0
  59. {structtype-0.14.0 → structtype-0.15.0}/tests/test_typecheck.py +0 -0
  60. {structtype-0.14.0 → structtype-0.15.0}/tests/test_utils.py +0 -0
  61. {structtype-0.14.0 → structtype-0.15.0}/tests/typecheck/api.py +0 -0
  62. {structtype-0.14.0 → structtype-0.15.0}/tests/typecheck/types.py +0 -0
  63. {structtype-0.14.0 → structtype-0.15.0}/tests/utils.py +0 -0
@@ -34,7 +34,11 @@ Use `make` targets where available. Targeted tests can be run directly with
34
34
  | Static checks | `make check` |
35
35
 
36
36
  Benchmark targets: `make bench`, `make bench-validators`, `make bench-codecs`,
37
- `make bench-csv-1m`, `make bench-strings` (run sequentially; CPU-bound).
37
+ `make bench-field-types`, `make bench-csv-1m`, `make bench-strings`
38
+ (run sequentially; CPU-bound; never in parallel).
39
+
40
+ Benchmarks run on free-threaded CPython (3.15t) via `BENCH_PYTHON` (override
41
+ with `make bench BENCH_PYTHON=3.15`); see `docs/benchmarks.rst`.
38
42
 
39
43
  ## Conventions
40
44
 
@@ -48,12 +52,14 @@ Benchmark targets: `make bench`, `make bench-validators`, `make bench-codecs`,
48
52
  - Sentinel values: `NODEFAULT`, `UNSET`, `_NoDefault`, `UnsetType`
49
53
  - never do git commit
50
54
  - check for performance regressions, speed is a goal
55
+ - no parallel benchmarks
56
+ - keep C code simple, fast, readable and compatible to freethreading
51
57
 
52
58
  ## Key API
53
59
 
54
60
  - `structtype.Struct` — base class with config options (frozen, tag, rename, etc.)
55
61
  - `structtype.Field` — field metadata (alias, title, description, examples, deprecated, json_schema_extra)
56
- - `structtype.Constraint` — base constraint (callable `fn`); subclasses: `NumericConstraint`, `StrConstraint`, `BytesConstraint`, `CollectionConstraint`, `TimezoneConstraint`
62
+ - `structtype.Constraint` — base constraint (callable `_fn`, positional-only in the constructor); subclasses: `NumericConstraint`, `StrConstraint`, `BytesConstraint`, `CollectionConstraint`, `TimezoneConstraint`
57
63
  - `structtype.Serializer` — load/dump codecs for supported custom and native types
58
64
  - `structtype.fields(type_or_instance)` — get FieldInfo tuple for a struct type/instance
59
65
  - `structtype._inspect.type_info()` / `multi_type_info()` — type introspection
@@ -1,4 +1,3 @@
1
- prune .devcontainer
2
1
  prune .github
3
2
  prune benchmarks
4
3
  prune docs
@@ -6,7 +6,8 @@ export UV_MANAGED_PYTHON ?= 1
6
6
  ##@ Build
7
7
  .PHONY: build
8
8
  build: ## Build
9
- uv build
9
+ uv build --sdist
10
+ uv build --wheel
10
11
 
11
12
  .PHONY: docs
12
13
  docs: ## build docs
@@ -107,9 +108,7 @@ test-debug: ## Build core with Py_DEBUG + ASan/UBSan + debug allocator and run a
107
108
  $(DEBUG_VENV)/bin/python -m pytest
108
109
 
109
110
  .PHONY: check
110
- check: ## Run all checks
111
- -uvx ty check ${SOURCE_DIR}
112
- uvx ruff check ${SOURCE_DIR}
111
+ check: ruff-check type-check ## Run all checks
113
112
 
114
113
  .PHONY: ruff-check
115
114
  ruff-check: ## Lint using ruff
@@ -117,8 +116,7 @@ ruff-check: ## Lint using ruff
117
116
 
118
117
  .PHONY: type-check
119
118
  type-check: ## Type check with
120
- -uvx ty check ${SOURCE_DIR}
121
- uvx pyrefly check ${SOURCE_DIR}
119
+ uvx ty check ${SOURCE_DIR}
122
120
 
123
121
  .PHONY: typecheck-tests
124
122
  typecheck-tests: ## Type check tests/typecheck fixtures (skipped when tools unavailable)
@@ -131,25 +129,34 @@ format: ## Format files using ruff format
131
129
  uvx ruff format ${SOURCE_DIR}
132
130
 
133
131
  ##@ Benchmark
132
+ # Published comparisons use free-threaded CPython (see docs/benchmarks.rst).
133
+ # Benchmark scripts are PEP 723 scripts, so this builds an isolated per-script
134
+ # environment and does not touch the project `.venv`.
135
+ BENCH_PYTHON ?= 3.15t
136
+
134
137
  .PHONY: bench
135
138
  bench: ## run benchmarks
136
- uv run -p 3.15 benchmarks/bench_libs.py
139
+ uv run -p $(BENCH_PYTHON) benchmarks/bench_libs.py
137
140
 
138
141
  .PHONY: bench-validators
139
142
  bench-validators: ## run Serializer/Validator benchmarks
140
- uv run -p 3.15 benchmarks/bench_validators.py
143
+ uv run -p $(BENCH_PYTHON) benchmarks/bench_validators.py
141
144
 
142
145
  .PHONY: bench-codecs
143
146
  bench-codecs: ## run Constraint + Serializer heavy benchmarks
144
- uv run -p 3.15 benchmarks/bench_codecs.py
147
+ uv run -p $(BENCH_PYTHON) benchmarks/bench_codecs.py
148
+
149
+ .PHONY: bench-field-types
150
+ bench-field-types: ## run per-field-type benchmarks
151
+ uv run -p $(BENCH_PYTHON) benchmarks/bench_field_types.py
145
152
 
146
153
  .PHONY: bench-csv-1m
147
154
  bench-csv-1m: ## run 1M-row on-disk CSV benchmark
148
- uv run -p 3.15 benchmarks/bench_csv_1m.py
155
+ uv run -p $(BENCH_PYTHON) benchmarks/bench_csv_1m.py
149
156
 
150
157
  .PHONY: bench-strings
151
158
  bench-strings: ## run long-string JSON codec benchmark
152
- uv run -p 3.15 benchmarks/bench_strings.py
159
+ uv run -p $(BENCH_PYTHON) benchmarks/bench_strings.py
153
160
 
154
161
  ##@ Utility
155
162
  .PHONY: clean
@@ -192,4 +199,4 @@ update-python: ## Reinstall managed Python versions to latest release
192
199
 
193
200
  .PHONY: help
194
201
  help: ## Display this help
195
- @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make <target>\033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
202
+ @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make <target>\033[36m\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-18s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structtype
3
- Version: 0.14.0
3
+ Version: 0.15.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,5 +1,4 @@
1
1
  # ruff: noqa: PYI041, PYI015
2
- import enum
3
2
  from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
4
3
  from inspect import Signature
5
4
  from typing import (
@@ -16,17 +15,15 @@ from typing import (
16
15
  from typing_extensions import Buffer, Self, dataclass_transform
17
16
 
18
17
  @final
19
- class UnsetType(enum.Enum):
20
- UNSET = "UNSET"
18
+ class UnsetType:
21
19
  def __bool__(self) -> Literal[False]: ...
22
20
 
23
- UNSET: Final = UnsetType.UNSET
21
+ UNSET: Final[UnsetType]
24
22
 
25
23
  @final
26
- class _NoDefault(enum.Enum):
27
- NODEFAULT = "NODEFAULT"
24
+ class _NoDefaultType: ...
28
25
 
29
- NODEFAULT: Final = _NoDefault.NODEFAULT
26
+ NODEFAULT: Final[_NoDefaultType]
30
27
 
31
28
  class StructConfig(TypedDict, total=False):
32
29
  frozen: bool
@@ -59,8 +56,6 @@ class StructMeta(type):
59
56
  def __signature__(self) -> Signature: ...
60
57
  @property
61
58
  def __struct_config__(self) -> StructConfig: ...
62
- @property
63
- def struct_config(self) -> StructConfig: ...
64
59
 
65
60
  @dataclass_transform(field_specifiers=("Field",)) # type: ignore
66
61
  class Struct(metaclass=StructMeta):
@@ -70,6 +65,7 @@ class Struct(metaclass=StructMeta):
70
65
  __struct_alias_fields__: ClassVar[tuple[str, ...]]
71
66
  __struct_defaults__: ClassVar[tuple[Any, ...]]
72
67
  __match_args__: ClassVar[tuple[str, ...]] = ...
68
+ __slots__: ClassVar[tuple[str, ...]] = ...
73
69
  # A default __init__ so that Structs with unknown field types
74
70
  # won't error on every call to `__init__`
75
71
  def __init__(self, *args: Any, **kwargs: Any) -> None: ...
@@ -78,6 +74,14 @@ class Struct(metaclass=StructMeta):
78
74
  def __reduce__(self) -> tuple: ...
79
75
  def __replace__(self, **changes: Any) -> Self: ...
80
76
  def __iter__(self) -> Iterator[tuple[str, Any]]: ...
77
+ # Generated only when `struct_config = StructConfig(order=True)`. They are
78
+ # declared unconditionally because the class-body config dict is invisible
79
+ # to `@dataclass_transform`. A different struct type is never orderable
80
+ # against this one, hence `Self`.
81
+ def __lt__(self, other: Self) -> bool: ...
82
+ def __le__(self, other: Self) -> bool: ...
83
+ def __gt__(self, other: Self) -> bool: ...
84
+ def __ge__(self, other: Self) -> bool: ...
81
85
  def struct_dump_json(
82
86
  self,
83
87
  *,
@@ -122,7 +126,7 @@ _NonNegativeInt: TypeAlias = int
122
126
 
123
127
  @final
124
128
  class Factory:
125
- def __new__(cls, factory: Callable[[], Any]) -> Any: ...
129
+ def __new__(cls, factory: Callable[[], Any], /) -> Any: ...
126
130
  factory: Final[Callable[[], Any]]
127
131
 
128
132
  @final
@@ -157,7 +161,12 @@ class Serializer:
157
161
  dump: Final[Callable[[Any], Any] | None]
158
162
 
159
163
  class Constraint:
160
- def __init__(self, fn: Callable[[Any], Any] | None = None) -> None: ...
164
+ # `_fn` is positional-only and stored privately: it is not part of the
165
+ # public interface. The callable follows the `Constraint.__call__`
166
+ # protocol — it receives the value and signals failure by raising, so its
167
+ # return type is `None`.
168
+ def __init__(self, fn: Callable[[Any], None] | None = None, /) -> None: ...
169
+ _fn: Final[Callable[[Any], None] | None]
161
170
  def __call__(self, value: Any) -> None: ...
162
171
 
163
172
  @final
@@ -243,7 +252,7 @@ class CollectionConstraint(Constraint):
243
252
 
244
253
  @final
245
254
  class TimezoneConstraint(Constraint):
246
- def __init__(self, *, tz: bool) -> None: ...
255
+ def __init__(self, tz: bool) -> None: ...
247
256
  tz: Final[bool]
248
257
 
249
258
  class FieldInfo(Struct):
@@ -62,6 +62,22 @@ def _has_constraint_on_union(ann):
62
62
  return any(_has_constraint_on_union(arg) for arg in get_args(ann))
63
63
 
64
64
 
65
+ def _has_constraint(ann):
66
+ """True if the annotation carries a ``Constraint`` anywhere."""
67
+ metadata = getattr(ann, "__metadata__", None)
68
+ if metadata is not None:
69
+ for meta in metadata:
70
+ if isinstance(meta, _Constraint):
71
+ return True
72
+ supertype = getattr(ann, "__supertype__", None) # NewType
73
+ if supertype is not None and _has_constraint(supertype):
74
+ return True
75
+ value = getattr(ann, "__value__", None) # PEP 695 type alias
76
+ if value is not None and _has_constraint(value):
77
+ return True
78
+ return any(_has_constraint(arg) for arg in get_args(ann))
79
+
80
+
65
81
  class StructAdapter:
66
82
  """Adapter for validating and serializing types without subclassing ``Struct``.
67
83
 
@@ -101,6 +117,10 @@ class StructAdapter:
101
117
  "for optional fields"
102
118
  )
103
119
  self._type = type
120
+ # Validate constraint/type compatibility eagerly, matching `Struct`
121
+ # class creation, without disturbing the lazy decoder cache below.
122
+ if _has_constraint(type):
123
+ _JSONDecoder(type, strict=True)
104
124
  self._decoder_loose = None
105
125
  self._decoder_strict = None
106
126
 
@@ -7,7 +7,8 @@ class StructConfig(TypedDict, total=False):
7
7
 
8
8
  Used as a class-body ``struct_config`` attribute. Keys not present inherit
9
9
  from the base class; ``__struct_config__`` returns the fully-resolved dict.
10
- ``struct_config`` returns exactly what the user wrote (sparse dict).
10
+ ``struct_config`` returns exactly what the user wrote (sparse dict), and is
11
+ always present — it defaults to ``{}`` when never declared.
11
12
  """
12
13
 
13
14
  frozen: bool