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.
- {structtype-0.14.0 → structtype-0.15.0}/AGENTS.md +8 -2
- {structtype-0.14.0 → structtype-0.15.0}/MANIFEST.in +0 -1
- {structtype-0.14.0 → structtype-0.15.0}/Makefile +19 -12
- {structtype-0.14.0 → structtype-0.15.0}/PKG-INFO +1 -1
- {structtype-0.14.0 → structtype-0.15.0}/src/structtype/__init__.pyi +21 -12
- {structtype-0.14.0 → structtype-0.15.0}/src/structtype/_adapter.py +20 -0
- {structtype-0.14.0 → structtype-0.15.0}/src/structtype/_config.py +2 -1
- {structtype-0.14.0 → structtype-0.15.0}/src/structtype/_core.c +523 -183
- {structtype-0.14.0 → structtype-0.15.0}/src/structtype/_inspect.py +12 -4
- {structtype-0.14.0 → structtype-0.15.0}/src/structtype/_json_schema.py +8 -4
- {structtype-0.14.0 → structtype-0.15.0}/src/structtype/_version.py +2 -2
- {structtype-0.14.0 → structtype-0.15.0}/src/structtype/atof.h +5 -0
- {structtype-0.14.0 → structtype-0.15.0}/src/structtype.egg-info/PKG-INFO +1 -1
- {structtype-0.14.0 → structtype-0.15.0}/src/structtype.egg-info/SOURCES.txt +2 -1
- structtype-0.15.0/src/structtype.egg-info/scm_file_list.json +97 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_adapter.py +17 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_annotations.py +13 -11
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_check.py +68 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_constraints.py +1 -1
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_csv.py +23 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_free_threading.py +9 -72
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_json.py +95 -0
- structtype-0.15.0/tests/test_protocols.py +181 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_schema.py +39 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_struct.py +93 -19
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_struct_meta.py +170 -7
- {structtype-0.14.0 → structtype-0.15.0}/tests/typecheck/basic.py +15 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/typecheck/fields.py +21 -0
- {structtype-0.14.0 → structtype-0.15.0}/uv.lock +401 -349
- structtype-0.14.0/.opencode/opencode.json +0 -4
- {structtype-0.14.0 → structtype-0.15.0}/LICENSE +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/README.md +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/pyproject.toml +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/setup.cfg +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/setup.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/src/structtype/__init__.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/src/structtype/_utils.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/src/structtype/atof_consts.h +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/src/structtype/common.h +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/src/structtype/itoa.h +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/src/structtype/py.typed +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/src/structtype/ryu.h +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/src/structtype.egg-info/dependency_links.txt +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/src/structtype.egg-info/top_level.txt +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/__init__.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/conftest.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_JSONTestSuite.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_attrs.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_cpylint.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_frozendict.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_inspect.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_ipnetwork.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_memory.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_msgspec.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_purepath.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_pydantic.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_roundtrip.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_threading.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_typecheck.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/test_utils.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/typecheck/api.py +0 -0
- {structtype-0.14.0 → structtype-0.15.0}/tests/typecheck/types.py +0 -0
- {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`
|
|
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 `
|
|
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
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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,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
|
|
20
|
-
UNSET = "UNSET"
|
|
18
|
+
class UnsetType:
|
|
21
19
|
def __bool__(self) -> Literal[False]: ...
|
|
22
20
|
|
|
23
|
-
UNSET: Final
|
|
21
|
+
UNSET: Final[UnsetType]
|
|
24
22
|
|
|
25
23
|
@final
|
|
26
|
-
class
|
|
27
|
-
NODEFAULT = "NODEFAULT"
|
|
24
|
+
class _NoDefaultType: ...
|
|
28
25
|
|
|
29
|
-
NODEFAULT: Final
|
|
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
|
-
|
|
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,
|
|
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
|