structtype 0.3.0__tar.gz → 0.5.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 (52) hide show
  1. structtype-0.5.0/.opencode/opencode.json +6 -0
  2. structtype-0.5.0/.opencode/plugins/graphify.js +30 -0
  3. {structtype-0.3.0 → structtype-0.5.0}/AGENTS.md +15 -2
  4. {structtype-0.3.0 → structtype-0.5.0}/Makefile +27 -3
  5. {structtype-0.3.0 → structtype-0.5.0}/PKG-INFO +8 -2
  6. {structtype-0.3.0 → structtype-0.5.0}/README.md +7 -1
  7. {structtype-0.3.0 → structtype-0.5.0}/pyproject.toml +1 -2
  8. {structtype-0.3.0 → structtype-0.5.0}/setup.py +9 -2
  9. {structtype-0.3.0 → structtype-0.5.0}/src/structtype/__init__.py +2 -0
  10. {structtype-0.3.0 → structtype-0.5.0}/src/structtype/__init__.pyi +30 -26
  11. {structtype-0.3.0 → structtype-0.5.0}/src/structtype/_adapter.py +59 -23
  12. {structtype-0.3.0 → structtype-0.5.0}/src/structtype/_core.c +1134 -830
  13. {structtype-0.3.0 → structtype-0.5.0}/src/structtype/_inspect.py +21 -8
  14. {structtype-0.3.0 → structtype-0.5.0}/src/structtype/_version.py +2 -2
  15. {structtype-0.3.0 → structtype-0.5.0}/src/structtype.egg-info/PKG-INFO +8 -2
  16. {structtype-0.3.0 → structtype-0.5.0}/src/structtype.egg-info/SOURCES.txt +20 -18
  17. {structtype-0.3.0/tests/unit → structtype-0.5.0/tests}/conftest.py +10 -0
  18. structtype-0.5.0/tests/test_adapter.py +346 -0
  19. {structtype-0.3.0/tests/unit → structtype-0.5.0/tests}/test_check.py +6 -7
  20. {structtype-0.3.0/tests/unit → structtype-0.5.0/tests}/test_constraints.py +7 -1
  21. structtype-0.5.0/tests/test_free_threading.py +167 -0
  22. {structtype-0.3.0/tests/unit → structtype-0.5.0/tests}/test_inspect.py +9 -1
  23. {structtype-0.3.0/tests/unit → structtype-0.5.0/tests}/test_json.py +347 -44
  24. structtype-0.5.0/tests/test_memory.py +81 -0
  25. {structtype-0.3.0/tests/unit → structtype-0.5.0/tests}/test_schema.py +11 -0
  26. {structtype-0.3.0/tests/unit → structtype-0.5.0/tests}/test_struct.py +103 -40
  27. {structtype-0.3.0 → structtype-0.5.0}/uv.lock +299 -193
  28. structtype-0.3.0/tests/unit/__init__.py +0 -0
  29. structtype-0.3.0/tests/unit/test_adapter.py +0 -122
  30. structtype-0.3.0/tests/unit/test_free_threading.py +0 -59
  31. {structtype-0.3.0 → structtype-0.5.0}/LICENSE +0 -0
  32. {structtype-0.3.0 → structtype-0.5.0}/MANIFEST.in +0 -0
  33. {structtype-0.3.0 → structtype-0.5.0}/setup.cfg +0 -0
  34. {structtype-0.3.0 → structtype-0.5.0}/src/structtype/_json_schema.py +0 -0
  35. {structtype-0.3.0 → structtype-0.5.0}/src/structtype/_utils.py +0 -0
  36. {structtype-0.3.0 → structtype-0.5.0}/src/structtype/atof.h +0 -0
  37. {structtype-0.3.0 → structtype-0.5.0}/src/structtype/atof_consts.h +0 -0
  38. {structtype-0.3.0 → structtype-0.5.0}/src/structtype/common.h +0 -0
  39. {structtype-0.3.0 → structtype-0.5.0}/src/structtype/itoa.h +0 -0
  40. {structtype-0.3.0 → structtype-0.5.0}/src/structtype/py.typed +0 -0
  41. {structtype-0.3.0 → structtype-0.5.0}/src/structtype/ryu.h +0 -0
  42. {structtype-0.3.0 → structtype-0.5.0}/src/structtype.egg-info/dependency_links.txt +0 -0
  43. {structtype-0.3.0 → structtype-0.5.0}/src/structtype.egg-info/top_level.txt +0 -0
  44. {structtype-0.3.0 → structtype-0.5.0}/tests/__init__.py +0 -0
  45. {structtype-0.3.0/tests/unit → structtype-0.5.0/tests}/test_JSONTestSuite.py +0 -0
  46. {structtype-0.3.0/tests/unit → structtype-0.5.0/tests}/test_cpylint.py +0 -0
  47. {structtype-0.3.0/tests/unit → structtype-0.5.0/tests}/test_msgspec.py +0 -0
  48. {structtype-0.3.0/tests/unit → structtype-0.5.0/tests}/test_pydantic.py +0 -0
  49. {structtype-0.3.0/tests/unit → structtype-0.5.0/tests}/test_raw.py +0 -0
  50. {structtype-0.3.0/tests/unit → structtype-0.5.0/tests}/test_struct_meta.py +0 -0
  51. {structtype-0.3.0/tests/unit → structtype-0.5.0/tests}/test_utils.py +0 -0
  52. {structtype-0.3.0/tests/unit → structtype-0.5.0/tests}/utils.py +0 -0
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "plugin": [
4
+ ".opencode/plugins/graphify.js"
5
+ ]
6
+ }
@@ -0,0 +1,30 @@
1
+ // graphify OpenCode plugin
2
+ // Injects a knowledge graph reminder before bash tool calls when the graph exists.
3
+ //
4
+ // IMPORTANT: keep the reminder string free of backticks and $(...) constructs.
5
+ // The hook prepends `echo "<reminder>" && <cmd>` to the user's bash command;
6
+ // backticks inside the double-quoted echo trigger bash command substitution,
7
+ // which both corrupts tool output and silently executes the very graphify
8
+ // command we are only suggesting. Plain words render fine in opencode's TUI.
9
+ import { existsSync } from "fs";
10
+ import { join } from "path";
11
+
12
+ export const GraphifyPlugin = async ({ directory }) => {
13
+ let reminded = false;
14
+
15
+ return {
16
+ "tool.execute.before": async (input, output) => {
17
+ if (reminded) return;
18
+ if (!existsSync(join(directory, "graphify-out", "graph.json"))) return;
19
+
20
+ if (input.tool === "bash") {
21
+ // ';' not '&&' — Windows PowerShell 5.1 rejects '&&' as a statement
22
+ // separator, breaking the first bash command of the session (#1646).
23
+ output.args.command =
24
+ 'echo "[graphify] knowledge graph at graphify-out/. For focused questions, run graphify query with your question (scoped subgraph, usually much smaller than GRAPH_REPORT.md) instead of grepping raw files. Read GRAPH_REPORT.md only for broad architecture context." ; ' +
25
+ output.args.command;
26
+ reminded = true;
27
+ }
28
+ },
29
+ };
30
+ };
@@ -19,7 +19,7 @@ All commands go through `make`.
19
19
  | Task | Command |
