pytypehint 0.0.1__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-0.0.1/.github/dependabot.yml +6 -0
- pytypehint-0.0.1/.github/workflows/ci.yml +28 -0
- pytypehint-0.0.1/.github/workflows/publish.yml +24 -0
- pytypehint-0.0.1/.gitignore +11 -0
- pytypehint-0.0.1/.hypothesis/.gitignore +9 -0
- pytypehint-0.0.1/.hypothesis/constants/2678b14aa369a75b +4 -0
- pytypehint-0.0.1/.hypothesis/constants/394532c786c7fa39 +4 -0
- pytypehint-0.0.1/.hypothesis/constants/417463f20a0e2f4a +4 -0
- pytypehint-0.0.1/.hypothesis/constants/7ea8177d35d3f469 +4 -0
- pytypehint-0.0.1/.hypothesis/constants/9260f428ce0a0a9c +4 -0
- pytypehint-0.0.1/.hypothesis/constants/ad3acb7981ed8c4e +4 -0
- pytypehint-0.0.1/.hypothesis/constants/b60ecb6f6243dd23 +4 -0
- pytypehint-0.0.1/.hypothesis/constants/be68644653efe775 +4 -0
- pytypehint-0.0.1/.hypothesis/constants/edf98ac0cc5cb0cb +4 -0
- pytypehint-0.0.1/.hypothesis/constants/f49b7bbd9251f442 +4 -0
- pytypehint-0.0.1/CHANGELOG.md +7 -0
- pytypehint-0.0.1/LICENSE +21 -0
- pytypehint-0.0.1/PKG-INFO +112 -0
- pytypehint-0.0.1/README.md +98 -0
- pytypehint-0.0.1/docs/atoms.md +80 -0
- pytypehint-0.0.1/docs/build.md +96 -0
- pytypehint-0.0.1/docs/comparison.md +35 -0
- pytypehint-0.0.1/docs/defaults.md +37 -0
- pytypehint-0.0.1/docs/philosophy.md +88 -0
- pytypehint-0.0.1/docs/resolve.md +30 -0
- pytypehint-0.0.1/docs/restrictions.md +209 -0
- pytypehint-0.0.1/docs/vocabulary.md +60 -0
- pytypehint-0.0.1/pyproject.toml +34 -0
- pytypehint-0.0.1/src/pytypehint/__init__.py +58 -0
- pytypehint-0.0.1/src/pytypehint/atoms.py +220 -0
- pytypehint-0.0.1/src/pytypehint/bridge.py +309 -0
- pytypehint-0.0.1/src/pytypehint/errors.py +56 -0
- pytypehint-0.0.1/src/pytypehint/py.typed +0 -0
- pytypehint-0.0.1/src/pytypehint/shapes.py +586 -0
- pytypehint-0.0.1/src/pytypehint/signature.py +29 -0
- pytypehint-0.0.1/src/pytypehint/structure.py +351 -0
- pytypehint-0.0.1/src/pytypehint/utils.py +20 -0
- pytypehint-0.0.1/src/pytypehint/validation.py +10 -0
- pytypehint-0.0.1/tests/test_annotated.py +555 -0
- pytypehint-0.0.1/tests/test_annotated_hoisting_properties.py +329 -0
- pytypehint-0.0.1/tests/test_atom_table_reflection.py +233 -0
- pytypehint-0.0.1/tests/test_atoms.py +118 -0
- pytypehint-0.0.1/tests/test_bool.py +195 -0
- pytypehint-0.0.1/tests/test_bridge.py +290 -0
- pytypehint-0.0.1/tests/test_build.py +95 -0
- pytypehint-0.0.1/tests/test_certification.py +94 -0
- pytypehint-0.0.1/tests/test_composition.py +189 -0
- pytypehint-0.0.1/tests/test_datetime.py +266 -0
- pytypehint-0.0.1/tests/test_default_types.py +127 -0
- pytypehint-0.0.1/tests/test_discriminator.py +105 -0
- pytypehint-0.0.1/tests/test_edges1.py +596 -0
- pytypehint-0.0.1/tests/test_enum.py +256 -0
- pytypehint-0.0.1/tests/test_equality.py +35 -0
- pytypehint-0.0.1/tests/test_errors_pickle.py +200 -0
- pytypehint-0.0.1/tests/test_extra.py +136 -0
- pytypehint-0.0.1/tests/test_extra1.py +224 -0
- pytypehint-0.0.1/tests/test_extra10.py +243 -0
- pytypehint-0.0.1/tests/test_extra2.py +255 -0
- pytypehint-0.0.1/tests/test_extra3.py +150 -0
- pytypehint-0.0.1/tests/test_extra4.py +282 -0
- pytypehint-0.0.1/tests/test_extra5.py +213 -0
- pytypehint-0.0.1/tests/test_extra6.py +172 -0
- pytypehint-0.0.1/tests/test_extra7.py +85 -0
- pytypehint-0.0.1/tests/test_extra8.py +161 -0
- pytypehint-0.0.1/tests/test_extra9.py +241 -0
- pytypehint-0.0.1/tests/test_field_dataclass_surface.py +123 -0
- pytypehint-0.0.1/tests/test_final_restrictions.py +127 -0
- pytypehint-0.0.1/tests/test_float.py +234 -0
- pytypehint-0.0.1/tests/test_initvar_rejection.py +138 -0
- pytypehint-0.0.1/tests/test_int.py +713 -0
- pytypehint-0.0.1/tests/test_list.py +615 -0
- pytypehint-0.0.1/tests/test_literal.py +214 -0
- pytypehint-0.0.1/tests/test_missing.py +23 -0
- pytypehint-0.0.1/tests/test_none.py +471 -0
- pytypehint-0.0.1/tests/test_none_transparency.py +170 -0
- pytypehint-0.0.1/tests/test_optional_toggle.py +125 -0
- pytypehint-0.0.1/tests/test_presentation_atoms_surface.py +101 -0
- pytypehint-0.0.1/tests/test_recursive.py +225 -0
- pytypehint-0.0.1/tests/test_remat_runtime_failure.py +253 -0
- pytypehint-0.0.1/tests/test_rematerialization.py +242 -0
- pytypehint-0.0.1/tests/test_resolve.py +228 -0
- pytypehint-0.0.1/tests/test_signature.py +514 -0
- pytypehint-0.0.1/tests/test_signature_build.py +33 -0
- pytypehint-0.0.1/tests/test_signature_field_rejection.py +36 -0
- pytypehint-0.0.1/tests/test_single_pass_validation.py +352 -0
- pytypehint-0.0.1/tests/test_str.py +328 -0
- pytypehint-0.0.1/tests/test_struct.py +457 -0
- pytypehint-0.0.1/tests/test_structured_errors.py +224 -0
- pytypehint-0.0.1/tests/test_union_field_atoms.py +103 -0
- pytypehint-0.0.1/tests/test_union_option_collisions.py +211 -0
- pytypehint-0.0.1/tests/test_unions.py +382 -0
- pytypehint-0.0.1/tests/test_unions_extra.py +393 -0
- pytypehint-0.0.1/tests/test_validate.py +323 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
check:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
strategy:
|
|
16
|
+
matrix:
|
|
17
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v7
|
|
20
|
+
- uses: actions/setup-python@v6
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
- uses: astral-sh/setup-uv@v7
|
|
24
|
+
with:
|
|
25
|
+
enable-cache: true
|
|
26
|
+
- run: uv pip install --system -e ".[dev]"
|
|
27
|
+
- run: mypy src
|
|
28
|
+
- run: pytest
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
id-token: write
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v7
|
|
14
|
+
- uses: actions/setup-python@v6
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.11"
|
|
17
|
+
- uses: astral-sh/setup-uv@v7
|
|
18
|
+
with:
|
|
19
|
+
enable-cache: true
|
|
20
|
+
- run: uv pip install --system -e ".[dev]"
|
|
21
|
+
- run: mypy src
|
|
22
|
+
- run: pytest
|
|
23
|
+
- run: uv build
|
|
24
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# This .gitignore file was automatically created by Hypothesis. Hypothesis gitignores
|
|
2
|
+
# .hypothesis by default, because we generally recommend that .hypothesis not be checked
|
|
3
|
+
# into version control.
|
|
4
|
+
#
|
|
5
|
+
# If you *would* like to check .hypothesis into version control, you should delete this
|
|
6
|
+
# file. Hypothesis will not re-create this .gitignore unless .hypothesis is deleted (and
|
|
7
|
+
# if it does, that's a bug - please report it!)
|
|
8
|
+
|
|
9
|
+
*
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# file: /home/runner/work/pytypehint/pytypehint/src/pytypehint/structure.py
|
|
2
|
+
# hypothesis_version: 6.156.6
|
|
3
|
+
|
|
4
|
+
[' | ', '$type', 'Field', 'Struct(<incomplete>)', '_deferred', '_recipe', 'cls', 'default', 'description', 'fields', 'key', 'label', 'optional_toggle', 'tuple[Field, ...]']
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# file: /home/runner/work/pytypehint/pytypehint/src/pytypehint/__init__.py
|
|
2
|
+
# hypothesis_version: 6.156.6
|
|
3
|
+
|
|
4
|
+
['0.0.1', 'Bool', 'Choices', 'Date', 'Description', 'EnumShape', 'Extra', 'Field', 'Float', 'Int', 'IsPassword', 'IsPathFile', 'Label', 'List', 'MISSING', 'Max', 'Min', 'MultipleOf', 'NoneShape', 'OptionalToggle', 'Pattern', 'Placeholder', 'Rows', 'SchemaTypeError', 'SchemaValueError', 'Shape', 'Signature', 'Slider', 'Step', 'Str', 'Struct', 'Time', 'signature_of', 'struct_of']
|
pytypehint-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Beltrán Offerrall
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytypehint
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Compiles Python type hints into strict, inspectable schemas
|
|
5
|
+
Author: Beltrán Offerrall
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: hypothesis; extra == 'dev'
|
|
11
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# pytypehint
|
|
16
|
+
|
|
17
|
+
[](https://pypi.org/project/pytypehint/)
|
|
18
|
+
|
|
19
|
+
`pytypehint` compiles Python type hints into strict, inspectable schemas. A
|
|
20
|
+
hint carries everything there is to know about a field — its type, its limits,
|
|
21
|
+
and its presentation — so the dataclass is the single source of truth: the
|
|
22
|
+
core validates plain input data, fills fresh defaults and constructs dataclass
|
|
23
|
+
instances; wrapper authors read the same schema to render controls, coerce
|
|
24
|
+
external input and execute functions themselves. What the core hands them is
|
|
25
|
+
raw, inspectable structure, never an opinion about it: interpretive
|
|
26
|
+
conveniences belong to wrappers and to intermediate packages built on the core.
|
|
27
|
+
Stdlib only; Python 3.11+; `py.typed` included.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install pytypehint
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from typing import Annotated
|
|
36
|
+
from pytypehint import Label, Max, Min, signature_of, struct_of
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class Page:
|
|
40
|
+
number: Annotated[int, Min(1)] = 1
|
|
41
|
+
size: Annotated[int, Min(1), Max(100), Label("Page size")] = 20
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class Search:
|
|
45
|
+
query: str
|
|
46
|
+
page: Page = Page()
|
|
47
|
+
tags: list[str] = field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
value = struct_of(Search).build({"query": "python", "page": {"size": 50}})
|
|
50
|
+
# Search(query='python', page=Page(number=1, size=50), tags=[])
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
struct_of(Search).build({"query": "python", "page": {"size": 500}})
|
|
54
|
+
except ValueError as error:
|
|
55
|
+
assert str(error) == "page: size: too large: 500, maximum 100"
|
|
56
|
+
|
|
57
|
+
def search(query: str, page: Page = Page()): ...
|
|
58
|
+
kwargs = signature_of(search).build({"query": "python"})
|
|
59
|
+
search(**kwargs) # execution belongs to the caller
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Guarantees
|
|
63
|
+
|
|
64
|
+
- Exact types: `type(value) is T`; the core never coerces.
|
|
65
|
+
- Data enters as dictionaries and lists; dataclass instances leave through `build`.
|
|
66
|
+
- Defaults are certified at compilation and rematerialized fresh per missing key.
|
|
67
|
+
- Invalid constraints fail while compiling the schema.
|
|
68
|
+
- Errors retain the complete field and list-index path, as the message text and
|
|
69
|
+
as data: `SchemaTypeError` and `SchemaValueError` carry `path` and `leaf`, and
|
|
70
|
+
subclass `TypeError` and `ValueError`.
|
|
71
|
+
- Notation atoms are stored and cross-checked but never affect validation;
|
|
72
|
+
presentation belongs to the wrapper.
|
|
73
|
+
- `Struct`, `Field` and `Signature` compare by identity; compile once and share.
|
|
74
|
+
- `build` validates the input once and constructs directly; the cost is linear.
|
|
75
|
+
- `resolve` validates and fills defaults without constructing nested dictionaries.
|
|
76
|
+
- `Signature.build` returns constructed keyword arguments and never invokes the function.
|
|
77
|
+
|
|
78
|
+
## Vocabulary
|
|
79
|
+
|
|
80
|
+
| Hint | Shape | Input |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `int` | `Int` | exact `int` |
|
|
83
|
+
| `float` | `Float` | exact `float` |
|
|
84
|
+
| `str` | `Str` | exact `str` |
|
|
85
|
+
| `bool` | `Bool` | exact `bool` |
|
|
86
|
+
| `date` | `Date` | exact `datetime.date` |
|
|
87
|
+
| `time` | `Time` | exact naive `datetime.time` |
|
|
88
|
+
| `Enum` subclass | `EnumShape` | exact member type |
|
|
89
|
+
| `None` | `NoneShape` | `None` |
|
|
90
|
+
| `list[X]` | `List` | list; nesting and union items supported |
|
|
91
|
+
| dataclass | `Struct` | dictionary; `build` constructs it |
|
|
92
|
+
| `A \| B` | tuple of shapes | exact scalar type or routed dataclass dictionary |
|
|
93
|
+
| `Literal[...]` | `Int` or `Str` with `Choices` | homogeneous `int` or `str` literals |
|
|
94
|
+
|
|
95
|
+
## Public API
|
|
96
|
+
|
|
97
|
+
Everything public is exported from `pytypehint`:
|
|
98
|
+
|
|
99
|
+
- `struct_of`, `signature_of`;
|
|
100
|
+
- `Struct`, `Field`, `Signature`;
|
|
101
|
+
- errors: `SchemaTypeError`, `SchemaValueError`;
|
|
102
|
+
- `Shape`, `Int`, `Float`, `Str`, `Bool`, `Date`, `Time`, `List`,
|
|
103
|
+
`NoneShape`, `EnumShape`;
|
|
104
|
+
- limits: `Min`, `Max`, `Choices`, `MultipleOf`, `Pattern`, `IsPathFile`;
|
|
105
|
+
- notation: `Label`, `Description`, `Placeholder`, `Step`, `Slider`,
|
|
106
|
+
`IsPassword`, `Rows`, `Extra`, `OptionalToggle`;
|
|
107
|
+
- `MISSING`.
|
|
108
|
+
|
|
109
|
+
Start with [the design principles](docs/philosophy.md), then read
|
|
110
|
+
[build](docs/build.md), [resolve](docs/resolve.md), [defaults](docs/defaults.md),
|
|
111
|
+
[vocabulary](docs/vocabulary.md), [atoms](docs/atoms.md),
|
|
112
|
+
[restrictions](docs/restrictions.md), and [comparison](docs/comparison.md).
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# pytypehint
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/pytypehint/)
|
|
4
|
+
|
|
5
|
+
`pytypehint` compiles Python type hints into strict, inspectable schemas. A
|
|
6
|
+
hint carries everything there is to know about a field — its type, its limits,
|
|
7
|
+
and its presentation — so the dataclass is the single source of truth: the
|
|
8
|
+
core validates plain input data, fills fresh defaults and constructs dataclass
|
|
9
|
+
instances; wrapper authors read the same schema to render controls, coerce
|
|
10
|
+
external input and execute functions themselves. What the core hands them is
|
|
11
|
+
raw, inspectable structure, never an opinion about it: interpretive
|
|
12
|
+
conveniences belong to wrappers and to intermediate packages built on the core.
|
|
13
|
+
Stdlib only; Python 3.11+; `py.typed` included.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install pytypehint
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Annotated
|
|
22
|
+
from pytypehint import Label, Max, Min, signature_of, struct_of
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class Page:
|
|
26
|
+
number: Annotated[int, Min(1)] = 1
|
|
27
|
+
size: Annotated[int, Min(1), Max(100), Label("Page size")] = 20
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Search:
|
|
31
|
+
query: str
|
|
32
|
+
page: Page = Page()
|
|
33
|
+
tags: list[str] = field(default_factory=list)
|
|
34
|
+
|
|
35
|
+
value = struct_of(Search).build({"query": "python", "page": {"size": 50}})
|
|
36
|
+
# Search(query='python', page=Page(number=1, size=50), tags=[])
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
struct_of(Search).build({"query": "python", "page": {"size": 500}})
|
|
40
|
+
except ValueError as error:
|
|
41
|
+
assert str(error) == "page: size: too large: 500, maximum 100"
|
|
42
|
+
|
|
43
|
+
def search(query: str, page: Page = Page()): ...
|
|
44
|
+
kwargs = signature_of(search).build({"query": "python"})
|
|
45
|
+
search(**kwargs) # execution belongs to the caller
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Guarantees
|
|
49
|
+
|
|
50
|
+
- Exact types: `type(value) is T`; the core never coerces.
|
|
51
|
+
- Data enters as dictionaries and lists; dataclass instances leave through `build`.
|
|
52
|
+
- Defaults are certified at compilation and rematerialized fresh per missing key.
|
|
53
|
+
- Invalid constraints fail while compiling the schema.
|
|
54
|
+
- Errors retain the complete field and list-index path, as the message text and
|
|
55
|
+
as data: `SchemaTypeError` and `SchemaValueError` carry `path` and `leaf`, and
|
|
56
|
+
subclass `TypeError` and `ValueError`.
|
|
57
|
+
- Notation atoms are stored and cross-checked but never affect validation;
|
|
58
|
+
presentation belongs to the wrapper.
|
|
59
|
+
- `Struct`, `Field` and `Signature` compare by identity; compile once and share.
|
|
60
|
+
- `build` validates the input once and constructs directly; the cost is linear.
|
|
61
|
+
- `resolve` validates and fills defaults without constructing nested dictionaries.
|
|
62
|
+
- `Signature.build` returns constructed keyword arguments and never invokes the function.
|
|
63
|
+
|
|
64
|
+
## Vocabulary
|
|
65
|
+
|
|
66
|
+
| Hint | Shape | Input |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `int` | `Int` | exact `int` |
|
|
69
|
+
| `float` | `Float` | exact `float` |
|
|
70
|
+
| `str` | `Str` | exact `str` |
|
|
71
|
+
| `bool` | `Bool` | exact `bool` |
|
|
72
|
+
| `date` | `Date` | exact `datetime.date` |
|
|
73
|
+
| `time` | `Time` | exact naive `datetime.time` |
|
|
74
|
+
| `Enum` subclass | `EnumShape` | exact member type |
|
|
75
|
+
| `None` | `NoneShape` | `None` |
|
|
76
|
+
| `list[X]` | `List` | list; nesting and union items supported |
|
|
77
|
+
| dataclass | `Struct` | dictionary; `build` constructs it |
|
|
78
|
+
| `A \| B` | tuple of shapes | exact scalar type or routed dataclass dictionary |
|
|
79
|
+
| `Literal[...]` | `Int` or `Str` with `Choices` | homogeneous `int` or `str` literals |
|
|
80
|
+
|
|
81
|
+
## Public API
|
|
82
|
+
|
|
83
|
+
Everything public is exported from `pytypehint`:
|
|
84
|
+
|
|
85
|
+
- `struct_of`, `signature_of`;
|
|
86
|
+
- `Struct`, `Field`, `Signature`;
|
|
87
|
+
- errors: `SchemaTypeError`, `SchemaValueError`;
|
|
88
|
+
- `Shape`, `Int`, `Float`, `Str`, `Bool`, `Date`, `Time`, `List`,
|
|
89
|
+
`NoneShape`, `EnumShape`;
|
|
90
|
+
- limits: `Min`, `Max`, `Choices`, `MultipleOf`, `Pattern`, `IsPathFile`;
|
|
91
|
+
- notation: `Label`, `Description`, `Placeholder`, `Step`, `Slider`,
|
|
92
|
+
`IsPassword`, `Rows`, `Extra`, `OptionalToggle`;
|
|
93
|
+
- `MISSING`.
|
|
94
|
+
|
|
95
|
+
Start with [the design principles](docs/philosophy.md), then read
|
|
96
|
+
[build](docs/build.md), [resolve](docs/resolve.md), [defaults](docs/defaults.md),
|
|
97
|
+
[vocabulary](docs/vocabulary.md), [atoms](docs/atoms.md),
|
|
98
|
+
[restrictions](docs/restrictions.md), and [comparison](docs/comparison.md).
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Atoms
|
|
2
|
+
|
|
3
|
+
Atoms are frozen values inside `Annotated`. Limits affect validation; notation
|
|
4
|
+
is stored for wrappers and ignored by validation.
|
|
5
|
+
|
|
6
|
+
| Shape | Accepted atoms |
|
|
7
|
+
|---|---|
|
|
8
|
+
| `Int` | `Min`, `Max`, `Choices`, `MultipleOf`, `Step`, `Slider`, `Placeholder`, `Extra` |
|
|
9
|
+
| `Float` | `Min`, `Max`, `Choices`, `Step`, `Slider`, `Placeholder`, `Extra` |
|
|
10
|
+
| `Str` | `Min`, `Max`, `Choices`, `Pattern`, `IsPathFile`, `IsPassword`, `Rows`, `Placeholder`, `Extra` |
|
|
11
|
+
| `Date`, `Time` | `Min`, `Max`, `Choices`, `Placeholder`, `Extra` |
|
|
12
|
+
| `List` | `Min`, `Max` for length, `Extra` |
|
|
13
|
+
| `Bool`, `NoneShape` | `Extra` |
|
|
14
|
+
| `EnumShape`, dataclass (`Struct`) | none; annotate struct fields, not nesting |
|
|
15
|
+
| any field | `Label`, `Description` |
|
|
16
|
+
| optional field (`X \| None`) | `OptionalToggle` |
|
|
17
|
+
|
|
18
|
+
## Atoms
|
|
19
|
+
|
|
20
|
+
`Min(value, *, exclusive=False)` and `Max(...)` set lower and upper bounds.
|
|
21
|
+
On strings and lists they constrain length and cannot be exclusive.
|
|
22
|
+
|
|
23
|
+
`Choices(values=(...))` requires a non-empty tuple of unique, hashable values.
|
|
24
|
+
Choices must have the shape's exact type and satisfy its other limits.
|
|
25
|
+
|
|
26
|
+
`MultipleOf(value)` accepts a positive integer and applies only to `Int`.
|
|
27
|
+
Integer-only divisibility avoids floating-point ambiguity.
|
|
28
|
+
|
|
29
|
+
`Pattern(regex, *, message=None)` applies a full regular-expression match to
|
|
30
|
+
`Str`. A custom message replaces the standard mismatch message.
|
|
31
|
+
|
|
32
|
+
`Step(value)` is positive numeric wrapper notation. `Slider(show_value=True)`
|
|
33
|
+
is notation for numeric fields and requires both `Min` and `Max`.
|
|
34
|
+
|
|
35
|
+
`Placeholder(text)` is non-empty wrapper notation for scalar inputs.
|
|
36
|
+
`IsPassword()` and `Rows(n)` are string notation; rows must be positive.
|
|
37
|
+
|
|
38
|
+
`Extra(text)` is opaque, non-empty wrapper notation. The core stores it and
|
|
39
|
+
never interprets it; layering follows the standard rule.
|
|
40
|
+
|
|
41
|
+
`IsPathFile(extensions=(...))` marks a string as a path input. Extensions are
|
|
42
|
+
lowercase dotted suffixes; validation checks the suffix, never file existence.
|
|
43
|
+
|
|
44
|
+
`Label(text)` and `Description(text)` are non-empty field-level notation.
|
|
45
|
+
|
|
46
|
+
`OptionalToggle(enabled)` is field-level notation for `X | None`. `True` starts
|
|
47
|
+
a wrapper toggle on, `False` starts it off, and absence leaves the choice to the
|
|
48
|
+
wrapper. It never changes resolution or defaults.
|
|
49
|
+
|
|
50
|
+
## Compile-time cross-checks
|
|
51
|
+
|
|
52
|
+
The schema rejects empty ranges; choices outside bounds or failing pattern,
|
|
53
|
+
multiple or extension rules; ranges containing no valid multiple; sliders
|
|
54
|
+
without both bounds; wrong bound types; and `OptionalToggle` on a non-optional
|
|
55
|
+
field. These contradictions fail during schema compilation because a compiled
|
|
56
|
+
schema must be structurally valid. Compilation rejects contradictions it can
|
|
57
|
+
determine exactly; it does not attempt a general satisfiability proof across
|
|
58
|
+
constraints such as a regular expression combined with length bounds.
|
|
59
|
+
|
|
60
|
+
Unsupported metadata reports `unsupported metadata for <type>: <atom>`.
|
|
61
|
+
Metadata across a multi-type union must be placed per option.
|
|
62
|
+
|
|
63
|
+
## Layering
|
|
64
|
+
|
|
65
|
+
For repeated atom classes, the outer layer wins; within one layer, the
|
|
66
|
+
rightmost atom wins. The rule applies uniformly to limits and field notation:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from typing import Annotated
|
|
70
|
+
from pytypehint import Max, Min, OptionalToggle
|
|
71
|
+
|
|
72
|
+
Percent = Annotated[int, Min(0), Max(100)]
|
|
73
|
+
Narrow = Annotated[Percent, Max(50)]
|
|
74
|
+
|
|
75
|
+
Optional = Annotated[int | None, OptionalToggle(True)]
|
|
76
|
+
Closed = Annotated[Optional, OptionalToggle(False)]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Conflicting field atoms hoisted from different union options fail with
|
|
80
|
+
`conflicting ... across union options`; an explicit outer atom overrides them.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Build
|
|
2
|
+
|
|
3
|
+
`Struct.build(data)` returns a validated dataclass instance.
|
|
4
|
+
`Signature.build(data)` returns validated, constructed keyword arguments. It
|
|
5
|
+
never calls the function; use `fn(**kwargs)` or `await fn(**kwargs)` yourself.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pytypehint import signature_of, struct_of
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Config:
|
|
13
|
+
n: int = 1
|
|
14
|
+
|
|
15
|
+
def run(config: Config):
|
|
16
|
+
return config.n
|
|
17
|
+
|
|
18
|
+
data = {"n": 2}
|
|
19
|
+
instance = struct_of(Config).build(data)
|
|
20
|
+
kwargs = signature_of(run).build({"config": data})
|
|
21
|
+
result = run(**kwargs)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Data in, objects out
|
|
25
|
+
|
|
26
|
+
Input uses dictionaries for every dataclass, including nested values. Instances
|
|
27
|
+
are rejected because input belongs to the external-data side of the boundary:
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
page: expected dict, got Page instance
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Defaults are authored schema data and may still be dataclass instances.
|
|
34
|
+
|
|
35
|
+
Nested dictionaries become instances; nested lists are rebuilt with their
|
|
36
|
+
contents constructed. Input values are validated by exact type before any
|
|
37
|
+
constructor runs.
|
|
38
|
+
|
|
39
|
+
## Dataclass unions and `$type`
|
|
40
|
+
|
|
41
|
+
Scalar union values already carry their Python type. A dictionary does not, so a
|
|
42
|
+
union containing two or more dataclasses requires the reserved `$type` key. Its
|
|
43
|
+
value is the selected class's `__name__`.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from dataclasses import dataclass
|
|
47
|
+
from pytypehint import struct_of
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class File: path: str
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class Url: value: str
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class Source: value: File | Url
|
|
57
|
+
|
|
58
|
+
data = {"value": {"$type": "Url", "value": "https://example.test"}}
|
|
59
|
+
struct_of(Source).resolve(data)
|
|
60
|
+
# {'value': {'$type': 'Url', 'value': 'https://example.test'}}
|
|
61
|
+
struct_of(Source).build(data) # Source(value=Url(...)); consumes $type
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Missing and invalid discriminators fail as follows:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
value: ambiguous dict: field accepts File | Url — add "$type" naming the variant
|
|
68
|
+
value: $type: not a choice: 'Other', expected one of ('File', 'Url')
|
|
69
|
+
value: $type: expected str, got int
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`$type` on a non-ambiguous dataclass is an ordinary unexpected key. The name
|
|
73
|
+
cannot collide with a dataclass field because fields must be identifiers.
|
|
74
|
+
Discrimination works at every nesting depth and in union-valued list items.
|
|
75
|
+
|
|
76
|
+
## Errors and constructors
|
|
77
|
+
|
|
78
|
+
Validation errors accumulate field names and list indexes:
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
cart: items: [0]: size: default: too large: 145, maximum 100
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Nested `resolve` failures receive the complete path. Once validation succeeds,
|
|
85
|
+
`__init__` and `__post_init__` exceptions propagate unchanged: they are program
|
|
86
|
+
errors, not input-coordinate errors. Put cross-field validation in
|
|
87
|
+
`__post_init__`; it runs after each successful construction.
|
|
88
|
+
|
|
89
|
+
`build` validates the input tree once and then constructs from it. Do not mutate
|
|
90
|
+
the input while that construction is running: `__post_init__` is for cross-field
|
|
91
|
+
validation, not for effects on the input data. A constructor that reaches back
|
|
92
|
+
and edits the dictionaries still being built reads values the core has already
|
|
93
|
+
validated and will not check again, so the result is undefined. This is the same
|
|
94
|
+
promise [defaults.md](defaults.md) asks for recipes, and it belongs to the author
|
|
95
|
+
for the same reason: the core cannot prove it without charging every honest input
|
|
96
|
+
for the check.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Comparison
|
|
2
|
+
|
|
3
|
+
| Capability | pytypehint core | Wrapper | Dataclasses alone |
|
|
4
|
+
|---|---:|---:|---:|
|
|
5
|
+
| Compile type hints into inspectable shapes | yes | consumes | no |
|
|
6
|
+
| Exact validation and error paths | yes | may present | no |
|
|
7
|
+
| Cross-check atom contradictions at import | yes | no | no |
|
|
8
|
+
| Fresh rematerialized defaults | yes | consumes | factories only |
|
|
9
|
+
| Construct nested dataclasses from data | yes | calls | no |
|
|
10
|
+
| Coerce HTTP/CLI/form input | no | yes | no |
|
|
11
|
+
| Render controls | no | yes | no |
|
|
12
|
+
| Accumulate every failure in a tree | no | yes | no |
|
|
13
|
+
| Interpret the schema (optionality, traversal) | no | yes | no |
|
|
14
|
+
| Execute or await functions | no | yes | normal Python |
|
|
15
|
+
|
|
16
|
+
The core column is schema, validation and construction, and stops there. The
|
|
17
|
+
wrapper column need not be a single wrapper: coercion, presentation, inspection
|
|
18
|
+
ergonomics and error accumulation may live in the wrapper itself or in an
|
|
19
|
+
intermediate package that depends on the core and versions its own conveniences
|
|
20
|
+
on its own schedule. See [philosophy.md](philosophy.md).
|
|
21
|
+
|
|
22
|
+
## When not to use it
|
|
23
|
+
|
|
24
|
+
Do not use pytypehint when input is already trusted Python objects, when coercion
|
|
25
|
+
is the primary task and no wrapper boundary exists, or when the required types
|
|
26
|
+
fall outside its closed vocabulary. Direct dataclass construction is simpler for
|
|
27
|
+
internal code without an external-data boundary.
|
|
28
|
+
|
|
29
|
+
## Cost
|
|
30
|
+
|
|
31
|
+
Compile schemas once and share them. Compilation resolves hints, checks atoms and
|
|
32
|
+
certifies defaults. `build` then validates the supplied tree once and constructs
|
|
33
|
+
directly from it; missing defaults are rematerialized and validated at their own
|
|
34
|
+
level. Every value is validated exactly once, so the cost is linear in the size
|
|
35
|
+
of the input.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Defaults
|
|
2
|
+
|
|
3
|
+
A default is a recipe. It is certified when the schema compiles and
|
|
4
|
+
rematerialized whenever a build or resolve omits its key.
|
|
5
|
+
|
|
6
|
+
| Default | Rematerialization |
|
|
7
|
+
|---|---|
|
|
8
|
+
| `int`, `float`, `str`, `bool`, `date`, `time`, `None` | passed as is; immutable |
|
|
9
|
+
| Enum member | passed as is; members are singletons |
|
|
10
|
+
| list | fresh list; items rematerialized recursively |
|
|
11
|
+
| dataclass instance | reconstructed through its constructor |
|
|
12
|
+
| `default_factory` | factory called again |
|
|
13
|
+
|
|
14
|
+
Recipes run once at `struct_of`/`signature_of` for certification and once per
|
|
15
|
+
missing-key serving. A provided key never runs its recipe. The served value is
|
|
16
|
+
validated each time, so an impure recipe that drifts outside its schema
|
|
17
|
+
fails with a `default` path segment.
|
|
18
|
+
|
|
19
|
+
Defaults must be pure and deterministic: same recipe, equal result, no shared
|
|
20
|
+
mutable state or observable side effects. The core cannot prove that promise.
|
|
21
|
+
Mutating an externally held recipe object or the certified `field.default`
|
|
22
|
+
violates the same rule from outside and remains the author's responsibility. The
|
|
23
|
+
same promise covers the input: `build` validates it once and constructs from it,
|
|
24
|
+
so the core watches neither concurrent mutation nor a `__post_init__` that edits
|
|
25
|
+
the input data. See [build.md](build.md).
|
|
26
|
+
|
|
27
|
+
Instance reconstruction preserves equality, not internal alias topology. It
|
|
28
|
+
runs `__init__` and `__post_init__` on every serving.
|
|
29
|
+
|
|
30
|
+
Python rejects a non-frozen dataclass instance used directly as a dataclass
|
|
31
|
+
field default before pytypehint runs (`ValueError: mutable default ... is not
|
|
32
|
+
allowed`). Use `field(default_factory=...)`. Function defaults have no such
|
|
33
|
+
Python restriction and may use an instance directly.
|
|
34
|
+
|
|
35
|
+
Calling a function directly still uses Python's shared defaults. Fresh function
|
|
36
|
+
defaults apply only when data passes through `signature_of(fn).resolve(...)` or
|
|
37
|
+
`.build(...)`.
|