copier-pydantic 0.1.3__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.
- copier_pydantic-0.1.3/LICENSE +21 -0
- copier_pydantic-0.1.3/PKG-INFO +88 -0
- copier_pydantic-0.1.3/README.md +76 -0
- copier_pydantic-0.1.3/pyproject.toml +46 -0
- copier_pydantic-0.1.3/src/copier_pydantic/__init__.py +9 -0
- copier_pydantic-0.1.3/src/copier_pydantic/multiline_validation.py +31 -0
- copier_pydantic-0.1.3/src/copier_pydantic/py.typed +0 -0
- copier_pydantic-0.1.3/src/copier_pydantic/validators.py +132 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Christopher Brown
|
|
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,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: copier-pydantic
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Adds support for Pydantic Models in Copier templates
|
|
5
|
+
Author: Chris Brown
|
|
6
|
+
Author-email: Chris Brown <cbrown1234@hotmail.co.uk>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: copier>=3.6
|
|
10
|
+
Requires-Python: >=3.13
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Copier Pydantic
|
|
14
|
+
|
|
15
|
+
[](https://pypi.org/project/copier-pydantic/)
|
|
16
|
+
|
|
17
|
+
Jinja2 extensions for Copier that enable using Pydantic Models for validation and within templates
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
With pip:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install copier-pydantic
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
With uv:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv tool install copier --with copier-pydantic
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
With pipx:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pipx install copier
|
|
37
|
+
pipx inject copier copier-pydantic
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage with Copier
|
|
41
|
+
|
|
42
|
+
In your copier template configuration:
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
# Add the jinja extensions
|
|
46
|
+
_jinja_extensions:
|
|
47
|
+
- copier_pydantic.MultilineValidation
|
|
48
|
+
- copier_pydantic.PydanticExtension
|
|
49
|
+
|
|
50
|
+
# and exclude the model.py file from your template
|
|
51
|
+
_exclude:
|
|
52
|
+
- models.py
|
|
53
|
+
# or use the best practice of having the template in a sub directory
|
|
54
|
+
_subdirectory: template
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
So your template will look something like this
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
📁 template_root
|
|
61
|
+
├── 📄 models.py
|
|
62
|
+
├── 📄 copier.yml
|
|
63
|
+
└── 📁 template
|
|
64
|
+
├── 📄 {{_copier_conf.answers_file}}.jinja
|
|
65
|
+
└── 📄 ...
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
With `models.py` containing your Pydantic `BaseModel`'s like this
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from pydantic import BaseModel
|
|
72
|
+
|
|
73
|
+
class DatabaseConfig(BaseModel):
|
|
74
|
+
db_host: str
|
|
75
|
+
db_port: int
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
you can then use your model to validate the question input like this
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
validated_example:
|
|
82
|
+
type: yaml
|
|
83
|
+
multiline: true
|
|
84
|
+
default: |
|
|
85
|
+
db_host: 'localhost'
|
|
86
|
+
db_port: 5432
|
|
87
|
+
validator: "{{ validated_example | validate_as(DatabaseConfig) }}"
|
|
88
|
+
```
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Copier Pydantic
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/copier-pydantic/)
|
|
4
|
+
|
|
5
|
+
Jinja2 extensions for Copier that enable using Pydantic Models for validation and within templates
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
With pip:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install copier-pydantic
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
With uv:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv tool install copier --with copier-pydantic
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
With pipx:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pipx install copier
|
|
25
|
+
pipx inject copier copier-pydantic
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage with Copier
|
|
29
|
+
|
|
30
|
+
In your copier template configuration:
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
# Add the jinja extensions
|
|
34
|
+
_jinja_extensions:
|
|
35
|
+
- copier_pydantic.MultilineValidation
|
|
36
|
+
- copier_pydantic.PydanticExtension
|
|
37
|
+
|
|
38
|
+
# and exclude the model.py file from your template
|
|
39
|
+
_exclude:
|
|
40
|
+
- models.py
|
|
41
|
+
# or use the best practice of having the template in a sub directory
|
|
42
|
+
_subdirectory: template
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
So your template will look something like this
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
📁 template_root
|
|
49
|
+
├── 📄 models.py
|
|
50
|
+
├── 📄 copier.yml
|
|
51
|
+
└── 📁 template
|
|
52
|
+
├── 📄 {{_copier_conf.answers_file}}.jinja
|
|
53
|
+
└── 📄 ...
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
With `models.py` containing your Pydantic `BaseModel`'s like this
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from pydantic import BaseModel
|
|
60
|
+
|
|
61
|
+
class DatabaseConfig(BaseModel):
|
|
62
|
+
db_host: str
|
|
63
|
+
db_port: int
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
you can then use your model to validate the question input like this
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
validated_example:
|
|
70
|
+
type: yaml
|
|
71
|
+
multiline: true
|
|
72
|
+
default: |
|
|
73
|
+
db_host: 'localhost'
|
|
74
|
+
db_port: 5432
|
|
75
|
+
validator: "{{ validated_example | validate_as(DatabaseConfig) }}"
|
|
76
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "copier-pydantic"
|
|
3
|
+
version = "0.1.3"
|
|
4
|
+
description = "Adds support for Pydantic Models in Copier templates"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Chris Brown", email = "cbrown1234@hotmail.co.uk" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"copier>=3.6",
|
|
12
|
+
]
|
|
13
|
+
license = "MIT"
|
|
14
|
+
license-files = ["LICEN[CS]E*"]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["uv_build>=0.9.16,<0.10.0"]
|
|
18
|
+
build-backend = "uv_build"
|
|
19
|
+
|
|
20
|
+
[tool.ruff]
|
|
21
|
+
line-length = 88
|
|
22
|
+
target-version = "py313"
|
|
23
|
+
|
|
24
|
+
[tool.ruff.lint]
|
|
25
|
+
select = ["ALL"]
|
|
26
|
+
ignore = [
|
|
27
|
+
"D203",
|
|
28
|
+
"D213",
|
|
29
|
+
"Q000",
|
|
30
|
+
"Q003",
|
|
31
|
+
"COM812",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[tool.ruff.lint.extend-per-file-ignores]
|
|
35
|
+
"src/copier_pydantic/validators.py" = [
|
|
36
|
+
"ANN401",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.ruff.format]
|
|
40
|
+
quote-style = "single"
|
|
41
|
+
|
|
42
|
+
[[tool.uv.index]]
|
|
43
|
+
name = "testpypi"
|
|
44
|
+
url = "https://test.pypi.org/simple/"
|
|
45
|
+
publish-url = "https://test.pypi.org/legacy/"
|
|
46
|
+
explicit = true
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Enable multiline validation messages with Copier."""
|
|
2
|
+
|
|
3
|
+
import prompt_toolkit
|
|
4
|
+
from jinja2 import Environment
|
|
5
|
+
from jinja2.ext import Extension
|
|
6
|
+
|
|
7
|
+
_validation_toolbar_init = prompt_toolkit.widgets.ValidationToolbar.__init__
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _validation_toolbar_init_patched(
|
|
11
|
+
self: prompt_toolkit.widgets.ValidationToolbar,
|
|
12
|
+
*args: tuple,
|
|
13
|
+
**kwargs: dict,
|
|
14
|
+
) -> None:
|
|
15
|
+
"""Wrap ValidationToolbar __init__ for patching."""
|
|
16
|
+
_validation_toolbar_init(self, *args, **kwargs)
|
|
17
|
+
|
|
18
|
+
# Uncap validation message length to enable multiline error messages
|
|
19
|
+
self.container.content.height = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MultilineValidation(Extension):
|
|
23
|
+
"""Enable multiline error messages for Copier by loading."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, environment: Environment) -> None:
|
|
26
|
+
"""Implement extension logic."""
|
|
27
|
+
super().__init__(environment)
|
|
28
|
+
|
|
29
|
+
prompt_toolkit.widgets.ValidationToolbar.__init__ = (
|
|
30
|
+
_validation_toolbar_init_patched
|
|
31
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Pydantic model integration helpers."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import inspect
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from jinja2 import Environment
|
|
9
|
+
from jinja2.ext import Extension
|
|
10
|
+
from pydantic import BaseModel, RootModel, ValidationError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _to_model(value: Any, model: type[BaseModel], **kwargs: dict) -> BaseModel:
|
|
14
|
+
"""Jinja filter for instantiating model instance.
|
|
15
|
+
|
|
16
|
+
`{{ value | to_model(Example) }}`
|
|
17
|
+
"""
|
|
18
|
+
return model.model_validate(value, strict=True, **kwargs)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _to_model_dict(value: Any, model: type[BaseModel], **kwargs: dict) -> dict:
|
|
22
|
+
"""Jinja filter for instantiating model instance as dictionary.
|
|
23
|
+
|
|
24
|
+
`{{ value | to_model_dict(Example) }}`
|
|
25
|
+
|
|
26
|
+
Useful if you want simpler usage in jinja, but supports default values etc
|
|
27
|
+
"""
|
|
28
|
+
return model.model_validate(value, strict=True, **kwargs).model_dump()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _validate_as(value: Any, model: type[BaseModel]) -> str:
|
|
32
|
+
"""Jinja filter for model validation string in copier.yml.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
input:
|
|
36
|
+
type: yaml
|
|
37
|
+
multiline: true
|
|
38
|
+
validator: {{ input | validate_as(Example) }}
|
|
39
|
+
```
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
model.model_validate(value, strict=True)
|
|
43
|
+
except ValidationError as err:
|
|
44
|
+
return str(err)
|
|
45
|
+
return ''
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _is_valid_as(model: type[BaseModel]) -> callable:
|
|
49
|
+
"""Construct jinja test for compatibility with model."""
|
|
50
|
+
|
|
51
|
+
def test(value: dict) -> bool:
|
|
52
|
+
try:
|
|
53
|
+
model.model_validate(value, strict=True)
|
|
54
|
+
except ValidationError:
|
|
55
|
+
return False
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
return test
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _is_model_instance_test(model: type[BaseModel]) -> callable:
|
|
62
|
+
"""Construct jinja test for model instance."""
|
|
63
|
+
|
|
64
|
+
def test(value: Any) -> bool:
|
|
65
|
+
return isinstance(value, model)
|
|
66
|
+
|
|
67
|
+
return test
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Source: https://stackoverflow.com/questions/5362771/how-to-load-a-module-from-code-in-a-string
|
|
71
|
+
def _import_module_from_string(name: str, source: str) -> None:
|
|
72
|
+
"""Import module from source string.
|
|
73
|
+
|
|
74
|
+
Example use:
|
|
75
|
+
import_module_from_string("m", "f = lambda: print('hello')")
|
|
76
|
+
m.f()
|
|
77
|
+
"""
|
|
78
|
+
spec = importlib.util.spec_from_loader(name, loader=None)
|
|
79
|
+
module = importlib.util.module_from_spec(spec)
|
|
80
|
+
exec(source, module.__dict__) # noqa: S102
|
|
81
|
+
sys.modules[name] = module
|
|
82
|
+
globals()[name] = module
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _is_pydantic_model(obj: Any) -> bool:
|
|
86
|
+
"""Check if pydantic model itself (not instance)."""
|
|
87
|
+
return (
|
|
88
|
+
inspect.isclass(obj)
|
|
89
|
+
and issubclass(obj, BaseModel)
|
|
90
|
+
and obj not in (BaseModel, RootModel)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class PydanticExtension(Extension):
|
|
95
|
+
"""Adds Pydantic models and helpers."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, environment: Environment) -> None:
|
|
98
|
+
"""Implement extension logic."""
|
|
99
|
+
super().__init__(environment)
|
|
100
|
+
|
|
101
|
+
self._load_models()
|
|
102
|
+
|
|
103
|
+
# Enable lookup via alias
|
|
104
|
+
environment.globals['models'] = self.models
|
|
105
|
+
|
|
106
|
+
for model in self.models.values():
|
|
107
|
+
# Add models to namespace for use with generic functions
|
|
108
|
+
environment.globals[model.__name__] = model
|
|
109
|
+
|
|
110
|
+
# Add generic functions
|
|
111
|
+
# `{{ input | to_model(Example) }}` e.g. for default values
|
|
112
|
+
environment.filters['to_model'] = _to_model
|
|
113
|
+
# `{{ input | to_model_dict(Example) }}` e.g. for default values
|
|
114
|
+
environment.filters['to_model_dict'] = _to_model_dict
|
|
115
|
+
# `validator: {{ input | validate_as(Example) }}`
|
|
116
|
+
environment.filters['validate_as'] = _validate_as
|
|
117
|
+
|
|
118
|
+
# Pre-create other helpers
|
|
119
|
+
# `{% if input is Example %}`
|
|
120
|
+
environment.tests[f'{model.__name__}'] = _is_valid_as(model)
|
|
121
|
+
# `{% if input is Example_Model %}`
|
|
122
|
+
environment.tests[f'{model.__name__}_Model'] = _is_model_instance_test(
|
|
123
|
+
model,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def _load_models(self) -> None:
|
|
127
|
+
models_code, *_ = self.environment.loader.get_source(
|
|
128
|
+
self.environment,
|
|
129
|
+
'models.py',
|
|
130
|
+
)
|
|
131
|
+
_import_module_from_string('models_module', models_code)
|
|
132
|
+
self.models = dict(inspect.getmembers(models_module, _is_pydantic_model)) # noqa: F821
|