structtype 0.13.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 (64) hide show
  1. {structtype-0.13.0 → structtype-0.15.0}/AGENTS.md +18 -15
  2. {structtype-0.13.0 → structtype-0.15.0}/MANIFEST.in +0 -1
  3. {structtype-0.13.0 → structtype-0.15.0}/Makefile +23 -11
  4. {structtype-0.13.0 → structtype-0.15.0}/PKG-INFO +1 -1
  5. {structtype-0.13.0 → structtype-0.15.0}/src/structtype/__init__.pyi +21 -12
  6. {structtype-0.13.0 → structtype-0.15.0}/src/structtype/_adapter.py +20 -0
  7. {structtype-0.13.0 → structtype-0.15.0}/src/structtype/_config.py +2 -1
  8. {structtype-0.13.0 → structtype-0.15.0}/src/structtype/_core.c +1831 -787
  9. {structtype-0.13.0 → structtype-0.15.0}/src/structtype/_inspect.py +80 -4
  10. {structtype-0.13.0 → structtype-0.15.0}/src/structtype/_json_schema.py +55 -9
  11. {structtype-0.13.0 → structtype-0.15.0}/src/structtype/_version.py +2 -2
  12. {structtype-0.13.0 → structtype-0.15.0}/src/structtype/atof.h +5 -0
  13. {structtype-0.13.0 → structtype-0.15.0}/src/structtype.egg-info/PKG-INFO +1 -1
  14. {structtype-0.13.0 → structtype-0.15.0}/src/structtype.egg-info/SOURCES.txt +4 -2
  15. structtype-0.15.0/src/structtype.egg-info/scm_file_list.json +97 -0
  16. {structtype-0.13.0 → structtype-0.15.0}/tests/test_adapter.py +31 -0
  17. {structtype-0.13.0 → structtype-0.15.0}/tests/test_annotations.py +36 -13
  18. {structtype-0.13.0 → structtype-0.15.0}/tests/test_check.py +80 -4
  19. {structtype-0.13.0 → structtype-0.15.0}/tests/test_constraints.py +1 -1
  20. {structtype-0.13.0 → structtype-0.15.0}/tests/test_csv.py +45 -0
  21. {structtype-0.13.0 → structtype-0.15.0}/tests/test_free_threading.py +9 -72
  22. {structtype-0.13.0 → structtype-0.15.0}/tests/test_inspect.py +14 -0
  23. structtype-0.15.0/tests/test_ipnetwork.py +196 -0
  24. {structtype-0.13.0 → structtype-0.15.0}/tests/test_json.py +286 -0
  25. structtype-0.15.0/tests/test_protocols.py +181 -0
  26. structtype-0.15.0/tests/test_purepath.py +155 -0
  27. {structtype-0.13.0 → structtype-0.15.0}/tests/test_roundtrip.py +16 -0
  28. {structtype-0.13.0 → structtype-0.15.0}/tests/test_schema.py +102 -0
  29. {structtype-0.13.0 → structtype-0.15.0}/tests/test_struct.py +93 -19
  30. {structtype-0.13.0 → structtype-0.15.0}/tests/test_struct_meta.py +170 -7
  31. {structtype-0.13.0 → structtype-0.15.0}/tests/typecheck/basic.py +15 -0
  32. {structtype-0.13.0 → structtype-0.15.0}/tests/typecheck/fields.py +21 -0
  33. {structtype-0.13.0 → structtype-0.15.0}/uv.lock +401 -349
  34. structtype-0.13.0/.opencode/opencode.json +0 -6
  35. structtype-0.13.0/.opencode/plugins/graphify.js +0 -30
  36. {structtype-0.13.0 → structtype-0.15.0}/LICENSE +0 -0
  37. {structtype-0.13.0 → structtype-0.15.0}/README.md +0 -0
  38. {structtype-0.13.0 → structtype-0.15.0}/pyproject.toml +0 -0
  39. {structtype-0.13.0 → structtype-0.15.0}/setup.cfg +0 -0
  40. {structtype-0.13.0 → structtype-0.15.0}/setup.py +0 -0
  41. {structtype-0.13.0 → structtype-0.15.0}/src/structtype/__init__.py +0 -0
  42. {structtype-0.13.0 → structtype-0.15.0}/src/structtype/_utils.py +0 -0
  43. {structtype-0.13.0 → structtype-0.15.0}/src/structtype/atof_consts.h +0 -0
  44. {structtype-0.13.0 → structtype-0.15.0}/src/structtype/common.h +0 -0
  45. {structtype-0.13.0 → structtype-0.15.0}/src/structtype/itoa.h +0 -0
  46. {structtype-0.13.0 → structtype-0.15.0}/src/structtype/py.typed +0 -0
  47. {structtype-0.13.0 → structtype-0.15.0}/src/structtype/ryu.h +0 -0
  48. {structtype-0.13.0 → structtype-0.15.0}/src/structtype.egg-info/dependency_links.txt +0 -0
  49. {structtype-0.13.0 → structtype-0.15.0}/src/structtype.egg-info/top_level.txt +0 -0
  50. {structtype-0.13.0 → structtype-0.15.0}/tests/__init__.py +0 -0
  51. {structtype-0.13.0 → structtype-0.15.0}/tests/conftest.py +0 -0
  52. {structtype-0.13.0 → structtype-0.15.0}/tests/test_JSONTestSuite.py +0 -0
  53. {structtype-0.13.0 → structtype-0.15.0}/tests/test_attrs.py +0 -0
  54. {structtype-0.13.0 → structtype-0.15.0}/tests/test_cpylint.py +0 -0
  55. {structtype-0.13.0 → structtype-0.15.0}/tests/test_frozendict.py +0 -0
  56. {structtype-0.13.0 → structtype-0.15.0}/tests/test_memory.py +0 -0
  57. {structtype-0.13.0 → structtype-0.15.0}/tests/test_msgspec.py +0 -0
  58. {structtype-0.13.0 → structtype-0.15.0}/tests/test_pydantic.py +0 -0
  59. {structtype-0.13.0 → structtype-0.15.0}/tests/test_threading.py +0 -0
  60. {structtype-0.13.0 → structtype-0.15.0}/tests/test_typecheck.py +0 -0
  61. {structtype-0.13.0 → structtype-0.15.0}/tests/test_utils.py +0 -0
  62. {structtype-0.13.0 → structtype-0.15.0}/tests/typecheck/api.py +0 -0
  63. {structtype-0.13.0 → structtype-0.15.0}/tests/typecheck/types.py +0 -0
  64. {structtype-0.13.0 → structtype-0.15.0}/tests/utils.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`, ~21K lines).
6
+ Core is a monolithic C extension (`src/structtype/_core.c`, ~22K lines).
7
7
  No runtime deps.
8
8
 
9
9
  ## Setup
@@ -25,28 +25,41 @@ Use `make` targets where available. Targeted tests can be run directly with
25
25
  | Coverage (Python + C) | `make test-cov-c` |
26
26
  | Coverage (Python + C, all Pythons, merged) | `make test-cov-c-all` |
27
27
  | Tests in all supported Pythons | `make test-all` |
28
+ | Doctests | `make test-doc` |
29
+ | Build sdist + wheel | `make build` |
28
30
  | Build docs | `make docs` |
29
31
  | Format | `make format` |
30
32
  | Lint | `make ruff-check` |
31
33
  | Type check | `make type-check` |
32
34
  | Static checks | `make check` |
33
35
 
36
+ Benchmark targets: `make bench`, `make bench-validators`, `make bench-codecs`,
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`.
42
+
34
43
  ## Conventions
