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.
Files changed (93) hide show
  1. pytypehint-0.0.1/.github/dependabot.yml +6 -0
  2. pytypehint-0.0.1/.github/workflows/ci.yml +28 -0
  3. pytypehint-0.0.1/.github/workflows/publish.yml +24 -0
  4. pytypehint-0.0.1/.gitignore +11 -0
  5. pytypehint-0.0.1/.hypothesis/.gitignore +9 -0
  6. pytypehint-0.0.1/.hypothesis/constants/2678b14aa369a75b +4 -0
  7. pytypehint-0.0.1/.hypothesis/constants/394532c786c7fa39 +4 -0
  8. pytypehint-0.0.1/.hypothesis/constants/417463f20a0e2f4a +4 -0
  9. pytypehint-0.0.1/.hypothesis/constants/7ea8177d35d3f469 +4 -0
  10. pytypehint-0.0.1/.hypothesis/constants/9260f428ce0a0a9c +4 -0
  11. pytypehint-0.0.1/.hypothesis/constants/ad3acb7981ed8c4e +4 -0
  12. pytypehint-0.0.1/.hypothesis/constants/b60ecb6f6243dd23 +4 -0
  13. pytypehint-0.0.1/.hypothesis/constants/be68644653efe775 +4 -0
  14. pytypehint-0.0.1/.hypothesis/constants/edf98ac0cc5cb0cb +4 -0
  15. pytypehint-0.0.1/.hypothesis/constants/f49b7bbd9251f442 +4 -0
  16. pytypehint-0.0.1/CHANGELOG.md +7 -0
  17. pytypehint-0.0.1/LICENSE +21 -0
  18. pytypehint-0.0.1/PKG-INFO +112 -0
  19. pytypehint-0.0.1/README.md +98 -0
  20. pytypehint-0.0.1/docs/atoms.md +80 -0
  21. pytypehint-0.0.1/docs/build.md +96 -0
  22. pytypehint-0.0.1/docs/comparison.md +35 -0
  23. pytypehint-0.0.1/docs/defaults.md +37 -0
  24. pytypehint-0.0.1/docs/philosophy.md +88 -0
  25. pytypehint-0.0.1/docs/resolve.md +30 -0
  26. pytypehint-0.0.1/docs/restrictions.md +209 -0
  27. pytypehint-0.0.1/docs/vocabulary.md +60 -0
  28. pytypehint-0.0.1/pyproject.toml +34 -0
  29. pytypehint-0.0.1/src/pytypehint/__init__.py +58 -0
  30. pytypehint-0.0.1/src/pytypehint/atoms.py +220 -0
  31. pytypehint-0.0.1/src/pytypehint/bridge.py +309 -0
  32. pytypehint-0.0.1/src/pytypehint/errors.py +56 -0
  33. pytypehint-0.0.1/src/pytypehint/py.typed +0 -0
  34. pytypehint-0.0.1/src/pytypehint/shapes.py +586 -0
  35. pytypehint-0.0.1/src/pytypehint/signature.py +29 -0
  36. pytypehint-0.0.1/src/pytypehint/structure.py +351 -0
  37. pytypehint-0.0.1/src/pytypehint/utils.py +20 -0
  38. pytypehint-0.0.1/src/pytypehint/validation.py +10 -0
  39. pytypehint-0.0.1/tests/test_annotated.py +555 -0
  40. pytypehint-0.0.1/tests/test_annotated_hoisting_properties.py +329 -0
  41. pytypehint-0.0.1/tests/test_atom_table_reflection.py +233 -0
  42. pytypehint-0.0.1/tests/test_atoms.py +118 -0
  43. pytypehint-0.0.1/tests/test_bool.py +195 -0
  44. pytypehint-0.0.1/tests/test_bridge.py +290 -0
  45. pytypehint-0.0.1/tests/test_build.py +95 -0
  46. pytypehint-0.0.1/tests/test_certification.py +94 -0
  47. pytypehint-0.0.1/tests/test_composition.py +189 -0
  48. pytypehint-0.0.1/tests/test_datetime.py +266 -0
  49. pytypehint-0.0.1/tests/test_default_types.py +127 -0
  50. pytypehint-0.0.1/tests/test_discriminator.py +105 -0
  51. pytypehint-0.0.1/tests/test_edges1.py +596 -0
  52. pytypehint-0.0.1/tests/test_enum.py +256 -0
  53. pytypehint-0.0.1/tests/test_equality.py +35 -0
  54. pytypehint-0.0.1/tests/test_errors_pickle.py +200 -0
  55. pytypehint-0.0.1/tests/test_extra.py +136 -0
  56. pytypehint-0.0.1/tests/test_extra1.py +224 -0
  57. pytypehint-0.0.1/tests/test_extra10.py +243 -0
  58. pytypehint-0.0.1/tests/test_extra2.py +255 -0
  59. pytypehint-0.0.1/tests/test_extra3.py +150 -0
  60. pytypehint-0.0.1/tests/test_extra4.py +282 -0
  61. pytypehint-0.0.1/tests/test_extra5.py +213 -0
  62. pytypehint-0.0.1/tests/test_extra6.py +172 -0
  63. pytypehint-0.0.1/tests/test_extra7.py +85 -0
  64. pytypehint-0.0.1/tests/test_extra8.py +161 -0
  65. pytypehint-0.0.1/tests/test_extra9.py +241 -0
  66. pytypehint-0.0.1/tests/test_field_dataclass_surface.py +123 -0
  67. pytypehint-0.0.1/tests/test_final_restrictions.py +127 -0
  68. pytypehint-0.0.1/tests/test_float.py +234 -0
  69. pytypehint-0.0.1/tests/test_initvar_rejection.py +138 -0
  70. pytypehint-0.0.1/tests/test_int.py +713 -0
  71. pytypehint-0.0.1/tests/test_list.py +615 -0
  72. pytypehint-0.0.1/tests/test_literal.py +214 -0
  73. pytypehint-0.0.1/tests/test_missing.py +23 -0
  74. pytypehint-0.0.1/tests/test_none.py +471 -0
  75. pytypehint-0.0.1/tests/test_none_transparency.py +170 -0
  76. pytypehint-0.0.1/tests/test_optional_toggle.py +125 -0
  77. pytypehint-0.0.1/tests/test_presentation_atoms_surface.py +101 -0
  78. pytypehint-0.0.1/tests/test_recursive.py +225 -0
  79. pytypehint-0.0.1/tests/test_remat_runtime_failure.py +253 -0
  80. pytypehint-0.0.1/tests/test_rematerialization.py +242 -0
  81. pytypehint-0.0.1/tests/test_resolve.py +228 -0
  82. pytypehint-0.0.1/tests/test_signature.py +514 -0
  83. pytypehint-0.0.1/tests/test_signature_build.py +33 -0
  84. pytypehint-0.0.1/tests/test_signature_field_rejection.py +36 -0
  85. pytypehint-0.0.1/tests/test_single_pass_validation.py +352 -0
  86. pytypehint-0.0.1/tests/test_str.py +328 -0
  87. pytypehint-0.0.1/tests/test_struct.py +457 -0
  88. pytypehint-0.0.1/tests/test_structured_errors.py +224 -0
  89. pytypehint-0.0.1/tests/test_union_field_atoms.py +103 -0
  90. pytypehint-0.0.1/tests/test_union_option_collisions.py +211 -0
  91. pytypehint-0.0.1/tests/test_unions.py +382 -0
  92. pytypehint-0.0.1/tests/test_unions_extra.py +393 -0
  93. pytypehint-0.0.1/tests/test_validate.py +323 -0
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
@@ -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,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ build/
4
+ dist/
5
+ *.egg-info/
6
+ .venv/
7
+ venv/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .vscode/
11
+ .idea/
@@ -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/signature.py
2
+ # hypothesis_version: 6.156.6
3
+
4
+ ['argument']
@@ -0,0 +1,4 @@
1
+ # file: /opt/hostedtoolcache/Python/3.11.15/x64/bin/pytest
2
+ # hypothesis_version: 6.156.6
3
+
4
+ ['-script.pyw', '.exe', '__main__']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/pytypehint/pytypehint/src/pytypehint/errors.py
2
+ # hypothesis_version: 6.156.6
3
+
4
+ []
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/pytypehint/pytypehint/src/pytypehint/shapes.py
2
+ # hypothesis_version: 6.156.6
3
+
4
+ ['_compiled', 'choices', 'extra', 'is_password', 'is_path_file', 'max', 'min', 'multiple_of', 'pattern', 'placeholder', 'pytype', 'rows', 'slider', 'step']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/pytypehint/pytypehint/src/pytypehint/validation.py
2
+ # hypothesis_version: 6.156.6
3
+
4
+ [' | ']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/pytypehint/pytypehint/src/pytypehint/utils.py
2
+ # hypothesis_version: 6.156.6
3
+
4
+ ['MISSING']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/pytypehint/pytypehint/src/pytypehint/atoms.py
2
+ # hypothesis_version: 6.156.6
3
+
4
+ ['.']
@@ -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']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/pytypehint/pytypehint/src/pytypehint/bridge.py
2
+ # hypothesis_version: 6.156.6
3
+
4
+ ['<lambda>', '_deferred', 'cls', 'dataclass', 'default', 'enum', 'fields', 'list', 'self', 'typing.']
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [0.0.1] - 2026-07-15
6
+
7
+ - Initial release.
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/pytypehint.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/pytypehint.svg)](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(...)`.