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.
Files changed (60) hide show
  1. {structtype-0.11.0 → structtype-0.12.0}/AGENTS.md +11 -5
  2. {structtype-0.11.0 → structtype-0.12.0}/Makefile +41 -7
  3. {structtype-0.11.0/src/structtype.egg-info → structtype-0.12.0}/PKG-INFO +1 -1
  4. {structtype-0.11.0 → structtype-0.12.0}/src/structtype/_adapter.py +52 -3
  5. {structtype-0.11.0 → structtype-0.12.0}/src/structtype/_core.c +775 -247
  6. {structtype-0.11.0 → structtype-0.12.0}/src/structtype/_version.py +2 -2
  7. {structtype-0.11.0 → structtype-0.12.0/src/structtype.egg-info}/PKG-INFO +1 -1
  8. {structtype-0.11.0 → structtype-0.12.0}/src/structtype.egg-info/SOURCES.txt +2 -0
  9. {structtype-0.11.0 → structtype-0.12.0}/tests/test_adapter.py +42 -0
  10. {structtype-0.11.0 → structtype-0.12.0}/tests/test_annotations.py +1026 -41
  11. {structtype-0.11.0 → structtype-0.12.0}/tests/test_attrs.py +37 -0
  12. {structtype-0.11.0 → structtype-0.12.0}/tests/test_free_threading.py +38 -0
  13. structtype-0.12.0/tests/test_frozendict.py +93 -0
  14. {structtype-0.11.0 → structtype-0.12.0}/tests/test_json.py +502 -5
  15. structtype-0.12.0/tests/test_memory.py +151 -0
  16. structtype-0.12.0/tests/test_threading.py +177 -0
  17. {structtype-0.11.0 → structtype-0.12.0}/tests/utils.py +5 -0
  18. {structtype-0.11.0 → structtype-0.12.0}/uv.lock +3 -3
  19. structtype-0.11.0/tests/test_memory.py +0 -63
  20. {structtype-0.11.0 → structtype-0.12.0}/.opencode/opencode.json +0 -0
  21. {structtype-0.11.0 → structtype-0.12.0}/.opencode/plugins/graphify.js +0 -0
  22. {structtype-0.11.0 → structtype-0.12.0}/LICENSE +0 -0
  23. {structtype-0.11.0 → structtype-0.12.0}/MANIFEST.in +0 -0
  24. {structtype-0.11.0 → structtype-0.12.0}/README.md +0 -0
  25. {structtype-0.11.0 → structtype-0.12.0}/pyproject.toml +0 -0
  26. {structtype-0.11.0 → structtype-0.12.0}/setup.cfg +0 -0
  27. {structtype-0.11.0 → structtype-0.12.0}/setup.py +0 -0
  28. {structtype-0.11.0 → structtype-0.12.0}/src/structtype/__init__.py +0 -0
  29. {structtype-0.11.0 → structtype-0.12.0}/src/structtype/__init__.pyi +0 -0
  30. {structtype-0.11.0 → structtype-0.12.0}/src/structtype/_config.py +0 -0
  31. {structtype-0.11.0 → structtype-0.12.0}/src/structtype/_inspect.py +0 -0
  32. {structtype-0.11.0 → structtype-0.12.0}/src/structtype/_json_schema.py +0 -0
  33. {structtype-0.11.0 → structtype-0.12.0}/src/structtype/_utils.py +0 -0
  34. {structtype-0.11.0 → structtype-0.12.0}/src/structtype/atof.h +0 -0
  35. {structtype-0.11.0 → structtype-0.12.0}/src/structtype/atof_consts.h +0 -0
  36. {structtype-0.11.0 → structtype-0.12.0}/src/structtype/common.h +0 -0
  37. {structtype-0.11.0 → structtype-0.12.0}/src/structtype/itoa.h +0 -0
  38. {structtype-0.11.0 → structtype-0.12.0}/src/structtype/py.typed +0 -0
  39. {structtype-0.11.0 → structtype-0.12.0}/src/structtype/ryu.h +0 -0
  40. {structtype-0.11.0 → structtype-0.12.0}/src/structtype.egg-info/dependency_links.txt +0 -0
  41. {structtype-0.11.0 → structtype-0.12.0}/src/structtype.egg-info/top_level.txt +0 -0
  42. {structtype-0.11.0 → structtype-0.12.0}/tests/__init__.py +0 -0
  43. {structtype-0.11.0 → structtype-0.12.0}/tests/conftest.py +0 -0
  44. {structtype-0.11.0 → structtype-0.12.0}/tests/test_JSONTestSuite.py +0 -0
  45. {structtype-0.11.0 → structtype-0.12.0}/tests/test_check.py +0 -0
  46. {structtype-0.11.0 → structtype-0.12.0}/tests/test_constraints.py +0 -0
  47. {structtype-0.11.0 → structtype-0.12.0}/tests/test_cpylint.py +0 -0
  48. {structtype-0.11.0 → structtype-0.12.0}/tests/test_inspect.py +0 -0
  49. {structtype-0.11.0 → structtype-0.12.0}/tests/test_msgspec.py +0 -0
  50. {structtype-0.11.0 → structtype-0.12.0}/tests/test_pydantic.py +0 -0
  51. {structtype-0.11.0 → structtype-0.12.0}/tests/test_roundtrip.py +0 -0
  52. {structtype-0.11.0 → structtype-0.12.0}/tests/test_schema.py +0 -0
  53. {structtype-0.11.0 → structtype-0.12.0}/tests/test_struct.py +0 -0
  54. {structtype-0.11.0 → structtype-0.12.0}/tests/test_struct_meta.py +0 -0
  55. {structtype-0.11.0 → structtype-0.12.0}/tests/test_typecheck.py +0 -0
  56. {structtype-0.11.0 → structtype-0.12.0}/tests/test_utils.py +0 -0
  57. {structtype-0.11.0 → structtype-0.12.0}/tests/typecheck/api.py +0 -0
  58. {structtype-0.11.0 → structtype-0.12.0}/tests/typecheck/basic.py +0 -0
  59. {structtype-0.11.0 → structtype-0.12.0}/tests/typecheck/fields.py +0 -0
  60. {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`, ~19K lines).
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
- All commands go through `make`.
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
- | All checks | `make check` |
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` — custom-type load/dump codecs
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.0 cibuildwheel
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
- lcov --capture --directory build --output-file coverage-c.info
35
- lcov --extract coverage-c.info "*/src/structtype/*" --output-file coverage-c.info
36
- lcov --summary coverage-c.info
37
- genhtml coverage-c.info --output-directory htmlcov-c >/dev/null
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 -e .
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,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structtype
3
- Version: 0.11.0
3
+ Version: 0.12.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,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), unmatched fields cause an error.
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), unmatched fields cause an error.
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 with attributes instead of dict keys.
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,