typantic 0.1.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.
- typantic-0.1.0/PKG-INFO +139 -0
- typantic-0.1.0/README.md +116 -0
- typantic-0.1.0/pyproject.toml +86 -0
- typantic-0.1.0/src/typantic/__init__.py +8 -0
- typantic-0.1.0/src/typantic/_decorator.py +159 -0
- typantic-0.1.0/src/typantic/py.typed +0 -0
typantic-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: typantic
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Auto-generate Typer CLI interfaces from Pydantic models.
|
|
5
|
+
Keywords: typer,pydantic,cli,validation
|
|
6
|
+
Author: Kilian Schnelle
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Dist: pydantic>=2.0
|
|
16
|
+
Requires-Dist: typer>=0.9
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Project-URL: Homepage, https://github.com/KiSchnelle/typantic
|
|
19
|
+
Project-URL: Repository, https://github.com/KiSchnelle/typantic
|
|
20
|
+
Project-URL: Issues, https://github.com/KiSchnelle/typantic/issues
|
|
21
|
+
Project-URL: Changelog, https://github.com/KiSchnelle/typantic/blob/main/CHANGELOG.md
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# typantic
|
|
25
|
+
|
|
26
|
+
[](https://github.com/KiSchnelle/typantic/actions/workflows/ci.yml)
|
|
27
|
+
[](https://pypi.org/project/typantic/)
|
|
28
|
+
[](https://pypi.org/project/typantic/)
|
|
29
|
+
[](LICENSE)
|
|
30
|
+
|
|
31
|
+
Auto-generate [Typer](https://typer.tiangolo.com/) CLI interfaces from [Pydantic](https://docs.pydantic.dev/) models.
|
|
32
|
+
|
|
33
|
+
Define your config **once** as a Pydantic model with validators, and get a
|
|
34
|
+
fully-typed CLI for free — no duplication, no drift.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install typantic
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
from typing import Annotated
|
|
47
|
+
|
|
48
|
+
import typer
|
|
49
|
+
from pydantic import AfterValidator, BaseModel, Field
|
|
50
|
+
|
|
51
|
+
from typantic import pydantic_to_typer
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# 1. Define your config with validators
|
|
55
|
+
class Config(BaseModel):
|
|
56
|
+
images: Annotated[
|
|
57
|
+
list[Path],
|
|
58
|
+
Field(description="Image folders to process.", kw_only=False),
|
|
59
|
+
]
|
|
60
|
+
output: Annotated[
|
|
61
|
+
Path,
|
|
62
|
+
AfterValidator(Path.resolve),
|
|
63
|
+
Field(description="Output directory.", kw_only=True),
|
|
64
|
+
]
|
|
65
|
+
threshold: Annotated[
|
|
66
|
+
float,
|
|
67
|
+
Field(default=0.5, description="Detection threshold.", kw_only=True),
|
|
68
|
+
]
|
|
69
|
+
seed: Annotated[
|
|
70
|
+
int | None,
|
|
71
|
+
Field(default=None, description="Random seed.", kw_only=True),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# 2. Use the decorator — that's it
|
|
76
|
+
app = typer.Typer()
|
|
77
|
+
|
|
78
|
+
@app.command()
|
|
79
|
+
@pydantic_to_typer(Config)
|
|
80
|
+
def run(config: Config):
|
|
81
|
+
"""Process images with validation."""
|
|
82
|
+
print(config)
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
app()
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
$ python example.py --help
|
|
90
|
+
|
|
91
|
+
Usage: example.py [OPTIONS] IMAGES...
|
|
92
|
+
|
|
93
|
+
Process images with validation.
|
|
94
|
+
|
|
95
|
+
╭─ Arguments ──────────────────────────────────────────────────╮
|
|
96
|
+
│ * images IMAGES... Image folders to process. [required] │
|
|
97
|
+
╰──────────────────────────────────────────────────────────────╯
|
|
98
|
+
╭─ Options ────────────────────────────────────────────────────╮
|
|
99
|
+
│ * --output PATH Output directory. [required] │
|
|
100
|
+
│ --threshold FLOAT Detection threshold. [default: 0.5]│
|
|
101
|
+
│ --seed INTEGER Random seed. │
|
|
102
|
+
│ --help Show this message and exit. │
|
|
103
|
+
╰──────────────────────────────────────────────────────────────╯
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## How it works
|
|
107
|
+
|
|
108
|
+
The `@pydantic_to_typer(Model)` decorator:
|
|
109
|
+
|
|
110
|
+
1. Reads `Model.model_fields` to discover field names, types, descriptions, and defaults
|
|
111
|
+
2. Strips `Annotated` validator metadata to extract the base types Typer understands
|
|
112
|
+
3. Maps `kw_only=False` → `typer.Argument`, `kw_only=True` → `typer.Option`
|
|
113
|
+
4. Rewrites the function's `__signature__` so Typer sees the expanded parameters
|
|
114
|
+
5. At call time, passes the raw CLI values into `Model(...)` so all Pydantic validators run
|
|
115
|
+
|
|
116
|
+
Your function receives the **validated model instance** — validators, `default_factory`, union types, and everything else works exactly as in Pydantic.
|
|
117
|
+
|
|
118
|
+
## Features
|
|
119
|
+
|
|
120
|
+
| Pydantic | CLI result |
|
|
121
|
+
|-----------------------------------|-----------------------------------------|
|
|
122
|
+
| `kw_only=False` | `typer.Argument` (positional) |
|
|
123
|
+
| `kw_only=True` or unset | `typer.Option` (`--flag`) |
|
|
124
|
+
| `Field(description=...)` | `help=...` in the CLI |
|
|
125
|
+
| `Field(default=...)` | Default value shown in `--help` |
|
|
126
|
+
| `Field(default_factory=...)` | Factory called once at decoration time |
|
|
127
|
+
| `int \| None` | Optional CLI option |
|
|
128
|
+
| `list[Path]` | Variadic positional argument |
|
|
129
|
+
| `AfterValidator`, `BeforeValidator` | Run at call time via Pydantic |
|
|
130
|
+
|
|
131
|
+
## Requirements
|
|
132
|
+
|
|
133
|
+
- Python ≥ 3.12
|
|
134
|
+
- Pydantic ≥ 2.0
|
|
135
|
+
- Typer ≥ 0.9
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
typantic-0.1.0/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# typantic
|
|
2
|
+
|
|
3
|
+
[](https://github.com/KiSchnelle/typantic/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/typantic/)
|
|
5
|
+
[](https://pypi.org/project/typantic/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Auto-generate [Typer](https://typer.tiangolo.com/) CLI interfaces from [Pydantic](https://docs.pydantic.dev/) models.
|
|
9
|
+
|
|
10
|
+
Define your config **once** as a Pydantic model with validators, and get a
|
|
11
|
+
fully-typed CLI for free — no duplication, no drift.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install typantic
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Annotated
|
|
24
|
+
|
|
25
|
+
import typer
|
|
26
|
+
from pydantic import AfterValidator, BaseModel, Field
|
|
27
|
+
|
|
28
|
+
from typantic import pydantic_to_typer
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# 1. Define your config with validators
|
|
32
|
+
class Config(BaseModel):
|
|
33
|
+
images: Annotated[
|
|
34
|
+
list[Path],
|
|
35
|
+
Field(description="Image folders to process.", kw_only=False),
|
|
36
|
+
]
|
|
37
|
+
output: Annotated[
|
|
38
|
+
Path,
|
|
39
|
+
AfterValidator(Path.resolve),
|
|
40
|
+
Field(description="Output directory.", kw_only=True),
|
|
41
|
+
]
|
|
42
|
+
threshold: Annotated[
|
|
43
|
+
float,
|
|
44
|
+
Field(default=0.5, description="Detection threshold.", kw_only=True),
|
|
45
|
+
]
|
|
46
|
+
seed: Annotated[
|
|
47
|
+
int | None,
|
|
48
|
+
Field(default=None, description="Random seed.", kw_only=True),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# 2. Use the decorator — that's it
|
|
53
|
+
app = typer.Typer()
|
|
54
|
+
|
|
55
|
+
@app.command()
|
|
56
|
+
@pydantic_to_typer(Config)
|
|
57
|
+
def run(config: Config):
|
|
58
|
+
"""Process images with validation."""
|
|
59
|
+
print(config)
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
app()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
$ python example.py --help
|
|
67
|
+
|
|
68
|
+
Usage: example.py [OPTIONS] IMAGES...
|
|
69
|
+
|
|
70
|
+
Process images with validation.
|
|
71
|
+
|
|
72
|
+
╭─ Arguments ──────────────────────────────────────────────────╮
|
|
73
|
+
│ * images IMAGES... Image folders to process. [required] │
|
|
74
|
+
╰──────────────────────────────────────────────────────────────╯
|
|
75
|
+
╭─ Options ────────────────────────────────────────────────────╮
|
|
76
|
+
│ * --output PATH Output directory. [required] │
|
|
77
|
+
│ --threshold FLOAT Detection threshold. [default: 0.5]│
|
|
78
|
+
│ --seed INTEGER Random seed. │
|
|
79
|
+
│ --help Show this message and exit. │
|
|
80
|
+
╰──────────────────────────────────────────────────────────────╯
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## How it works
|
|
84
|
+
|
|
85
|
+
The `@pydantic_to_typer(Model)` decorator:
|
|
86
|
+
|
|
87
|
+
1. Reads `Model.model_fields` to discover field names, types, descriptions, and defaults
|
|
88
|
+
2. Strips `Annotated` validator metadata to extract the base types Typer understands
|
|
89
|
+
3. Maps `kw_only=False` → `typer.Argument`, `kw_only=True` → `typer.Option`
|
|
90
|
+
4. Rewrites the function's `__signature__` so Typer sees the expanded parameters
|
|
91
|
+
5. At call time, passes the raw CLI values into `Model(...)` so all Pydantic validators run
|
|
92
|
+
|
|
93
|
+
Your function receives the **validated model instance** — validators, `default_factory`, union types, and everything else works exactly as in Pydantic.
|
|
94
|
+
|
|
95
|
+
## Features
|
|
96
|
+
|
|
97
|
+
| Pydantic | CLI result |
|
|
98
|
+
|-----------------------------------|-----------------------------------------|
|
|
99
|
+
| `kw_only=False` | `typer.Argument` (positional) |
|
|
100
|
+
| `kw_only=True` or unset | `typer.Option` (`--flag`) |
|
|
101
|
+
| `Field(description=...)` | `help=...` in the CLI |
|
|
102
|
+
| `Field(default=...)` | Default value shown in `--help` |
|
|
103
|
+
| `Field(default_factory=...)` | Factory called once at decoration time |
|
|
104
|
+
| `int \| None` | Optional CLI option |
|
|
105
|
+
| `list[Path]` | Variadic positional argument |
|
|
106
|
+
| `AfterValidator`, `BeforeValidator` | Run at call time via Pydantic |
|
|
107
|
+
|
|
108
|
+
## Requirements
|
|
109
|
+
|
|
110
|
+
- Python ≥ 3.12
|
|
111
|
+
- Pydantic ≥ 2.0
|
|
112
|
+
- Typer ≥ 0.9
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "typantic"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Auto-generate Typer CLI interfaces from Pydantic models."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
authors = [{ name = "Kilian Schnelle" }]
|
|
9
|
+
keywords = ["typer", "pydantic", "cli", "validation"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
]
|
|
19
|
+
dependencies = ["pydantic>=2.0", "typer>=0.9"]
|
|
20
|
+
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
dev = ["mypy>=2.1.0", "pytest>=7.0", "ruff>=0.15.13"]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/KiSchnelle/typantic"
|
|
26
|
+
Repository = "https://github.com/KiSchnelle/typantic"
|
|
27
|
+
Issues = "https://github.com/KiSchnelle/typantic/issues"
|
|
28
|
+
Changelog = "https://github.com/KiSchnelle/typantic/blob/main/CHANGELOG.md"
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["uv_build>=0.11.1,<0.12.0"]
|
|
32
|
+
build-backend = "uv_build"
|
|
33
|
+
|
|
34
|
+
[tool.uv.build-backend]
|
|
35
|
+
module-root = "src"
|
|
36
|
+
module-name = "typantic"
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|
|
40
|
+
|
|
41
|
+
[tool.ruff]
|
|
42
|
+
target-version = "py314"
|
|
43
|
+
line-length = 88
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint]
|
|
46
|
+
extend-select = ["ALL"]
|
|
47
|
+
ignore = [
|
|
48
|
+
"TD003",
|
|
49
|
+
"FIX002",
|
|
50
|
+
"TD002",
|
|
51
|
+
"TD004",
|
|
52
|
+
"ERA001",
|
|
53
|
+
"COM812",
|
|
54
|
+
"TC002",
|
|
55
|
+
"TC003",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint.per-file-ignores]
|
|
59
|
+
"tests/**" = [
|
|
60
|
+
"S101", # assert is fine in tests
|
|
61
|
+
"D100", # no module docstring needed
|
|
62
|
+
"D101", # no class docstring needed
|
|
63
|
+
"D102", # no method docstring needed
|
|
64
|
+
"D103", # no function docstring needed
|
|
65
|
+
"D104", # no package docstring needed
|
|
66
|
+
"ANN", # no type annotations needed
|
|
67
|
+
"PLR2004", # magic values are fine in tests
|
|
68
|
+
"INP001", # implicit namespace package is fine
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[tool.ruff.lint.pydocstyle]
|
|
72
|
+
convention = "google"
|
|
73
|
+
|
|
74
|
+
[tool.ruff.format]
|
|
75
|
+
docstring-code-format = true
|
|
76
|
+
|
|
77
|
+
[tool.mypy]
|
|
78
|
+
disallow_untyped_defs = true
|
|
79
|
+
disallow_untyped_calls = true
|
|
80
|
+
disallow_incomplete_defs = true
|
|
81
|
+
strict = true
|
|
82
|
+
exclude = '(?:^|/)(tests|manual_tests|dist)(?:/|$)'
|
|
83
|
+
|
|
84
|
+
[tool.pyright]
|
|
85
|
+
ignore = ["manual_tests/**", "dist/**"]
|
|
86
|
+
typeCheckingMode = "strict"
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Core decorator for converting Pydantic models to Typer CLI interfaces."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import types
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from typing import Annotated, Any, Union, get_args, get_origin, get_type_hints
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from pydantic import BaseModel, ValidationError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _extract_base_type(annotation: object) -> object:
|
|
14
|
+
"""Strip ``Annotated`` validator metadata, keeping the structural type.
|
|
15
|
+
|
|
16
|
+
Recursively walks through ``Annotated``, ``Union``, ``list``, and
|
|
17
|
+
``tuple`` wrappers, discarding everything except the base types that
|
|
18
|
+
Typer can interpret.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
annotation: A (possibly nested) type annotation to unwrap.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The base type with all Pydantic validator metadata removed.
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
>>> from typing import Annotated
|
|
28
|
+
>>> from pydantic import AfterValidator, Field
|
|
29
|
+
>>> _extract_base_type(Annotated[float, Field(description="x")])
|
|
30
|
+
<class 'float'>
|
|
31
|
+
"""
|
|
32
|
+
if get_origin(annotation) is Annotated:
|
|
33
|
+
inner = get_args(annotation)[0]
|
|
34
|
+
return _extract_base_type(inner)
|
|
35
|
+
|
|
36
|
+
if get_origin(annotation) in (Union, types.UnionType):
|
|
37
|
+
cleaned = tuple(_extract_base_type(a) for a in get_args(annotation))
|
|
38
|
+
return Union[cleaned] # noqa: UP007
|
|
39
|
+
|
|
40
|
+
if get_origin(annotation) is list:
|
|
41
|
+
args = get_args(annotation)
|
|
42
|
+
if args:
|
|
43
|
+
return list[_extract_base_type(args[0])] # type: ignore[misc]
|
|
44
|
+
|
|
45
|
+
if get_origin(annotation) is tuple:
|
|
46
|
+
args = get_args(annotation)
|
|
47
|
+
if args:
|
|
48
|
+
cleaned = tuple(_extract_base_type(a) for a in args)
|
|
49
|
+
return tuple[cleaned] # type: ignore[valid-type]
|
|
50
|
+
|
|
51
|
+
return annotation
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def pydantic_to_typer(
|
|
55
|
+
model_cls: type[BaseModel],
|
|
56
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
57
|
+
"""Rewrite a function's signature so Typer sees individual CLI params.
|
|
58
|
+
|
|
59
|
+
The parameters are derived from the fields of ``model_cls``.
|
|
60
|
+
|
|
61
|
+
Mapping rules:
|
|
62
|
+
- ``kw_only=False`` -> ``typer.Argument``
|
|
63
|
+
- ``kw_only=True`` (or unset) -> ``typer.Option``
|
|
64
|
+
- ``Field(description=...)`` -> ``help=...``
|
|
65
|
+
- ``Field(default=...)`` -> Typer default value
|
|
66
|
+
- ``Field(default_factory=...)`` -> factory is called once at
|
|
67
|
+
decoration time to supply the Typer default
|
|
68
|
+
|
|
69
|
+
The decorated function receives the **validated** Pydantic model
|
|
70
|
+
instance, so all ``AfterValidator`` / ``BeforeValidator`` logic runs
|
|
71
|
+
as usual.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
model_cls: The Pydantic model class whose fields define the CLI
|
|
75
|
+
parameters.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
A decorator that transforms a ``func(model)`` signature into one
|
|
79
|
+
that Typer can introspect.
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
>>> import typer
|
|
83
|
+
>>> app = typer.Typer()
|
|
84
|
+
>>> @app.command()
|
|
85
|
+
... @pydantic_to_typer(MyConfig)
|
|
86
|
+
... def run(config: MyConfig): ...
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def decorator(
|
|
90
|
+
func: Callable[..., Any],
|
|
91
|
+
) -> Callable[..., Any]:
|
|
92
|
+
new_params: list[inspect.Parameter] = []
|
|
93
|
+
new_annotations: dict[str, object] = {}
|
|
94
|
+
|
|
95
|
+
resolved_hints = get_type_hints(
|
|
96
|
+
model_cls,
|
|
97
|
+
include_extras=True,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
for name, field_info in model_cls.model_fields.items():
|
|
101
|
+
base_type = _extract_base_type(resolved_hints[name])
|
|
102
|
+
help_text = field_info.description or ""
|
|
103
|
+
|
|
104
|
+
typer_meta: typer.models.ArgumentInfo | typer.models.OptionInfo
|
|
105
|
+
if field_info.kw_only is False:
|
|
106
|
+
typer_meta = typer.Argument(
|
|
107
|
+
help=help_text,
|
|
108
|
+
show_default=False,
|
|
109
|
+
)
|
|
110
|
+
else:
|
|
111
|
+
typer_meta = typer.Option(help=help_text)
|
|
112
|
+
|
|
113
|
+
annotated = Annotated[base_type, typer_meta] # type: ignore[valid-type]
|
|
114
|
+
new_annotations[name] = annotated
|
|
115
|
+
|
|
116
|
+
default: object
|
|
117
|
+
if field_info.is_required():
|
|
118
|
+
default = inspect.Parameter.empty
|
|
119
|
+
elif field_info.default_factory is not None:
|
|
120
|
+
default = field_info.default_factory() # type: ignore[call-arg]
|
|
121
|
+
else:
|
|
122
|
+
default = field_info.default
|
|
123
|
+
|
|
124
|
+
new_params.append(
|
|
125
|
+
inspect.Parameter(
|
|
126
|
+
name,
|
|
127
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD
|
|
128
|
+
if field_info.kw_only is False
|
|
129
|
+
else inspect.Parameter.KEYWORD_ONLY,
|
|
130
|
+
default=default,
|
|
131
|
+
annotation=annotated,
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
new_params.sort(
|
|
136
|
+
key=lambda p: (
|
|
137
|
+
p.kind == inspect.Parameter.KEYWORD_ONLY,
|
|
138
|
+
p.default is not inspect.Parameter.empty,
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
@wraps(func)
|
|
143
|
+
def wrapper(**kwargs: object) -> object:
|
|
144
|
+
try:
|
|
145
|
+
model = model_cls(**kwargs)
|
|
146
|
+
except ValidationError as exc:
|
|
147
|
+
messages: list[str] = []
|
|
148
|
+
for err in exc.errors():
|
|
149
|
+
loc = ".".join(str(p) for p in err["loc"])
|
|
150
|
+
msg = str(err["msg"])
|
|
151
|
+
messages.append(f"{loc}: {msg}" if loc else msg)
|
|
152
|
+
raise typer.BadParameter("\n ".join(messages)) from exc
|
|
153
|
+
return func(model)
|
|
154
|
+
|
|
155
|
+
wrapper.__signature__ = inspect.Signature(new_params) # type: ignore[attr-defined]
|
|
156
|
+
wrapper.__annotations__ = new_annotations
|
|
157
|
+
return wrapper
|
|
158
|
+
|
|
159
|
+
return decorator
|
|
File without changes
|