20
20
  |---|---|
21
21
  | Unit tests (reinstall + last-failed) | `make test-lf` |
22
- | Targeted tests | `uv run --reinstall pytest tests/unit/test_json.py -k test_something` |
22
+ | Targeted tests | `uv run --reinstall pytest tests/test_json.py -k test_something` |
23
23
  | Coverage | `make test-cov` |
24
24
  | Build docs | `make docs` |
25
25
  | Format | `make format` |
@@ -35,6 +35,7 @@ All commands go through `make`.
35
35
  - C code uses `ms_`/`MS_` prefix
36
36
  - Type stubs (`.pyi`) alongside public modules
37
37
  - Sentinel values: `NODEFAULT`, `UNSET`, `_NoDefault`, `UnsetType`
38
+ - never do git commit
38
39
 
39
40
  ## Key API
40
41
 
@@ -48,7 +49,6 @@ All commands go through `make`.
48
49
 
49
50
  - `obj.struct_dump_json(*, enc_hook=None, decimal_format=None, uuid_format=None, order=None)` — serialize to JSON bytes
50
51
  - `obj.struct_dump()` — convert to built-in Python types (uses `encode_name` for keys)
51
- - `obj.struct_force_setattr(name, value)` — set attr on frozen struct
52
52
  - `obj.struct_validate_self()` — validate field values against types + constraints
