structtype 0.11.0__tar.gz → 0.12.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.11.0 → structtype-0.12.0}/AGENTS.md +11 -5
- {structtype-0.11.0 → structtype-0.12.0}/Makefile +41 -7
- {structtype-0.11.0/src/structtype.egg-info → structtype-0.12.0}/PKG-INFO +1 -1
- {structtype-0.11.0 → structtype-0.12.0}/src/structtype/_adapter.py +52 -3
- {structtype-0.11.0 → structtype-0.12.0}/src/structtype/_core.c +775 -247
- {structtype-0.11.0 → structtype-0.12.0}/src/structtype/_version.py +2 -2
- {structtype-0.11.0 → structtype-0.12.0/src/structtype.egg-info}/PKG-INFO +1 -1
- {structtype-0.11.0 → structtype-0.12.0}/src/structtype.egg-info/SOURCES.txt +2 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/test_adapter.py +42 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/test_annotations.py +1026 -41
- {structtype-0.11.0 → structtype-0.12.0}/tests/test_attrs.py +37 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/test_free_threading.py +38 -0
- structtype-0.12.0/tests/test_frozendict.py +93 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/test_json.py +502 -5
- structtype-0.12.0/tests/test_memory.py +151 -0
- structtype-0.12.0/tests/test_threading.py +177 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/utils.py +5 -0
- {structtype-0.11.0 → structtype-0.12.0}/uv.lock +3 -3
- structtype-0.11.0/tests/test_memory.py +0 -63
- {structtype-0.11.0 → structtype-0.12.0}/.opencode/opencode.json +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/.opencode/plugins/graphify.js +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/LICENSE +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/MANIFEST.in +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/README.md +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/pyproject.toml +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/setup.cfg +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/setup.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/src/structtype/__init__.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/src/structtype/__init__.pyi +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/src/structtype/_config.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/src/structtype/_inspect.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/src/structtype/_json_schema.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/src/structtype/_utils.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/src/structtype/atof.h +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/src/structtype/atof_consts.h +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/src/structtype/common.h +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/src/structtype/itoa.h +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/src/structtype/py.typed +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/src/structtype/ryu.h +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/src/structtype.egg-info/dependency_links.txt +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/src/structtype.egg-info/top_level.txt +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/__init__.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/conftest.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/test_JSONTestSuite.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/test_check.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/test_constraints.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/test_cpylint.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/test_inspect.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/test_msgspec.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/test_pydantic.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/test_roundtrip.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/test_schema.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/test_struct.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/test_struct_meta.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/test_typecheck.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/test_utils.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/typecheck/api.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/typecheck/basic.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/typecheck/fields.py +0 -0
- {structtype-0.11.0 → structtype-0.12.0}/tests/typecheck/types.py +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## Project
|
|
4
4
|
|
|
5
5
|
Fast struct validation + JSON serialization for Python.
|
|
6
|
-
Core is a monolithic C extension (`src/structtype/_core.c`, ~
|
|
6
|
+
Core is a monolithic C extension (`src/structtype/_core.c`, ~21K lines).
|
|
7
7
|
No runtime deps.
|
|
8
8
|
|
|
9
9
|
## Setup
|
|
@@ -14,7 +14,8 @@ uv sync --frozen
|
|
|
14
14
|
|
|
15
15
|
## Commands
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Use `make` targets where available. Targeted tests can be run directly with
|
|
18
|
+
`uv run` as shown below.
|
|
18
19
|
|
|
19
20
|
| Task | Command |
|
|
20
21
|
|---|---|
|
|
@@ -22,12 +23,13 @@ All commands go through `make`.
|
|
|
22
23
|
| Targeted tests | `uv run --reinstall pytest tests/test_json.py -k test_something` |
|
|
23
24
|
| Coverage (Python) | `make test-cov` |
|
|
24
25
|
| Coverage (Python + C) | `make test-cov-c` |
|
|
26
|
+
| Coverage (Python + C, all Pythons, merged) | `make test-cov-c-all` |
|
|
25
27
|
| Tests in all supported Pythons | `make test-all` |
|
|
26
28
|
| Build docs | `make docs` |
|
|
27
29
|
| Format | `make format` |
|
|
28
30
|
| Lint | `make ruff-check` |
|
|
29
31
|
| Type check | `make type-check` |
|
|
30
|
-
|
|
|
32
|
+
| Static checks | `make check` |
|
|
31
33
|
|
|
32
34
|
## Conventions
|
|
33
35
|
|
|
@@ -38,16 +40,17 @@ All commands go through `make`.
|
|
|
38
40
|
- Type stubs (`.pyi`) alongside public modules
|
|
39
41
|
- Sentinel values: `NODEFAULT`, `UNSET`, `_NoDefault`, `UnsetType`
|
|
40
42
|
- never do git commit
|
|
43
|
+
- check for performance regressions, speed is a goal
|
|
41
44
|
|
|
42
45
|
## Key API
|
|
43
46
|
|
|
44
47
|
- `structtype.Struct` — base class with config options (frozen, tag, rename, etc.)
|
|
45
48
|
- `structtype.Field` — field metadata (alias, title, description, examples, deprecated, json_schema_extra)
|
|
46
49
|
- `structtype.Constraint` — base constraint (callable `fn`); subclasses: `NumericConstraint`, `StrConstraint`, `BytesConstraint`, `CollectionConstraint`, `TimezoneConstraint`
|
|
47
|
-
- `structtype.Serializer` —
|
|
48
|
-
- `structtype.Raw` — lazy JSON passthrough
|
|
50
|
+
- `structtype.Serializer` — load/dump codecs for supported custom and native types
|
|
49
51
|
- `structtype.fields(type_or_instance)` — get FieldInfo tuple for a struct type/instance
|
|
50
52
|
- `structtype._inspect.type_info()` / `multi_type_info()` — type introspection
|
|
53
|
+
- `structtype.StructAdapter` — validate and serialize values against arbitrary types
|
|
51
54
|
|
|
52
55
|
### Struct Methods
|
|
53
56
|
|
|
@@ -67,7 +70,10 @@ Struct instances support the mapping protocol:
|
|
|
67
70
|
|
|
68
71
|
- `make test-cov` reinstalls the C extension before running. `make test-cov-c` builds an `-O0 --coverage` instrumented extension **in place**; afterwards any reinstalling target (`make test`, `make test-cov`) restores the optimized build.
|
|
69
72
|
- C coverage requires `lcov`/`genhtml`; report lands in `htmlcov-c/`.
|
|
73
|
+
- `make check` runs static type and Ruff checks only; it does not run tests,
|
|
74
|
+
documentation builds, or formatting.
|
|
70
75
|
- Validation matches keys by the **alias** name only, except `struct_validate(obj, from_attributes=True)` on a **non-dict object**, which matches by both the python field name and the alias. Dict/JSON input (even with `from_attributes=True`) and all dump/serialization use only the alias name.
|
|
76
|
+
- For `Annotated[MyType, Serializer(load=...)]`, an existing `MyType` instance bypasses `load`; other Python values and JSON representations are passed to `load`. Serializers remain rejected on `Any`, `bool`, `int`, `float`, `str`, `list`, `dict`, `tuple`, `TypedDict`, `NamedTuple`, `frozendict`, and `Literal`. `Serializer` and `Constraint` must attach to a concrete type inside `Annotated` — `Annotated[Union[A, B], ...]` and `Annotated[Optional[T], ...]` are rejected; use `Annotated[T, ...] | None` for optional fields.
|
|
71
77
|
|
|
72
78
|
## graphify
|
|
73
79
|
|
|
@@ -19,7 +19,7 @@ docs-serve: ## Open built docs in browser
|
|
|
19
19
|
|
|
20
20
|
.PHONY: wheels
|
|
21
21
|
wheels: ## build wheels (musllinux skipped; unset CIBW_SKIP or run cibuildwheel directly to include it)
|
|
22
|
-
CIBW_SKIP='*-musllinux_*' uvx --from cibuildwheel==4.2.
|
|
22
|
+
CIBW_SKIP='*-musllinux_*' uvx --from cibuildwheel==4.2.1 cibuildwheel
|
|
23
23
|
|
|
24
24
|
##@ Quality
|
|
25
25
|
.PHONY: test-cov
|
|
@@ -30,11 +30,41 @@ test-cov: ## Run tests with coverage
|
|
|
30
30
|
test-cov-c: ## Run tests with Python + C coverage (lcov report in htmlcov-c/)
|
|
31
31
|
rm -rf build coverage-c.info coverage-c.info.* htmlcov-c
|
|
32
32
|
STRUCTTYPE_COVERAGE=1 uv run --with setuptools python setup.py build_ext --inplace --force
|
|
33
|
-
./.venv/bin/python -m pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=structtype
|
|
34
|
-
|
|
35
|
-
lcov --
|
|
36
|
-
lcov --
|
|
37
|
-
|
|
33
|
+
./.venv/bin/python -m pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=structtype \
|
|
34
|
+
--deselect tests/test_json.py::TestEncoderMisc::test_encode_infinite_recursive_object_errors
|
|
35
|
+
lcov --capture --directory build --output-file coverage-c.info --rc branch_coverage=1 --rc geninfo_unexecuted_blocks=1
|
|
36
|
+
lcov --extract coverage-c.info "*/src/structtype/*" --output-file coverage-c.info --rc branch_coverage=1
|
|
37
|
+
lcov --summary coverage-c.info --rc branch_coverage=1
|
|
38
|
+
genhtml coverage-c.info --output-directory htmlcov-c --branch-coverage >/dev/null
|
|
39
|
+
|
|
40
|
+
# C coverage is version-dependent: `_core.c` has many `#if PY30x_PLUS` and
|
|
41
|
+
# `#ifdef Py_GIL_DISABLED` regions, so a single build only covers part of the
|
|
42
|
+
# code. This target builds an instrumented extension for every supported
|
|
43
|
+
# Python, runs the suite under each, and merges the lcov data.
|
|
44
|
+
.PHONY: test-cov-c-all
|
|
45
|
+
test-cov-c-all: ## Run C coverage across all supported Pythons, merged (htmlcov-c/)
|
|
46
|
+
rm -rf build coverage-c.info coverage-c.info.* htmlcov-c
|
|
47
|
+
@set -e; \
|
|
48
|
+
for py in $(PY_VERSIONS); do \
|
|
49
|
+
tag=$$(echo "$$py" | tr -d '.'); \
|
|
50
|
+
venv=".venv-cov-$$tag"; \
|
|
51
|
+
echo "=== C coverage: $$py ($$venv) ==="; \
|
|
52
|
+
rm -rf build; \
|
|
53
|
+
env -u VIRTUAL_ENV UV_PROJECT_ENVIRONMENT="$$venv" STRUCTTYPE_COVERAGE=1 \
|
|
54
|
+
uv run -p "$$py" --with setuptools python setup.py build_ext --inplace --force; \
|
|
55
|
+
"$$venv/bin/python" -m pytest -q \
|
|
56
|
+
--deselect tests/test_json.py::TestEncoderMisc::test_encode_infinite_recursive_object_errors; \
|
|
57
|
+
lcov --capture --directory build --output-file "coverage-c.info.$$tag" \
|
|
58
|
+
--rc branch_coverage=1 --rc geninfo_unexecuted_blocks=1; \
|
|
59
|
+
lcov --extract "coverage-c.info.$$tag" "*/src/structtype/*" \
|
|
60
|
+
--output-file "coverage-c.info.$$tag" --rc branch_coverage=1; \
|
|
61
|
+
done; \
|
|
62
|
+
lcov $$(for f in coverage-c.info.*; do printf ' -a %s' "$$f"; done) \
|
|
63
|
+
--output-file coverage-c.info --rc branch_coverage=1; \
|
|
64
|
+
lcov --summary coverage-c.info --rc branch_coverage=1; \
|
|
65
|
+
genhtml coverage-c.info --output-directory htmlcov-c --branch-coverage >/dev/null; \
|
|
66
|
+
env -u VIRTUAL_ENV uv run --reinstall-package structtype python -c "import structtype" >/dev/null; \
|
|
67
|
+
echo "Merged C coverage written to htmlcov-c/. Re-run 'make test-all' to restore the other optimized builds."
|
|
38
68
|
|
|
39
69
|
.PHONY: test
|
|
40
70
|
test: ## Run tests in current Python
|
|
@@ -71,7 +101,7 @@ DEBUG_VENV = .venv-debug
|
|
|
71
101
|
.PHONY: test-debug
|
|
72
102
|
test-debug: ## Build core with Py_DEBUG + ASan/UBSan + debug allocator and run all tests
|
|
73
103
|
uv venv --clear --python $(DEBUG_PY) $(DEBUG_VENV)
|
|
74
|
-
STRUCTTYPE_SANITIZE=1 uv pip install --python $(DEBUG_VENV) --reinstall --no-cache --group dev
|
|
104
|
+
STRUCTTYPE_SANITIZE=1 uv pip install --python $(DEBUG_VENV) --reinstall --no-cache --group dev .
|
|
75
105
|
$(SANITIZE_PRELOAD) STRUCTTYPE_ASAN_RUNTIME=$(ASAN_RUNTIME) ASAN_OPTIONS=detect_leaks=0 \
|
|
76
106
|
PYTHONMALLOC=debug PYTHONFAULTHANDLER=1 PYTHONDEVMODE=1 \
|
|
77
107
|
$(DEBUG_VENV)/bin/python -m pytest
|
|
@@ -109,6 +139,10 @@ bench: ## run benchmarks
|
|
|
109
139
|
bench-validators: ## run Serializer/Validator benchmarks
|
|
110
140
|
uv run -p 3.15 benchmarks/bench_validators.py
|
|
111
141
|
|
|
142
|
+
.PHONY: bench-codecs
|
|
143
|
+
bench-codecs: ## run Constraint + Serializer heavy benchmarks
|
|
144
|
+
uv run -p 3.15 benchmarks/bench_codecs.py
|
|
145
|
+
|
|
112
146
|
##@ Utility
|
|
113
147
|
.PHONY: clean
|
|
114
148
|
clean: ## Delete all temporary files
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import types
|
|
2
|
+
import typing
|
|
1
3
|
from typing import Any, get_args
|
|
2
4
|
|
|
3
5
|
from ._core import ( # type: ignore
|
|
6
|
+
Constraint as _Constraint,
|
|
4
7
|
JSONDecoder as _JSONDecoder,
|
|
5
8
|
Serializer as _Serializer,
|
|
6
9
|
_dump,
|
|
@@ -27,6 +30,38 @@ def _has_serializer(ann):
|
|
|
27
30
|
return any(_has_serializer(arg) for arg in get_args(ann))
|
|
28
31
|
|
|
29
32
|
|
|
33
|
+
def _is_union(t):
|
|
34
|
+
"""True if ``t`` is ``typing.Union`` or a ``types.UnionType`` (``X | Y``)."""
|
|
35
|
+
if getattr(t, "__origin__", None) is typing.Union:
|
|
36
|
+
return True
|
|
37
|
+
return isinstance(t, types.UnionType)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _has_constraint_on_union(ann):
|
|
41
|
+
"""True if a ``Constraint`` is attached to a union or optional type.
|
|
42
|
+
|
|
43
|
+
``Struct`` rejects this at class creation; ``StructAdapter`` mirrors the
|
|
44
|
+
same rule at construction. A concrete member may still be constrained and
|
|
45
|
+
then made optional, e.g. ``Annotated[T, Constraint(...)] | None``.
|
|
46
|
+
"""
|
|
47
|
+
metadata = getattr(ann, "__metadata__", None)
|
|
48
|
+
if metadata is not None:
|
|
49
|
+
origin = getattr(ann, "__origin__", None)
|
|
50
|
+
if (
|
|
51
|
+
origin is not None
|
|
52
|
+
and _is_union(origin)
|
|
53
|
+
and any(isinstance(meta, _Constraint) for meta in metadata)
|
|
54
|
+
):
|
|
55
|
+
return True
|
|
56
|
+
supertype = getattr(ann, "__supertype__", None) # NewType
|
|
57
|
+
if supertype is not None and _has_constraint_on_union(supertype):
|
|
58
|
+
return True
|
|
59
|
+
value = getattr(ann, "__value__", None) # PEP 695 type alias
|
|
60
|
+
if value is not None and _has_constraint_on_union(value):
|
|
61
|
+
return True
|
|
62
|
+
return any(_has_constraint_on_union(arg) for arg in get_args(ann))
|
|
63
|
+
|
|
64
|
+
|
|
30
65
|
class StructAdapter:
|
|
31
66
|
"""Adapter for validating and serializing types without subclassing ``Struct``.
|
|
32
67
|
|
|
@@ -38,6 +73,11 @@ class StructAdapter:
|
|
|
38
73
|
``struct_dump`` / ``struct_validate`` protocol methods on the custom type,
|
|
39
74
|
or use a ``Struct``.
|
|
40
75
|
|
|
76
|
+
``Constraint`` annotations are supported on concrete types. A ``Constraint``
|
|
77
|
+
attached to a union or optional type is rejected at construction, matching
|
|
78
|
+
``Struct`` class creation; make the field optional with
|
|
79
|
+
``Annotated[T, Constraint(...)] | None``.
|
|
80
|
+
|
|
41
81
|
>>> from structtype import StructAdapter
|
|
42
82
|
>>> adapter = StructAdapter(list[int])
|
|
43
83
|
>>> adapter.struct_validate_json(b"[1, 2, 3]")
|
|
@@ -54,6 +94,12 @@ class StructAdapter:
|
|
|
54
94
|
"`struct_validate` methods on the custom type, or use a "
|
|
55
95
|
"`Struct` instead"
|
|
56
96
|
)
|
|
97
|
+
if _has_constraint_on_union(type):
|
|
98
|
+
raise TypeError(
|
|
99
|
+
"`Constraint` must be applied to a concrete type, not a union "
|
|
100
|
+
"or optional type; use `Annotated[T, Constraint(...)] | None` "
|
|
101
|
+
"for optional fields"
|
|
102
|
+
)
|
|
57
103
|
self._type = type
|
|
58
104
|
self._decoder_loose = None
|
|
59
105
|
self._decoder_strict = None
|
|
@@ -66,7 +112,8 @@ class StructAdapter:
|
|
|
66
112
|
buf : str or bytes
|
|
67
113
|
The JSON message to decode.
|
|
68
114
|
strict : bool, optional
|
|
69
|
-
If True (default),
|
|
115
|
+
If True (default), use strict type validation and coercion rules.
|
|
116
|
+
If False, allow the documented lax-mode conversions.
|
|
70
117
|
"""
|
|
71
118
|
if strict:
|
|
72
119
|
decoder = self._decoder_strict
|
|
@@ -118,9 +165,11 @@ class StructAdapter:
|
|
|
118
165
|
obj : Any
|
|
119
166
|
A Python object to validate and convert.
|
|
120
167
|
strict : bool, optional
|
|
121
|
-
If True (default),
|
|
168
|
+
If True (default), use strict type validation and coercion rules.
|
|
169
|
+
If False, allow the documented lax-mode conversions.
|
|
122
170
|
from_attributes : bool, optional
|
|
123
|
-
If True, accept objects
|
|
171
|
+
If True, accept non-dict objects by reading matching attributes.
|
|
172
|
+
Dict input continues to match fields by serialized alias names.
|
|
124
173
|
"""
|
|
125
174
|
return _validate(
|
|
126
175
|
obj,
|