pytypehint 1.2.2__tar.gz → 1.3.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.
- {pytypehint-1.2.2 → pytypehint-1.3.0}/CHANGELOG.md +22 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/PKG-INFO +14 -13
- pytypehint-1.3.0/README.md +43 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/docs/contract.md +2 -1
- {pytypehint-1.2.2 → pytypehint-1.3.0}/docs/decode.md +1 -1
- {pytypehint-1.2.2 → pytypehint-1.3.0}/docs/design.md +1 -1
- pytypehint-1.3.0/docs/encode.md +74 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/docs/immutable.md +1 -1
- {pytypehint-1.2.2 → pytypehint-1.3.0}/docs/overview.md +5 -3
- {pytypehint-1.2.2 → pytypehint-1.3.0}/docs/vocabulary.md +2 -2
- {pytypehint-1.2.2 → pytypehint-1.3.0}/src/pytypehint/__init__.py +1 -1
- {pytypehint-1.2.2 → pytypehint-1.3.0}/src/pytypehint/contract.py +2 -41
- {pytypehint-1.2.2 → pytypehint-1.3.0}/src/pytypehint/signature.py +4 -1
- {pytypehint-1.2.2 → pytypehint-1.3.0}/src/pytypehint/structure.py +69 -0
- pytypehint-1.3.0/tests/test_encode.py +447 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_hardening_a.py +1 -1
- pytypehint-1.2.2/README.md +0 -42
- {pytypehint-1.2.2 → pytypehint-1.3.0}/.github/dependabot.yml +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/.github/workflows/ci.yml +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/.github/workflows/publish.yml +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/.gitignore +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/LICENSE +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/docs/atoms.md +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/docs/build.md +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/docs/defaults.md +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/docs/limits.md +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/docs/resolve.md +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/docs/tuples.md +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/pyproject.toml +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/src/pytypehint/atoms.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/src/pytypehint/bridge.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/src/pytypehint/errors.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/src/pytypehint/immutable.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/src/pytypehint/py.typed +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/src/pytypehint/shapes.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/src/pytypehint/utils.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/src/pytypehint/validation.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/conftest.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_annotated.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_annotated_hoisting_properties.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_atom_argument_validation.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_atom_table_reflection.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_atoms.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_bool.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_bridge.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_build.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_certification.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_collection_and_recursion_matrix.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_composition.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_contract.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_contract_graph.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_crossround_x.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_crossround_y.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_datetime.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_decode.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_decode_boundary.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_decode_routing.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_default_container_isolation.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_default_types.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_discriminated_wrapper.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_discriminator.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_documentation_contract.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_enum.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_equality.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_errors_pickle.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_extra.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_field_dataclass_surface.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_file_hint.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_final_restrictions.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_float.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_hardening_b.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_hardening_c.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_hardening_d.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_hardening_e.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_hardening_f.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_hardening_g.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_immutable.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_initvar_rejection.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_instance_validation_and_homonym_enums.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_int.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_list.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_literal.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_matches_no_option_notes.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_missing.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_none.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_none_transparency.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_numeric_constraint_validation.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_optional_toggle.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_portable_properties.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_presentation_atoms_surface.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_recursive.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_remat_runtime_failure.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_rematerialization.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_resolve.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_scalar_constraint_validation.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_scalar_option_identity.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_scalar_type_strictness.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_schema_model_invariants.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_shape_constraint_certification.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_shape_invariants.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_signature.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_signature_build.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_signature_field_rejection.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_single_pass_validation.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_str.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_string_and_literal_integration.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_struct.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_structured_errors.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_temporal_constraint_validation.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_time_second_precision.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_tuple.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_union_field_atoms.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_union_option_collisions.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_union_routing_matrix.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_unions.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_validate.py +0 -0
- {pytypehint-1.2.2 → pytypehint-1.3.0}/tests/test_wrapper_layer_composition.py +0 -0
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.0 - 2026-11-13
|
|
4
|
+
|
|
5
|
+
- Add `Struct.encode(instance)` and `Signature.encode(kwargs)`: write a valid
|
|
6
|
+
value as the portable tree `decode` reads. For a valid value,
|
|
7
|
+
`schema.build(schema.decode(schema.encode(value)))` returns it with its types,
|
|
8
|
+
also after a JSON transport.
|
|
9
|
+
- Name union options exactly as `to_dict()` already does for defaults; both now
|
|
10
|
+
share one writer. The portable format remains `v: 1` and exported documents
|
|
11
|
+
are unchanged.
|
|
12
|
+
- Encode does not validate. It rejects only what it cannot write: a wrong root
|
|
13
|
+
type, a value belonging to no option of its slot, a fixed tuple of the wrong
|
|
14
|
+
length and unknown signature arguments, each with its `path`.
|
|
15
|
+
- The public surface grows by no new names: both are methods on the compiled
|
|
16
|
+
schemas. Existing operations retain their behavior.
|
|
17
|
+
|
|
18
|
+
## 1.2.3 - 2026-09-29
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Documentation only: the README's Documentation list links each page on the
|
|
23
|
+
documentation site, so readers on GitHub and PyPI land there. The code is the same as 1.2.2.
|
|
24
|
+
|
|
3
25
|
## 1.2.2 - 2026-09-29
|
|
4
26
|
|
|
5
27
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pytypehint
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Compiles Python type hints into strict, inspectable, portable contracts
|
|
5
5
|
Project-URL: Documentation, https://offerrall.github.io/pytypehint/
|
|
6
6
|
Project-URL: Repository, https://github.com/offerrall/pytypehint
|
|
@@ -52,15 +52,16 @@ The full documentation is at https://offerrall.github.io/pytypehint/.
|
|
|
52
52
|
|
|
53
53
|
## Documentation
|
|
54
54
|
|
|
55
|
-
- [Overview](
|
|
56
|
-
- [Immutable models](
|
|
57
|
-
- [Types and API](
|
|
58
|
-
- [Atoms](
|
|
59
|
-
- [Build](
|
|
60
|
-
- [Resolve](
|
|
61
|
-
- [Decode](
|
|
62
|
-
- [
|
|
63
|
-
- [
|
|
64
|
-
- [
|
|
65
|
-
- [
|
|
66
|
-
- [
|
|
55
|
+
- [Overview](https://offerrall.github.io/pytypehint/): immutable models, portable data, field metadata and the five operations.
|
|
56
|
+
- [Immutable models](https://offerrall.github.io/pytypehint/immutable/): the `@immutable` contract, allowed fields, reuse and Python behavior.
|
|
57
|
+
- [Types and API](https://offerrall.github.io/pytypehint/vocabulary/): which hints compile to which shapes, and the public API.
|
|
58
|
+
- [Atoms](https://offerrall.github.io/pytypehint/atoms/): constraints and notation per shape, file names, layering and extras.
|
|
59
|
+
- [Build](https://offerrall.github.io/pytypehint/build/): validating and constructing instances or keyword arguments, union selection and errors.
|
|
60
|
+
- [Resolve](https://offerrall.github.io/pytypehint/resolve/): validated dictionaries with defaults filled at the current level.
|
|
61
|
+
- [Decode](https://offerrall.github.io/pytypehint/decode/): restoring portable values such as dates, enums and tuples before validation.
|
|
62
|
+
- [Encode](https://offerrall.github.io/pytypehint/encode/): writing valid values as portable data, the inverse of decode.
|
|
63
|
+
- [Tuples](https://offerrall.github.io/pytypehint/tuples/): fixed, variadic and empty tuples and the `Tuple` shape.
|
|
64
|
+
- [Defaults](https://offerrall.github.io/pytypehint/defaults/): certification and rematerialization of default values.
|
|
65
|
+
- [Portable contract](https://offerrall.github.io/pytypehint/contract/): the JSON-compatible schema document returned by `to_dict()`.
|
|
66
|
+
- [Limits](https://offerrall.github.io/pytypehint/limits/): rejected definitions, option identity and runtime limits.
|
|
67
|
+
- [Design](https://offerrall.github.io/pytypehint/design/): what the core owns, what consumers own, and why.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# pytypehint
|
|
2
|
+
|
|
3
|
+
Define data once with ordinary Python type hints and dataclasses. pytypehint
|
|
4
|
+
compiles them into strict schemas that validate input with exact types and no
|
|
5
|
+
implicit coercion, build nested dataclasses, and export a portable description
|
|
6
|
+
another process can read.
|
|
7
|
+
|
|
8
|
+
Use `@immutable` for deeply immutable models that validate themselves and reuse
|
|
9
|
+
already validated children. pytypehint is the schema and validation core of
|
|
10
|
+
[func-to-web](https://offerrall.github.io/func-to-web/).
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Annotated
|
|
15
|
+
from pytypehint import Min, struct_of
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Item:
|
|
19
|
+
name: str
|
|
20
|
+
quantity: Annotated[int, Min(1)] = 1
|
|
21
|
+
|
|
22
|
+
schema = struct_of(Item)
|
|
23
|
+
item = schema.build({"name": "Pen"}) # Item(name='Pen', quantity=1)
|
|
24
|
+
schema.build({"name": "Pen", "quantity": 0}) # SchemaValueError: quantity
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The full documentation is at https://offerrall.github.io/pytypehint/.
|
|
28
|
+
|
|
29
|
+
## Documentation
|
|
30
|
+
|
|
31
|
+
- [Overview](https://offerrall.github.io/pytypehint/): immutable models, portable data, field metadata and the five operations.
|
|
32
|
+
- [Immutable models](https://offerrall.github.io/pytypehint/immutable/): the `@immutable` contract, allowed fields, reuse and Python behavior.
|
|
33
|
+
- [Types and API](https://offerrall.github.io/pytypehint/vocabulary/): which hints compile to which shapes, and the public API.
|
|
34
|
+
- [Atoms](https://offerrall.github.io/pytypehint/atoms/): constraints and notation per shape, file names, layering and extras.
|
|
35
|
+
- [Build](https://offerrall.github.io/pytypehint/build/): validating and constructing instances or keyword arguments, union selection and errors.
|
|
36
|
+
- [Resolve](https://offerrall.github.io/pytypehint/resolve/): validated dictionaries with defaults filled at the current level.
|
|
37
|
+
- [Decode](https://offerrall.github.io/pytypehint/decode/): restoring portable values such as dates, enums and tuples before validation.
|
|
38
|
+
- [Encode](https://offerrall.github.io/pytypehint/encode/): writing valid values as portable data, the inverse of decode.
|
|
39
|
+
- [Tuples](https://offerrall.github.io/pytypehint/tuples/): fixed, variadic and empty tuples and the `Tuple` shape.
|
|
40
|
+
- [Defaults](https://offerrall.github.io/pytypehint/defaults/): certification and rematerialization of default values.
|
|
41
|
+
- [Portable contract](https://offerrall.github.io/pytypehint/contract/): the JSON-compatible schema document returned by `to_dict()`.
|
|
42
|
+
- [Limits](https://offerrall.github.io/pytypehint/limits/): rejected definitions, option identity and runtime limits.
|
|
43
|
+
- [Design](https://offerrall.github.io/pytypehint/design/): what the core owns, what consumers own, and why.
|
|
@@ -95,7 +95,8 @@ A field default taken from the document round-trips through
|
|
|
95
95
|
`schema.build(schema.decode(data))`, subject to the author's
|
|
96
96
|
[default purity requirements](defaults.md).
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
[`encode`](encode.md) writes any valid value in this spelling. To select an
|
|
99
|
+
option when writing portable values by other means:
|
|
99
100
|
|
|
100
101
|
| Slot | Encoding |
|
|
101
102
|
|---|---|
|
|
@@ -16,7 +16,7 @@ booking = schema.build(schema.decode({"day": "2026-08-08"}))
|
|
|
16
16
|
assert booking.day == date(2026, 8, 8)
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The caller parses JSON text. Decode accepts the resulting plain dictionaries,
|
|
19
|
+
[`encode`](encode.md) is the inverse operation. The caller parses JSON text. Decode accepts the resulting plain dictionaries,
|
|
20
20
|
lists, strings, numbers, booleans and `None`; it does not parse or emit JSON.
|
|
21
21
|
|
|
22
22
|
| Shape | Restoration |
|
|
@@ -10,7 +10,7 @@ checks requiring files, networks or application state:
|
|
|
10
10
|
| Task | Responsible layer |
|
|
11
11
|
|---|---|
|
|
12
12
|
| Define ordinary models | Python dataclasses and type hints |
|
|
13
|
-
| Inspect/export schemas, validate values, restore portable types, construct models | pytypehint |
|
|
13
|
+
| Inspect/export schemas, validate values, restore and write portable types, construct models | pytypehint |
|
|
14
14
|
| Parse JSON, coerce form/CLI input, render controls, collect multiple errors | Consumer |
|
|
15
15
|
| Call functions, check files, manage persistence/authentication | Application |
|
|
16
16
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Encode
|
|
2
|
+
|
|
3
|
+
`schema.encode(value)` writes a valid value as the portable tree that
|
|
4
|
+
[decode](decode.md) reads. It is the inverse of `decode`:
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import date
|
|
9
|
+
from pytypehint import struct_of
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Booking:
|
|
13
|
+
day: date
|
|
14
|
+
guests: int | float = 2
|
|
15
|
+
|
|
16
|
+
schema = struct_of(Booking)
|
|
17
|
+
booking = Booking(day=date(2026, 8, 8))
|
|
18
|
+
written = schema.encode(booking)
|
|
19
|
+
|
|
20
|
+
assert written == {"day": "2026-08-08", "guests": {"$type": "int", "$value": 2}}
|
|
21
|
+
assert schema.build(schema.decode(written)) == booking
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`Struct.encode` takes an exact instance of its dataclass. `Signature.encode`
|
|
25
|
+
takes a dictionary of arguments, such as the one `build` returns, and writes the
|
|
26
|
+
keys it is given; it fills no defaults and rejects unknown keys.
|
|
27
|
+
|
|
28
|
+
The result is a new tree of plain dictionaries, lists, strings, numbers, booleans
|
|
29
|
+
and `None`. The caller serializes it, for example with `json.dumps`. The value is
|
|
30
|
+
never modified, and the tree shares no container with it.
|
|
31
|
+
|
|
32
|
+
| Shape | Written as |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `Date` | `"YYYY-MM-DD"` |
|
|
35
|
+
| `Time` | `"HH:MM:SS"` |
|
|
36
|
+
| `EnumShape` | Member name |
|
|
37
|
+
| `List`, `Tuple` | Array |
|
|
38
|
+
| `Struct` | Object of its fields |
|
|
39
|
+
| Others | The value itself |
|
|
40
|
+
|
|
41
|
+
## Unions
|
|
42
|
+
|
|
43
|
+
An option is named exactly where its portable spelling collides with another
|
|
44
|
+
option of the same slot, following the [portable contract](contract.md#values-and-defaults):
|
|
45
|
+
dataclass variants carry an inline `$type`, other colliding options use the
|
|
46
|
+
`$type`/`$value` wrapper, and unambiguous options are written bare. These are the
|
|
47
|
+
spellings `to_dict()` uses for defaults.
|
|
48
|
+
|
|
49
|
+
Options sharing a Python type, such as `Annotated[list[str], Min(1)] | list[int]`,
|
|
50
|
+
are told apart by the first option that accepts the value.
|
|
51
|
+
|
|
52
|
+
## Round trip
|
|
53
|
+
|
|
54
|
+
For a value that passes validation:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
schema.build(schema.decode(schema.encode(value))) == value
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The same holds after `json.loads(json.dumps(...))`. Types are preserved: floats
|
|
61
|
+
stay floats, tuples stay tuples and each union value returns to its own option.
|
|
62
|
+
|
|
63
|
+
## Validation
|
|
64
|
+
|
|
65
|
+
Encode writes; it does not validate. It checks only what it needs in order to
|
|
66
|
+
write: the root type, that each value belongs to an option of its slot, and
|
|
67
|
+
fixed tuple lengths. Those failures raise `SchemaTypeError` or `SchemaValueError`
|
|
68
|
+
with the field names and indexes of the value in `path`.
|
|
69
|
+
|
|
70
|
+
A value outside its limits, such as a number below `Min`, is written faithfully,
|
|
71
|
+
and `build` rejects it when the tree is read back. Time fractions and offsets are
|
|
72
|
+
kept in the text for the same reason. Non-finite floats are written as they are
|
|
73
|
+
and have no standard JSON spelling. Cycles and excessive nesting can raise
|
|
74
|
+
`RecursionError`.
|
|
@@ -78,6 +78,6 @@ validation cannot undo their external effects. Use ordinary dataclasses and
|
|
|
78
78
|
Model classes and compiled contracts remain registered for the process lifetime;
|
|
79
79
|
define reusable models rather than generating a new class for each value.
|
|
80
80
|
|
|
81
|
-
`resolve`, `build`, `decode` and `to_dict` remain available through `struct_of`.
|
|
81
|
+
`resolve`, `build`, `decode`, `encode` and `to_dict` remain available through `struct_of`.
|
|
82
82
|
Their input rules and full recursive validation are unchanged; the reuse shortcut
|
|
83
83
|
belongs only to `@immutable` construction.
|
|
@@ -61,8 +61,8 @@ see [Immutable models](immutable.md) for the full contract, inheritance and defa
|
|
|
61
61
|
## Portable data and inspection
|
|
62
62
|
|
|
63
63
|
The same models work with the schema API. `decode` restores portable values such
|
|
64
|
-
as JSON arrays and discriminated unions,
|
|
65
|
-
UI or another process:
|
|
64
|
+
as JSON arrays and discriminated unions, `encode` writes them back, and `to_dict`
|
|
65
|
+
describes the schema for a UI or another process:
|
|
66
66
|
|
|
67
67
|
```python
|
|
68
68
|
from pytypehint import struct_of
|
|
@@ -77,6 +77,7 @@ loaded = schema.build(schema.decode({
|
|
|
77
77
|
}))
|
|
78
78
|
|
|
79
79
|
assert loaded == original
|
|
80
|
+
assert schema.build(schema.decode(schema.encode(original))) == original
|
|
80
81
|
contract = schema.to_dict() # Portable description for a UI or another process
|
|
81
82
|
```
|
|
82
83
|
|
|
@@ -95,11 +96,12 @@ assert size_field.label.value == "Brush size"
|
|
|
95
96
|
assert size_shape.max.value == 256
|
|
96
97
|
```
|
|
97
98
|
|
|
98
|
-
## The
|
|
99
|
+
## The five operations
|
|
99
100
|
|
|
100
101
|
| Method | Result |
|
|
101
102
|
|---|---|
|
|
102
103
|
| [`build(data)`](build.md) | Dataclass instance; `signature_of(fn)` returns kwargs for `fn(**kwargs)` |
|
|
103
104
|
| [`resolve(data)`](resolve.md) | Validated dictionary with missing defaults filled at this level |
|
|
104
105
|
| [`decode(data)`](decode.md) | Portable values restored to Python types |
|
|
106
|
+
| [`encode(value)`](encode.md) | A valid value written as portable data |
|
|
105
107
|
| [`to_dict()`](contract.md) | Portable schema description |
|
|
@@ -48,8 +48,8 @@ Everything public is exported from `pytypehint`:
|
|
|
48
48
|
`Extra`, `OptionalToggle`;
|
|
49
49
|
- `MISSING`.
|
|
50
50
|
|
|
51
|
-
`Struct` and `Signature` expose the [
|
|
52
|
-
`.build(data)`, `.resolve(data)`, `.decode(data)` and `.to_dict()`.
|
|
51
|
+
`Struct` and `Signature` expose the [five operations](overview.md#the-five-operations):
|
|
52
|
+
`.build(data)`, `.resolve(data)`, `.decode(data)`, `.encode(value)` and `.to_dict()`.
|
|
53
53
|
`Struct.fields` and `Signature.params` contain `Field` objects; `Field.shape`
|
|
54
54
|
contains the available shapes, and `Field.default` is `MISSING` when no default
|
|
55
55
|
exists. `Shape.option_id()` gives the discriminator identity.
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
from datetime import date, time
|
|
2
2
|
|
|
3
|
-
from pytypehint.errors import SchemaValueError
|
|
4
3
|
from pytypehint.shapes import (
|
|
5
4
|
Bool, Date, EnumShape, Float, Int, List, NoneShape, Str, Time, Tuple,
|
|
6
5
|
)
|
|
7
|
-
from pytypehint.structure import Struct,
|
|
6
|
+
from pytypehint.structure import Struct, _encode_options
|
|
8
7
|
from pytypehint.utils import MISSING
|
|
9
|
-
from pytypehint.validation import value_branch
|
|
10
8
|
|
|
11
9
|
_VERSION = 1
|
|
12
10
|
|
|
@@ -17,43 +15,6 @@ _TYPE_NAMES = {
|
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
|
|
20
|
-
def _portable(shapes, value):
|
|
21
|
-
shape = value_branch(shapes, value)
|
|
22
|
-
if shape is None:
|
|
23
|
-
raise SchemaValueError(
|
|
24
|
-
f"cannot describe {value!r}: it matches no option of this slot")
|
|
25
|
-
written = _written(shape, value)
|
|
26
|
-
|
|
27
|
-
if type(shape) is Struct:
|
|
28
|
-
if len([s for s in shapes if type(s) is Struct]) > 1:
|
|
29
|
-
return {_TYPE: shape.cls.__name__, **written}
|
|
30
|
-
return written
|
|
31
|
-
|
|
32
|
-
if any(option is shape for option in _portable_options(shapes)):
|
|
33
|
-
return {_TYPE: shape.option_id(), _VALUE: written}
|
|
34
|
-
return written
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _written(shape, value):
|
|
38
|
-
kind = type(shape)
|
|
39
|
-
if kind is Date:
|
|
40
|
-
return value.isoformat()
|
|
41
|
-
if kind is Time:
|
|
42
|
-
return value.isoformat(timespec="seconds")
|
|
43
|
-
if kind is EnumShape:
|
|
44
|
-
return value.name
|
|
45
|
-
if kind is List:
|
|
46
|
-
return [_portable(shape.item, item) for item in value]
|
|
47
|
-
if kind is Tuple:
|
|
48
|
-
return [_portable(shape._item_at(i), item) for i, item in enumerate(value)]
|
|
49
|
-
if kind is Struct:
|
|
50
|
-
return {f.name: _portable(f.shape, getattr(value, f.name))
|
|
51
|
-
for f in shape.fields}
|
|
52
|
-
if kind is Float:
|
|
53
|
-
return _as_float(value)
|
|
54
|
-
return value
|
|
55
|
-
|
|
56
|
-
|
|
57
18
|
def _as_float(value):
|
|
58
19
|
try:
|
|
59
20
|
restored = float(value)
|
|
@@ -234,7 +195,7 @@ def _field_node(f, doc: _Document) -> dict:
|
|
|
234
195
|
if f.optional_toggle is not None:
|
|
235
196
|
node["optional_toggle"] = f.optional_toggle.enabled
|
|
236
197
|
if f.default is not MISSING:
|
|
237
|
-
node["default"] =
|
|
198
|
+
node["default"] = _encode_options(f.shape, f.default, (f.name, "default"))
|
|
238
199
|
node["shape"] = _slot(f.shape, doc)
|
|
239
200
|
return node
|
|
240
201
|
|
|
@@ -2,7 +2,7 @@ from dataclasses import dataclass
|
|
|
2
2
|
|
|
3
3
|
from pytypehint.contract import _signature_document
|
|
4
4
|
from pytypehint.structure import (
|
|
5
|
-
Field, _build_kwargs, _decode_fields, _resolve_fields,
|
|
5
|
+
Field, _build_kwargs, _decode_fields, _encode_fields, _resolve_fields,
|
|
6
6
|
)
|
|
7
7
|
|
|
8
8
|
|
|
@@ -28,6 +28,9 @@ class Signature:
|
|
|
28
28
|
def decode(self, kwargs) -> dict:
|
|
29
29
|
return _decode_fields(self.params, kwargs)
|
|
30
30
|
|
|
31
|
+
def encode(self, kwargs) -> dict:
|
|
32
|
+
return _encode_fields(self.params, kwargs, kind="argument")
|
|
33
|
+
|
|
31
34
|
def resolve(self, kwargs) -> dict:
|
|
32
35
|
return _resolve_fields(self.params, kwargs, kind="argument")
|
|
33
36
|
|
|
@@ -57,6 +57,12 @@ class Struct(Shape):
|
|
|
57
57
|
def decode(self, data) -> dict:
|
|
58
58
|
return _decode_fields(self.fields, data)
|
|
59
59
|
|
|
60
|
+
def encode(self, instance) -> dict:
|
|
61
|
+
if type(instance) is not self.cls:
|
|
62
|
+
raise SchemaTypeError(
|
|
63
|
+
f"expected {self.cls.__name__}, got {type(instance).__name__}")
|
|
64
|
+
return _encode_shape(self, instance, ())
|
|
65
|
+
|
|
60
66
|
def resolve(self, data) -> dict:
|
|
61
67
|
if type(data) is self.cls:
|
|
62
68
|
raise SchemaTypeError(f"expected dict, got {self.cls.__name__} instance")
|
|
@@ -430,6 +436,69 @@ def _decode_fields(fields, data) -> dict:
|
|
|
430
436
|
return decoded
|
|
431
437
|
|
|
432
438
|
|
|
439
|
+
# Encode checks only what routing needs; build validates what decode restores.
|
|
440
|
+
def _encode_options(shapes, value, path):
|
|
441
|
+
shape = value_branch(shapes, value)
|
|
442
|
+
if shape is None:
|
|
443
|
+
try:
|
|
444
|
+
check_options_value(shapes, value)
|
|
445
|
+
except (TypeError, ValueError) as e:
|
|
446
|
+
raise _prefixed(e, path) from e
|
|
447
|
+
raise AssertionError("unrouted value matches an option")
|
|
448
|
+
|
|
449
|
+
written = _encode_shape(shape, value, path)
|
|
450
|
+
|
|
451
|
+
if type(shape) is Struct:
|
|
452
|
+
if len([s for s in shapes if type(s) is Struct]) > 1:
|
|
453
|
+
return {_TYPE: shape.cls.__name__, **written}
|
|
454
|
+
return written
|
|
455
|
+
|
|
456
|
+
if any(option is shape for option in _portable_options(shapes)):
|
|
457
|
+
return {_TYPE: shape.option_id(), _VALUE: written}
|
|
458
|
+
return written
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _encode_shape(shape, value, path):
|
|
462
|
+
kind = type(shape)
|
|
463
|
+
# Keep an invalid fraction or offset in the text for validation to report.
|
|
464
|
+
if kind in (Date, Time):
|
|
465
|
+
return value.isoformat()
|
|
466
|
+
if kind is EnumShape:
|
|
467
|
+
return value.name
|
|
468
|
+
if kind is List:
|
|
469
|
+
return [_encode_options(shape.item, item, (*path, i))
|
|
470
|
+
for i, item in enumerate(value)]
|
|
471
|
+
if kind is Tuple:
|
|
472
|
+
# Length limits are left for build; only a fixed size decides the slots.
|
|
473
|
+
if not shape.variadic and len(value) != len(shape.items):
|
|
474
|
+
raise SchemaValueError(
|
|
475
|
+
f"expected {len(shape.items)} items, got {len(value)}", path)
|
|
476
|
+
return [_encode_options(shape._item_at(i), item, (*path, i))
|
|
477
|
+
for i, item in enumerate(value)]
|
|
478
|
+
if kind is Struct:
|
|
479
|
+
return {f.name: _encode_options(f.shape, getattr(value, f.name), (*path, f.name))
|
|
480
|
+
for f in shape.fields}
|
|
481
|
+
return value
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _encode_fields(fields, data, *, kind: str) -> dict:
|
|
485
|
+
if type(data) is not dict:
|
|
486
|
+
raise SchemaTypeError(f"expected dict, got {type(data).__name__}")
|
|
487
|
+
|
|
488
|
+
invalid_key = next((key for key in data if type(key) is not str), MISSING)
|
|
489
|
+
if invalid_key is not MISSING:
|
|
490
|
+
raise SchemaTypeError(
|
|
491
|
+
f"expected string keys, got {type(invalid_key).__name__}")
|
|
492
|
+
|
|
493
|
+
known = {f.name for f in fields}
|
|
494
|
+
extra = sorted(k for k in data if k not in known)
|
|
495
|
+
if extra:
|
|
496
|
+
raise SchemaTypeError(f"unexpected {kind}(s): {', '.join(extra)}")
|
|
497
|
+
|
|
498
|
+
return {f.name: _encode_options(f.shape, data[f.name], (f.name,))
|
|
499
|
+
for f in fields if f.name in data}
|
|
500
|
+
|
|
501
|
+
|
|
433
502
|
# Build disables present-value checks after the outer resolve validated them.
|
|
434
503
|
def _resolve_fields(fields, data, *, kind: str, fill: bool = True,
|
|
435
504
|
check_present: bool = True) -> dict:
|