53
53
  - `cls.struct_validate_json(buf, *, strict=True, dec_hook=None)` — deserialize from JSON
54
54
  - `cls.struct_validate(obj, *, strict=True, from_attributes=False, dec_hook=None)` — convert built-in types to struct
@@ -62,3 +62,16 @@ Struct instances support the mapping protocol:
62
62
  ## Gotchas
63
63
 
64
64
  - `make test-lf` reinstalls the C extension before running (last-failed first)
65
+
66
+ ## graphify
67
+
68
+ This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
69
+
70
+ When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else.
71
+
72
+ Rules:
73
+ - 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.
74
+ - 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.
75
+ - If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
76
+ - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
77
+ - After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
@@ -13,13 +13,17 @@ docs: ## build docs
13
13
  rm -rf .doctrees site
14
14
  uv run --group docs sphinx-build -d .doctrees -b html docs site --fail-on-warning
15
15
 
16
+ .PHONY: docs-serve
17
+ docs-serve: ## Open built docs in browser
18
+ uv run python -m webbrowser site/index.html
19
+
16
20
  .PHONY: wheels
17
21
  wheels: ## build wheels
18
- uvx --from cibuildwheel==4.1.1 cibuildwheel
22
+ uvx --from cibuildwheel==4.2.0 cibuildwheel
19
23
 
20
24
  ##@ Quality
21
25
  .PHONY: test-cov
22
- test-cov: ## Run tests with coverage
26
+ test-cov: build ## Run tests with coverage
23
27
  uv run pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=structtype
24
28
 
25
29
  .PHONY: test-lf
@@ -33,9 +37,29 @@ test-doc: ## Run doctests
33
37
  .PHONY: test-all
34
38
  test-all: ## Run tests in all supporte Python versions
35
39
  for py_v in $(PY_VERSIONS); do \
36
- uv run --isolated -p $$py_v pytest; \
40
+ uv run --isolated --reinstall-package structtype -p $$py_v pytest; \
37
41
  done
38
42
 
43
+ UNAME_S := $(shell uname -s)
44
+ ifeq ($(UNAME_S),Darwin)
45
+ ASAN_RUNTIME := $(shell clang --print-file-name=libclang_rt.asan_osx_dynamic.dylib)
46
+ SANITIZE_PRELOAD := DYLD_INSERT_LIBRARIES=$(ASAN_RUNTIME)
47
+ else
48
+ ASAN_RUNTIME := $(shell gcc --print-file-name=libasan.so)
49
+ SANITIZE_PRELOAD := LD_PRELOAD=$(ASAN_RUNTIME)
50
+ endif
51
+
52
+ DEBUG_PY ?= 3.14+debug
53
+ DEBUG_VENV = .venv-debug
54
+
55
+ .PHONY: test-debug
56
+ test-debug: ## Build core with Py_DEBUG + ASan/UBSan + debug allocator and run all tests
57
+ uv venv --clear --python $(DEBUG_PY) $(DEBUG_VENV)
58
+ STRUCTTYPE_SANITIZE=1 uv pip install --python $(DEBUG_VENV) --reinstall --no-cache --group dev -e .
59
+ $(SANITIZE_PRELOAD) STRUCTTYPE_ASAN_RUNTIME=$(ASAN_RUNTIME) ASAN_OPTIONS=detect_leaks=0 \
60
+ PYTHONMALLOC=debug PYTHONFAULTHANDLER=1 PYTHONDEVMODE=1 \
61
+ $(DEBUG_VENV)/bin/python -m pytest
62
+
39
63
  .PHONY: check