35
44
 
36
45
  - **88-char lines**, formatted with `ruff format`
37
- - `ruff check` with rules `E`, `F`, `I`, `W`
46
+ - `ruff check` with Ruff's default rule set (the project config sets only
47
+ `target-version` and isort `combine-as-imports`; line length is enforced by
48
+ `ruff format`, not `E501`)
38
49
  - Private modules/functions prefixed with `_`
39
50
  - C code uses `ms_`/`MS_` prefix
40
51
  - Type stubs (`.pyi`) alongside public modules
41
52
  - Sentinel values: `NODEFAULT`, `UNSET`, `_NoDefault`, `UnsetType`
42
53
  - never do git commit
43
54
  - check for performance regressions, speed is a goal
55
+ - no parallel benchmarks
56
+ - keep C code simple, fast, readable and compatible to freethreading
44
57
 
45
58
  ## Key API
46
59
 
47
60
  - `structtype.Struct` — base class with config options (frozen, tag, rename, etc.)
48
61
  - `structtype.Field` — field metadata (alias, title, description, examples, deprecated, json_schema_extra)
49
- - `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`
50
63
  - `structtype.Serializer` — load/dump codecs for supported custom and native types
51
64
  - `structtype.fields(type_or_instance)` — get FieldInfo tuple for a struct type/instance
52
65
  - `structtype._inspect.type_info()` / `multi_type_info()` — type introspection
@@ -59,6 +72,8 @@ Use `make` targets where available. Targeted tests can be run directly with
59
72
  - `obj.struct_check_types()` — validate field values against types + constraints (pure type-check, no conversion)
60
73
  - `cls.struct_validate_json(buf, *, strict=True)` — deserialize from JSON
61
74
  - `cls.struct_validate(obj, *, strict=True, from_attributes=False)` — convert built-in types to struct
75
+ - `obj.struct_dump_csv()` — encode one row as `list[str]` for `csv.writer.writerow()`; flat scalar fields only, positional
76
+ - `cls.struct_validate_csv(row, *, null_values=("",))` — decode one CSV row (sequence of cell strings); always lax
62
77
 
63
78
  ### Dict & Iteration Protocol
64
79
 
@@ -75,15 +90,3 @@ Struct instances support the mapping protocol:
75
90
  - 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
91
  - 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.
77
92
 
78
- ## graphify
79
-
80
- This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
81
-
82
- When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else.
83
-
84
- Rules:
85
- - For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
86
- - Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it.
87
- - If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
88
- - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
89
- - After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
@@ -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,21 +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
156
+
157
+ .PHONY: bench-strings
158
+ bench-strings: ## run long-string JSON codec benchmark
159
+ uv run -p $(BENCH_PYTHON) benchmarks/bench_strings.py
149
160
 
150
161
  ##@ Utility
151
162
  .PHONY: clean
@@ -161,6 +172,7 @@ clean: ## Delete all temporary files
161
172
  rm -rf htmlcov-c
162
173
  rm -f .coverage
163
174
  rm -f coverage-c.info coverage-c.info.*
175
+ rm -rf .venv*
164
176
 
165
177
  .PHONY: install
166
178
  install: install-uv ## Install virtual environment
@@ -187,4 +199,4 @@ update-python: ## Reinstall managed Python versions to latest release
187
199
 
188
200
  .PHONY: help
189
201
  help: ## Display this help
190
- @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.13.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