40
64
  check: ## Run all checks
41
65
  -uvx ty check ${SOURCE_DIR}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structtype
3
- Version: 0.3.0
3
+ Version: 0.5.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
@@ -72,7 +72,13 @@ Full documentation is available at **https://tds333.github.io/structtype/**.
72
72
 
73
73
  ## Benchmarks
74
74
 
75
- structtype is as fast as msgspec and about 3-5x faster than pydantic.
75
+ structtype is as fast as msgspec and about 3-5x faster than pydantic. See the
76
+ [benchmarks page](https://tds333.github.io/structtype/benchmarks.html) for the full numbers.
77
+
78
+ ## Links
79
+
80
+ - [Changelog](https://tds333.github.io/structtype/changelog.html)
81
+ - [Source code](https://github.com/tds333/structtype)
76
82
 
77
83
  ## License
78
84
 
@@ -49,7 +49,13 @@ Full documentation is available at **https://tds333.github.io/structtype/**.
49
49
 
50
50
  ## Benchmarks
51
51
 
52
- structtype is as fast as msgspec and about 3-5x faster than pydantic.
52
+ structtype is as fast as msgspec and about 3-5x faster than pydantic. See the
53
+ [benchmarks page](https://tds333.github.io/structtype/benchmarks.html) for the full numbers.
54
+
55
+ ## Links
56
+
57
+ - [Changelog](https://tds333.github.io/structtype/changelog.html)
58
+ - [Source code](https://github.com/tds333/structtype)
53
59
 
54
60
  ## License
55
61
 
@@ -84,8 +84,7 @@ editable_mode = "compat"
84
84
  [tool.cibuildwheel]
85
85
  build = "cp3*"
86
86
  skip = ["pp*", "*-manylinux_i686", "*-musllinux*", "*-win32"]
87
- enable = ["cpython-prerelease"]
88
87
  test-command = "pip install pytest>=9 && pytest {project}/tests"
89
88
 
90
89
  [tool.ruff]
91
- target-version = "py310"
90
+ target-version = "py310"
@@ -27,8 +27,15 @@ DEBUG = os.environ.get("STRUCTTYPE_DEBUG", SANITIZE or COVERAGE)
27
27
  extra_compile_args = []
28
28
  extra_link_args = []
29
29
  if SANITIZE:
30
- extra_compile_args.extend(["-fsanitize=address", "-fsanitize=undefined"])
31
- extra_link_args.extend(["-lasan", "-lubsan"])
30
+ extra_compile_args.extend(
31
+ [
32
+ "-fsanitize=address",
33
+ "-fsanitize=undefined",
34
+ "-fsanitize=signed-integer-overflow",
35
+ ]
36
+ )
37
+ if sys.platform != "darwin":
38
+ extra_link_args.extend(["-lasan", "-lubsan"])
32
39
  if COVERAGE:
33
40
  extra_compile_args.append("--coverage")
34
41
  extra_link_args.append("-lgcov")
@@ -1,9 +1,11 @@
1
1
  from ._adapter import StrAdapter, StructAdapter
2
2
  from ._core import (
3
+ ALL_BUILTIN_TYPES,
3
4
  NODEFAULT,
4
5
  UNSET,
5
6
  DecodeError,
6
7
  EncodeError,
8
+ Factory,
7
9
  Field,
8
10
  Raw,
9
11
  Struct,
@@ -68,7 +68,7 @@ class _NoDefault(enum.Enum):
68
68
 
69
69
  NODEFAULT: Final = _NoDefault.NODEFAULT
70
70
 
71
- @dataclass_transform(field_specifiers=(Field,))
71
+ @dataclass_transform(field_specifiers=("Field",))
72
72
  class Struct(metaclass=StructMeta):
73
73
  __struct_fields__: ClassVar[tuple[str, ...]]
74
74
  __struct_config__: ClassVar[StructConfig]
@@ -110,22 +110,19 @@ class Struct(metaclass=StructMeta):
110
110
  def struct_dump_json(
111
111
  self,
112
112
  *,
113
- enc_hook: Callable[[Any], Any] | None = None,
114
113
  decimal_format: Literal["string", "number"]
115
114
  | Callable[[Any], Any]
116
115
  | None = None,
117
116
  uuid_format: Literal["canonical", "hex"] | None = None,
118
- order: Literal["deterministic", "sorted"] | None = None,
117
+ sort_keys: bool = False,
119
118
  ) -> bytes: ...
120
119
  def struct_dump(
121
120
  self,
122
121
  *,
123
- enc_hook: Callable[[Any], Any] | None = None,
124
- order: Literal["deterministic", "sorted"] | None = None,
122
+ sort_keys: bool = False,
125
123
  str_keys: bool = False,
126
124
  builtin_types: Iterable[type] | None = None,
127
125
  ) -> dict[str, Any] | list[Any]: ...
128
- def struct_force_setattr(self, name: str, value: Any) -> None: ...
129
126
  def struct_validate_self(self) -> None: ...
130
127
  @classmethod
131
128
  def struct_validate_json(
@@ -133,7 +130,6 @@ class Struct(metaclass=StructMeta):
133
130
  buf: str | Buffer,
134
131
  *,
135
132
  strict: bool = True,
136
- dec_hook: Callable[[type[Any], Any], Any] | None = None,
137
133
  ) -> _T: ...
138
134
  @classmethod
139
135
  def struct_validate(
@@ -142,7 +138,6 @@ class Struct(metaclass=StructMeta):
142
138
  *,
143
139
  strict: bool = True,
144
140
  from_attributes: bool = False,
145
- dec_hook: Callable[[type[Any], Any], Any] | None = None,
146
141
  ) -> _T: ...
147
142
 
148
143
  # Lie and say `Raw` is a subclass of `bytes`, so mypy will accept it in most
@@ -158,6 +153,11 @@ class Raw(bytes):
158
153
  #: We can't represent this in types, only via a name:
159
154
  _NonNegativeInt: TypeAlias = int
160
155
 
156
+ @final
157
+ class Factory:
158
+ def __init__(self, factory: Callable[[], Any]) -> None: ...
159
+ factory: Final[Callable[[], Any]]
160
+
161
161
  @final
162
162
  class Field:
163
163
  # Numeric:
@@ -168,13 +168,14 @@ class Field:
168
168
  gt: int | float | None = None,
169
169
  lt: int | float | None = None,
170
170
  multiple_of: int | float | None = None,
171
- default: Any = NODEFAULT,
172
- default_factory: Callable[[], Any] | None = None,
173
171
  alias: str | None = None,
174
172
  title: str | None = None,
175
173
  description: str | None = None,
176
174
  json_schema_extra: dict[str, Any] | None = None,
177
175
  examples: list[Any] | None = None,
176
+ deprecated: bool | None = None,
177
+ dump: Callable[[Any], Any] | None = None,
178
+ validate: Callable[[Any], Any] | None = None,
178
179
  ) -> None: ...
179
180
  @overload
180
181
  def __init__(
@@ -183,13 +184,14 @@ class Field:
183
184
  gt: int | float | None = None,
184
185
  le: int | float | None = None,
185
186
  multiple_of: int | float | None = None,
186
- default: Any = NODEFAULT,
187
- default_factory: Callable[[], Any] | None = None,
188
187
  alias: str | None = None,
189
188
  title: str | None = None,
190
189
  description: str | None = None,
191
190
  json_schema_extra: dict[str, Any] | None = None,
192
191
  examples: list[Any] | None = None,
192
+ deprecated: bool | None = None,
193
+ dump: Callable[[Any], Any] | None = None,
194
+ validate: Callable[[Any], Any] | None = None,
193
195
  ) -> None: ...
194
196
  @overload
195
197
  def __init__(
@@ -198,13 +200,14 @@ class Field:
198
200
  ge: int | float | None = None,
199
201
  lt: int | float | None = None,
200
202
  multiple_of: int | float | None = None,
201
- default: Any = NODEFAULT,
202
- default_factory: Callable[[], Any] | None = None,
203
203
  alias: str | None = None,
204
204
  title: str | None = None,
205
205
  description: str | None = None,
206
206
  json_schema_extra: dict[str, Any] | None = None,
207
207
  examples: list[Any] | None = None,
208
+ deprecated: bool | None = None,
209
+ dump: Callable[[Any], Any] | None = None,
210
+ validate: Callable[[Any], Any] | None = None,
208
211
  ) -> None: ...
209
212
  @overload
210
213
  def __init__(
@@ -213,13 +216,14 @@ class Field:
213
216
  ge: int | float | None = None,
214
217
  le: int | float | None = None,
215
218
  multiple_of: int | float | None = None,
216
- default: Any = NODEFAULT,
217
- default_factory: Callable[[], Any] | None = None,
218
219
  alias: str | None = None,
219
220
  title: str | None = None,
220
221
  description: str | None = None,
221
222
  json_schema_extra: dict[str, Any] | None = None,
222
223
  examples: list[Any] | None = None,
224
+ deprecated: bool | None = None,
225
+ dump: Callable[[Any], Any] | None = None,
226
+ validate: Callable[[Any], Any] | None = None,
223
227
  ) -> None: ...
224
228
  # Other (string/datetime):
225
229
  @overload
@@ -230,16 +234,15 @@ class Field:
230
234
  min_length: _NonNegativeInt | None = None,
231
235
  max_length: _NonNegativeInt | None = None,
232
236
  tz: bool | None = None,
233
- default: Any = NODEFAULT,
234
- default_factory: Callable[[], Any] | None = None,
235
237
  alias: str | None = None,
236
238
  title: str | None = None,
237
239
  description: str | None = None,
238
240
  json_schema_extra: dict[str, Any] | None = None,
239
241
  examples: list[Any] | None = None,
242
+ deprecated: bool | None = None,
243
+ dump: Callable[[Any], Any] | None = None,
244
+ validate: Callable[[Any], Any] | None = None,
240
245
  ) -> None: ...
241
- default: Final[Any]
242
- default_factory: Final[Callable[[], Any] | None]
243
246
  alias: Final[str | None]
244
247
  gt: Final[int | float | None]
245
248
  ge: Final[int | float | None]
@@ -253,7 +256,10 @@ class Field:
253
256
  title: Final[str | None]
254
257
  description: Final[str | None]
255
258
  examples: Final[list[Any] | None]
259
+ deprecated: Final[bool | None]
256
260
  json_schema_extra: Final[dict[str, Any] | None]
261
+ dump: Final[Callable[[Any], Any] | None]
262
+ validate: Final[Callable[[Any], Any] | None]
257
263
  def __rich_repr__(self) -> list[tuple[str, Any]]: ...
258
264
 
259
265
  class StructConfig:
@@ -309,33 +315,29 @@ class StructAdapter:
309
315
  buf: str | Buffer,
310
316
  *,
311
317
  strict: bool = True,
312
- dec_hook: Callable[[type[Any], Any], Any] | None = None,
313
318
  ) -> Any: ...
314
319
  def struct_dump_json(
315
320
  self,
316
321
  obj: Any,
317
322
  *,
318
- enc_hook: Callable[[Any], Any] | None = None,
319
323
  decimal_format: Literal["string", "number"]
320
324
  | Callable[[Any], Any]
321
325
  | None = None,
322
326
  uuid_format: Literal["canonical", "hex"] | None = None,
323
- order: Literal["deterministic", "sorted"] | None = None,
327
+ sort_keys: bool = False,
324
328
  ) -> bytes: ...
325
329
  def struct_validate(
326
330
  self,
327
331
  obj: Any,
328
332
  *,
329
333
  strict: bool = True,
330
- dec_hook: Callable[[type[Any], Any], Any] | None = None,
331
334
  from_attributes: bool = False,
332
335
  ) -> Any: ...
333
336
  def struct_dump(
334
337
  self,
335
338
  obj: Any,
336
339
  *,
337
- enc_hook: Callable[[Any], Any] | None = None,
338
- order: Literal["deterministic", "sorted"] | None = None,
340
+ sort_keys: bool = False,
339
341
  str_keys: bool = False,
340
342
  builtin_types: Iterable[type] | None = None,
341
343
  ) -> Any: ...
@@ -348,3 +350,5 @@ class DecodeError(ValueError): ...
348
350
  class ValidationError(ValueError): ...
349
351
 
350
352
  __version__: str
353
+
354
+ ALL_BUILTIN_TYPES: tuple[type, ...]
@@ -1,6 +1,32 @@
1
- from typing import Any
2
-
3
- from ._core import _dump, _json_decode, _json_encode, _validate
1
+ from typing import Any, get_args
2
+
3
+ from ._core import ( # type: ignore
4
+ Field as _Field,
5
+ )
6
+ from ._core import (
7
+ _dump,
8
+ _json_decode,
9
+ _json_encode,
10
+ _validate,
11
+ )
12
+
13
+
14
+ def _has_codec(ann):
15
+ """True if the annotation carries a ``Field`` with ``dump``/``validate``."""
16
+ metadata = getattr(ann, "__metadata__", None)
17
+ if metadata is not None:
18
+ for meta in metadata:
19
+ if isinstance(meta, _Field) and (
20
+ meta.dump is not None or meta.validate is not None
21
+ ):
22
+ return True
23
+ supertype = getattr(ann, "__supertype__", None) # NewType
24
+ if supertype is not None and _has_codec(supertype):
25
+ return True
26
+ value = getattr(ann, "__value__", None) # PEP 695 type alias
27
+ if value is not None and _has_codec(value):
28
+ return True
29
+ return any(_has_codec(arg) for arg in get_args(ann))
4
30
 
5
31
 
6
32
  class StructAdapter:
@@ -9,6 +35,11 @@ class StructAdapter:
9
35
  Useful when you want to validate or serialize plain Python types
10
36
  (e.g. ``list[int]``) without defining a full ``Struct`` subclass.
11
37
 
38
+ ``Field(dump=...)`` / ``Field(validate=...)`` codecs are not supported on
39
+ ``StructAdapter`` — annotations carrying one are rejected. Implement the
40
+ ``struct_dump`` / ``struct_validate`` protocol methods on the custom type,
41
+ or use a ``Struct``.
42
+
12
43
  >>> from structtype import StructAdapter
13
44
  >>> adapter = StructAdapter(list[int])
14
45
  >>> adapter.struct_validate_json(b"[1, 2, 3]")
@@ -18,9 +49,15 @@ class StructAdapter:
18
49
  __slots__ = ("_type",)
19
50
 
20
51
  def __init__(self, type: Any):
52
+ if _has_codec(type):
53
+ raise TypeError(
54
+ "`Field(dump=...)`/`Field(validate=...)` codecs are not supported "
55
+ "on StructAdapter; define `struct_dump`/`struct_validate` methods "
56
+ "on the custom type, or use a `Struct` instead"
57
+ )
21
58
  self._type = type
22
59
 
23
- def struct_validate_json(self, buf, *, strict=True, dec_hook=None):
60
+ def struct_validate_json(self, buf, *, strict=True):
24
61
  """Validate JSON bytes and decode into the adapter's type.
25
62
 
26
63
  Parameters
@@ -29,13 +66,16 @@ class StructAdapter:
29
66
  The JSON message to decode.
30
67
  strict : bool, optional
31
68
  If True (default), unmatched fields cause an error.
32
- dec_hook : callable, optional
33
- A callback for customizing decoding of specific types.
34
69
  """
35
- return _json_decode(buf, type=self._type, strict=strict, dec_hook=dec_hook)
70
+ return _json_decode(buf, type=self._type, strict=strict)
36
71
 
37
72
  def struct_dump_json(
38
- self, obj, *, enc_hook=None, decimal_format=None, uuid_format=None, order=None
73
+ self,
74
+ obj,
75
+ *,
76
+ decimal_format=None,
77
+ uuid_format=None,
78
+ sort_keys=False,
39
79
  ):
40
80
  """Encode a validated object to JSON bytes.
41
81
 
@@ -43,26 +83,21 @@ class StructAdapter:
43
83
  ----------
44
84
  obj : Any
45
85
  A value to encode. Must match the adapter's type.
46
- enc_hook : callable, optional
47
- A callback for customizing encoding of specific types.
48
86
  decimal_format : str or callable, optional
49
87
  Controls how ``Decimal`` values are encoded.
50
88
  uuid_format : str, optional
51
89
  Controls how ``UUID`` values are encoded.
52
- order : str, optional
53
- Determines key ordering in JSON objects.
90
+ sort_keys : bool, optional
91
+ If True, sort dict keys and set elements for deterministic output.
54
92
  """
55
93
  return _json_encode(
56
94
  obj,
57
- enc_hook=enc_hook,
58
95
  decimal_format=decimal_format,
59
96
  uuid_format=uuid_format,
60
- order=order,
97
+ sort_keys=sort_keys,
61
98
  )
62
99
 
63
- def struct_validate(
64
- self, obj, *, strict=True, dec_hook=None, from_attributes=False
65
- ):
100
+ def struct_validate(self, obj, *, strict=True, from_attributes=False):
66
101
  """Validate a Python object against the adapter's type.
67
102
 
68
103
  Parameters
@@ -71,8 +106,6 @@ class StructAdapter:
71
106
  A Python object to validate and convert.
72
107
  strict : bool, optional
73
108
  If True (default), unmatched fields cause an error.
74
- dec_hook : callable, optional
75
- A callback for customizing decoding of specific types.
76
109
  from_attributes : bool, optional
77
110
  If True, accept objects with attributes instead of dict keys.
78
111
  """
@@ -80,20 +113,23 @@ class StructAdapter:
80
113
  obj,
81
114
  self._type,
82
115
  strict=strict,
83
- dec_hook=dec_hook,
84
116
  from_attributes=from_attributes,
85
117
  )
86
118
 
87
119
  def struct_dump(
88
- self, obj, *, enc_hook=None, order=None, str_keys=False, builtin_types=None
120
+ self,
121
+ obj,
122
+ *,
123
+ sort_keys=False,
124
+ str_keys=False,
125
+ builtin_types=None,
89
126
  ):
90
127
  """Convert a validated object to built-in Python types (``dict``, ``list``, etc.)."""
91
128
  return _dump(
92
129
  obj,
93
130
  builtin_types=builtin_types,
94
131
  str_keys=str_keys,
95
- enc_hook=enc_hook,
96
- order=order,
132
+ sort_keys=sort_keys,
97
133
  )
98
134
 
99
135