makejinja 2.3.5__tar.gz → 2.5.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.
- {makejinja-2.3.5 → makejinja-2.5.0}/PKG-INFO +27 -32
- {makejinja-2.3.5 → makejinja-2.5.0}/README.md +24 -29
- {makejinja-2.3.5 → makejinja-2.5.0}/makejinja/__init__.py +4 -2
- {makejinja-2.3.5 → makejinja-2.5.0}/makejinja/app.py +93 -62
- {makejinja-2.3.5 → makejinja-2.5.0}/makejinja/config.py +36 -8
- makejinja-2.3.5/makejinja/loader.py → makejinja-2.5.0/makejinja/plugin.py +17 -18
- {makejinja-2.3.5 → makejinja-2.5.0}/pyproject.toml +7 -3
- {makejinja-2.3.5 → makejinja-2.5.0}/LICENSE +0 -0
- {makejinja-2.3.5 → makejinja-2.5.0}/makejinja/__main__.py +0 -0
- {makejinja-2.3.5 → makejinja-2.5.0}/makejinja/cli.py +0 -0
- {makejinja-2.3.5 → makejinja-2.5.0}/makejinja/py.typed +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: makejinja
|
|
3
|
-
Version: 2.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 2.5.0
|
|
4
|
+
Summary: Generate entire directory structures using Jinja templates with support for external data and custom plugins.
|
|
5
5
|
Home-page: https://mirkolenz.github.io/makejinja/
|
|
6
6
|
License: MIT
|
|
7
7
|
Keywords: jinja2,home-assistant,hassio,dashboard,lovelace,template,generator,cli,tool,library
|
|
@@ -33,24 +33,25 @@ Requires-Dist: immutables (>=0.20,<0.21)
|
|
|
33
33
|
Requires-Dist: jinja2 (>=3.1,<4.0)
|
|
34
34
|
Requires-Dist: pyyaml (>=6.0,<7.0)
|
|
35
35
|
Requires-Dist: rich-click (>=1.7,<2.0)
|
|
36
|
-
Requires-Dist: typed-settings[attrs,cattrs,click] (>=23.1,<
|
|
36
|
+
Requires-Dist: typed-settings[attrs,cattrs,click] (>=23.1,<25)
|
|
37
37
|
Project-URL: Documentation, https://mirkolenz.github.io/makejinja/makejinja/cli.html
|
|
38
38
|
Project-URL: Repository, https://github.com/mirkolenz/makejinja
|
|
39
39
|
Description-Content-Type: text/markdown
|
|
40
40
|
|
|
41
41
|
<!-- markdownlint-disable MD033 MD041 -->
|
|
42
|
+
<h2><p align="center">makejinja</p></h2>
|
|
42
43
|
<p align="center">
|
|
43
44
|
<img width="256px" alt="makejinja logo" src="https://raw.githubusercontent.com/mirkolenz/makejinja/main/assets/logo.png" />
|
|
44
45
|
</p>
|
|
45
46
|
<p align="center">
|
|
46
47
|
<a href="https://pypi.org/project/makejinja/">PyPI</a> |
|
|
48
|
+
<a href="https://github.com/users/mirkolenz/packages/container/package/makejinja">Docker</a> |
|
|
47
49
|
<a href="https://mirkolenz.github.io/makejinja">Docs</a> |
|
|
48
50
|
<a href="https://github.com/mirkolenz/makejinja/tree/main/tests/data">Example</a> |
|
|
49
|
-
<a href="https://jinja.palletsprojects.com/en/3.1.x/templates">
|
|
51
|
+
<a href="https://jinja.palletsprojects.com/en/3.1.x/templates">Jinja reference</a>
|
|
50
52
|
</p>
|
|
51
53
|
<p align="center">
|
|
52
|
-
|
|
53
|
-
Use it to easily generate complex Home Assistant dashboards!
|
|
54
|
+
Generate entire directory structures using Jinja templates with support for external data and custom plugins.
|
|
54
55
|
</p>
|
|
55
56
|
|
|
56
57
|
---
|
|
@@ -58,22 +59,18 @@ Description-Content-Type: text/markdown
|
|
|
58
59
|
# makejinja
|
|
59
60
|
|
|
60
61
|
makejinja can be used to automatically generate files from [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates).
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
It is conceptually similar to [Ansible templates](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/template_module.html) since both are built on top of Jinja.
|
|
63
|
+
However, makejinja is a standalone tool that can be used without Ansible and offers some advanced features like custom plugins.
|
|
63
64
|
A use case for this tool is generating config files for [Home Assistant](https://www.home-assistant.io/):
|
|
64
65
|
Using the same language that the built-in templates use, you can greatly simplify your configuration.
|
|
65
66
|
An [example for Home Assistant](https://github.com/mirkolenz/makejinja/tree/main/tests/data) can be found in the tests directory.
|
|
66
67
|
|
|
67
|
-
##
|
|
68
|
+
## Highlights
|
|
68
69
|
|
|
69
|
-
- Recursively
|
|
70
|
+
- Recursively render nested directories containing template files to a common output directory.
|
|
70
71
|
- Load data files containing variables to use in your Jinja templates from YAML, TOML, and Python files.
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
- Tailor the whitespace behavior to your needs.
|
|
74
|
-
- Use custom delimiters for Jinja blocks/comments/variables.
|
|
75
|
-
- Modify _all_ init options for the Jinja environment.
|
|
76
|
-
- Write custom **Python loaders** that implement a subset of our fully typed [abstract loader class](https://mirkolenz.github.io/makejinja/makejinja/loader.html#AbstractLoader)
|
|
72
|
+
- Write custom [plugins](https://mirkolenz.github.io/makejinja/makejinja/plugin.html#Plugin) to extend the functionality of makejinja.
|
|
73
|
+
- Adjust all Jinja options (e.g., whitespace behavior and delimiters) to your needs via CLI flags or a config file.
|
|
77
74
|
|
|
78
75
|
## Installation
|
|
79
76
|
|
|
@@ -82,32 +79,30 @@ It can be used as a CLI tool or as a Python library.
|
|
|
82
79
|
|
|
83
80
|
### PIP
|
|
84
81
|
|
|
85
|
-
makejinja is available
|
|
82
|
+
makejinja is available on [PyPI](https://pypi.org/project/makejinja/) and can be installed via `pip`:
|
|
86
83
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`pipx install makejinja`
|
|
93
|
-
|
|
94
|
-
You can then directly invoke the app as follows:
|
|
95
|
-
|
|
96
|
-
`makejinja -i ./input -o ./output`
|
|
84
|
+
```shell
|
|
85
|
+
pip install makejinja
|
|
86
|
+
makejinja -i ./input -o ./output
|
|
87
|
+
```
|
|
97
88
|
|
|
98
89
|
### Nix
|
|
99
90
|
|
|
100
91
|
If you use the `nix` package manager, you can add this repository as an input to your flake and use `makejinja.packages.${system}.default` or apply the overlay `makejinja.overlays.default`.
|
|
101
|
-
You can also run it directly
|
|
92
|
+
You can also run it directly as follows:
|
|
102
93
|
|
|
103
|
-
|
|
94
|
+
```shell
|
|
95
|
+
nix run github:mirkolenz/makejinja -- -i ./input -o ./output
|
|
96
|
+
```
|
|
104
97
|
|
|
105
98
|
### Docker
|
|
106
99
|
|
|
107
|
-
We automatically publish an image
|
|
108
|
-
To use it, mount a directory to the container and pass the options as the command
|
|
100
|
+
We automatically publish an image to the [GitHub Container Registry](https://ghcr.io/mirkolenz/makejinja).
|
|
101
|
+
To use it, mount a directory to the container and pass the options as the command:
|
|
109
102
|
|
|
110
|
-
|
|
103
|
+
```shell
|
|
104
|
+
docker run --rm -v $(pwd)/data:/data ghcr.io/mirkolenz/makejinja:latest -i /data/input -o /data/output
|
|
105
|
+
```
|
|
111
106
|
|
|
112
107
|
## Usage in Terminal / Command Line
|
|
113
108
|
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
<!-- markdownlint-disable MD033 MD041 -->
|
|
2
|
+
<h2><p align="center">makejinja</p></h2>
|
|
2
3
|
<p align="center">
|
|
3
4
|
<img width="256px" alt="makejinja logo" src="https://raw.githubusercontent.com/mirkolenz/makejinja/main/assets/logo.png" />
|
|
4
5
|
</p>
|
|
5
6
|
<p align="center">
|
|
6
7
|
<a href="https://pypi.org/project/makejinja/">PyPI</a> |
|
|
8
|
+
<a href="https://github.com/users/mirkolenz/packages/container/package/makejinja">Docker</a> |
|
|
7
9
|
<a href="https://mirkolenz.github.io/makejinja">Docs</a> |
|
|
8
10
|
<a href="https://github.com/mirkolenz/makejinja/tree/main/tests/data">Example</a> |
|
|
9
|
-
<a href="https://jinja.palletsprojects.com/en/3.1.x/templates">
|
|
11
|
+
<a href="https://jinja.palletsprojects.com/en/3.1.x/templates">Jinja reference</a>
|
|
10
12
|
</p>
|
|
11
13
|
<p align="center">
|
|
12
|
-
|
|
13
|
-
Use it to easily generate complex Home Assistant dashboards!
|
|
14
|
+
Generate entire directory structures using Jinja templates with support for external data and custom plugins.
|
|
14
15
|
</p>
|
|
15
16
|
|
|
16
17
|
---
|
|
@@ -18,22 +19,18 @@
|
|
|
18
19
|
# makejinja
|
|
19
20
|
|
|
20
21
|
makejinja can be used to automatically generate files from [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates).
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
It is conceptually similar to [Ansible templates](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/template_module.html) since both are built on top of Jinja.
|
|
23
|
+
However, makejinja is a standalone tool that can be used without Ansible and offers some advanced features like custom plugins.
|
|
23
24
|
A use case for this tool is generating config files for [Home Assistant](https://www.home-assistant.io/):
|
|
24
25
|
Using the same language that the built-in templates use, you can greatly simplify your configuration.
|
|
25
26
|
An [example for Home Assistant](https://github.com/mirkolenz/makejinja/tree/main/tests/data) can be found in the tests directory.
|
|
26
27
|
|
|
27
|
-
##
|
|
28
|
+
## Highlights
|
|
28
29
|
|
|
29
|
-
- Recursively
|
|
30
|
+
- Recursively render nested directories containing template files to a common output directory.
|
|
30
31
|
- Load data files containing variables to use in your Jinja templates from YAML, TOML, and Python files.
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
- Tailor the whitespace behavior to your needs.
|
|
34
|
-
- Use custom delimiters for Jinja blocks/comments/variables.
|
|
35
|
-
- Modify _all_ init options for the Jinja environment.
|
|
36
|
-
- Write custom **Python loaders** that implement a subset of our fully typed [abstract loader class](https://mirkolenz.github.io/makejinja/makejinja/loader.html#AbstractLoader)
|
|
32
|
+
- Write custom [plugins](https://mirkolenz.github.io/makejinja/makejinja/plugin.html#Plugin) to extend the functionality of makejinja.
|
|
33
|
+
- Adjust all Jinja options (e.g., whitespace behavior and delimiters) to your needs via CLI flags or a config file.
|
|
37
34
|
|
|
38
35
|
## Installation
|
|
39
36
|
|
|
@@ -42,32 +39,30 @@ It can be used as a CLI tool or as a Python library.
|
|
|
42
39
|
|
|
43
40
|
### PIP
|
|
44
41
|
|
|
45
|
-
makejinja is available
|
|
42
|
+
makejinja is available on [PyPI](https://pypi.org/project/makejinja/) and can be installed via `pip`:
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
`pipx install makejinja`
|
|
53
|
-
|
|
54
|
-
You can then directly invoke the app as follows:
|
|
55
|
-
|
|
56
|
-
`makejinja -i ./input -o ./output`
|
|
44
|
+
```shell
|
|
45
|
+
pip install makejinja
|
|
46
|
+
makejinja -i ./input -o ./output
|
|
47
|
+
```
|
|
57
48
|
|
|
58
49
|
### Nix
|
|
59
50
|
|
|
60
51
|
If you use the `nix` package manager, you can add this repository as an input to your flake and use `makejinja.packages.${system}.default` or apply the overlay `makejinja.overlays.default`.
|
|
61
|
-
You can also run it directly
|
|
52
|
+
You can also run it directly as follows:
|
|
62
53
|
|
|
63
|
-
|
|
54
|
+
```shell
|
|
55
|
+
nix run github:mirkolenz/makejinja -- -i ./input -o ./output
|
|
56
|
+
```
|
|
64
57
|
|
|
65
58
|
### Docker
|
|
66
59
|
|
|
67
|
-
We automatically publish an image
|
|
68
|
-
To use it, mount a directory to the container and pass the options as the command
|
|
60
|
+
We automatically publish an image to the [GitHub Container Registry](https://ghcr.io/mirkolenz/makejinja).
|
|
61
|
+
To use it, mount a directory to the container and pass the options as the command:
|
|
69
62
|
|
|
70
|
-
|
|
63
|
+
```shell
|
|
64
|
+
docker run --rm -v $(pwd)/data:/data ghcr.io/mirkolenz/makejinja:latest -i /data/input -o /data/output
|
|
65
|
+
```
|
|
71
66
|
|
|
72
67
|
## Usage in Terminal / Command Line
|
|
73
68
|
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
While mainly intended to be used as a command line tool, makejinja can also be from Python directly.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
from . import cli, config,
|
|
13
|
+
from . import cli, config, plugin
|
|
14
14
|
from .app import makejinja
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
loader = plugin
|
|
17
|
+
|
|
18
|
+
__all__ = ["makejinja", "config", "plugin", "loader", "cli"]
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import itertools
|
|
1
2
|
import json
|
|
2
3
|
import os
|
|
3
4
|
import shutil
|
|
5
|
+
import subprocess
|
|
4
6
|
import sys
|
|
5
7
|
import tomllib
|
|
6
8
|
from collections import abc
|
|
@@ -15,22 +17,36 @@ from jinja2.utils import import_string
|
|
|
15
17
|
from rich import print
|
|
16
18
|
|
|
17
19
|
from makejinja.config import Config
|
|
18
|
-
from makejinja.
|
|
20
|
+
from makejinja.plugin import Data, MutableData, PathFilter, Plugin
|
|
19
21
|
|
|
20
22
|
__all__ = ["makejinja"]
|
|
21
23
|
|
|
24
|
+
STDOUT_PATH = Path("/dev/stdout").resolve()
|
|
25
|
+
STDIN_PATH = Path("/dev/stdin").resolve()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def log(message: str, config: Config) -> None:
|
|
29
|
+
if not config.quiet and config.output != STDOUT_PATH:
|
|
30
|
+
print(message)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def exec(cmd: str) -> None:
|
|
34
|
+
subprocess.run(cmd, shell=True, check=True)
|
|
35
|
+
|
|
22
36
|
|
|
23
37
|
def makejinja(config: Config):
|
|
24
38
|
"""makejinja can be used to automatically generate files from [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/)."""
|
|
25
39
|
|
|
40
|
+
for cmd in config.exec_pre:
|
|
41
|
+
exec(cmd)
|
|
42
|
+
|
|
26
43
|
for path in config.import_paths:
|
|
27
44
|
sys.path.append(str(path.resolve()))
|
|
28
45
|
|
|
29
46
|
data = load_data(config)
|
|
30
47
|
|
|
31
48
|
if config.output.is_dir() and config.clean:
|
|
32
|
-
|
|
33
|
-
print(f"Remove output '{config.output}'")
|
|
49
|
+
log(f"Remove output '{config.output}'", config)
|
|
34
50
|
|
|
35
51
|
shutil.rmtree(config.output)
|
|
36
52
|
|
|
@@ -38,9 +54,16 @@ def makejinja(config: Config):
|
|
|
38
54
|
config.output.mkdir(exist_ok=True, parents=True)
|
|
39
55
|
|
|
40
56
|
env = init_jinja_env(config, data)
|
|
57
|
+
plugins: list[Plugin] = []
|
|
58
|
+
|
|
59
|
+
for plugin_name in itertools.chain(config.plugins, config.loaders):
|
|
60
|
+
plugins.append(load_plugin(plugin_name, env, data, config))
|
|
61
|
+
|
|
62
|
+
plugin_path_filters: list[PathFilter] = []
|
|
41
63
|
|
|
42
|
-
for
|
|
43
|
-
|
|
64
|
+
for plugin in plugins:
|
|
65
|
+
if hasattr(plugin, "path_filters"):
|
|
66
|
+
plugin_path_filters.extend(plugin.path_filters())
|
|
44
67
|
|
|
45
68
|
# Save rendered files to avoid duplicate work
|
|
46
69
|
# Even if two files are in two separate dirs, they will have the same template name (i.e., relative path)
|
|
@@ -53,15 +76,23 @@ def makejinja(config: Config):
|
|
|
53
76
|
rendered_dirs: dict[Path, Path] = {}
|
|
54
77
|
|
|
55
78
|
for user_input_path in config.inputs:
|
|
56
|
-
if user_input_path.is_file():
|
|
79
|
+
if user_input_path.is_file() or user_input_path == STDIN_PATH:
|
|
57
80
|
handle_input_file(user_input_path, config, env, rendered_files)
|
|
58
81
|
elif user_input_path.is_dir():
|
|
59
82
|
handle_input_dir(
|
|
60
|
-
user_input_path,
|
|
83
|
+
user_input_path,
|
|
84
|
+
config,
|
|
85
|
+
env,
|
|
86
|
+
rendered_files,
|
|
87
|
+
rendered_dirs,
|
|
88
|
+
plugin_path_filters,
|
|
61
89
|
)
|
|
62
90
|
|
|
63
91
|
postprocess_rendered_dirs(config, rendered_dirs)
|
|
64
92
|
|
|
93
|
+
for cmd in config.exec_post:
|
|
94
|
+
exec(cmd)
|
|
95
|
+
|
|
65
96
|
|
|
66
97
|
def postprocess_rendered_dirs(
|
|
67
98
|
config: Config,
|
|
@@ -72,24 +103,24 @@ def postprocess_rendered_dirs(
|
|
|
72
103
|
rendered_dirs.items(), key=lambda x: x[0], reverse=True
|
|
73
104
|
):
|
|
74
105
|
if not config.keep_empty and not any(output_path.iterdir()):
|
|
75
|
-
|
|
76
|
-
print(f"Remove empty dir '{output_path}'")
|
|
77
|
-
|
|
106
|
+
log(f"Remove empty dir '{output_path}'", config)
|
|
78
107
|
shutil.rmtree(output_path)
|
|
79
108
|
|
|
80
109
|
elif config.copy_metadata:
|
|
81
|
-
|
|
82
|
-
print(f"Copy dir metadata '{input_path}' -> '{output_path}'")
|
|
83
|
-
|
|
110
|
+
log(f"Copy dir metadata '{input_path}' -> '{output_path}'", config)
|
|
84
111
|
shutil.copystat(input_path, output_path)
|
|
85
112
|
|
|
86
113
|
|
|
87
114
|
def single_input_output_file(config: Config) -> bool:
|
|
88
115
|
"""Check if the user provided a single input and a single output"""
|
|
89
116
|
return (
|
|
90
|
-
len(config.inputs)
|
|
91
|
-
and config.inputs
|
|
92
|
-
and (
|
|
117
|
+
len(config.inputs) <= 1
|
|
118
|
+
and not any(path.is_dir() for path in config.inputs)
|
|
119
|
+
and (
|
|
120
|
+
config.output == STDOUT_PATH
|
|
121
|
+
or config.output.suffix != ""
|
|
122
|
+
or config.output.is_file()
|
|
123
|
+
)
|
|
93
124
|
and not config.output.is_dir()
|
|
94
125
|
)
|
|
95
126
|
|
|
@@ -122,6 +153,7 @@ def handle_input_dir(
|
|
|
122
153
|
env: Environment,
|
|
123
154
|
rendered_files: abc.MutableMapping[Path, Path],
|
|
124
155
|
rendered_dirs: abc.MutableMapping[Path, Path],
|
|
156
|
+
plugin_path_filters: abc.Sequence[abc.Callable[[Path], bool]],
|
|
125
157
|
) -> None:
|
|
126
158
|
input_paths = (
|
|
127
159
|
input_path
|
|
@@ -133,9 +165,14 @@ def handle_input_dir(
|
|
|
133
165
|
relative_path = input_path.relative_to(user_input_path)
|
|
134
166
|
output_path = generate_output_path(config, relative_path)
|
|
135
167
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
168
|
+
exclude_pattern_match = any(
|
|
169
|
+
input_path.match(x) for x in config.exclude_patterns
|
|
170
|
+
)
|
|
171
|
+
path_filter_match = any(
|
|
172
|
+
not path_filter(input_path) for path_filter in plugin_path_filters
|
|
173
|
+
)
|
|
174
|
+
if exclude_pattern_match or path_filter_match:
|
|
175
|
+
log(f"Skip excluded path '{input_path}'", config)
|
|
139
176
|
|
|
140
177
|
elif input_path.is_file() and output_path not in rendered_files:
|
|
141
178
|
render_file(
|
|
@@ -169,16 +206,12 @@ def init_jinja_env(
|
|
|
169
206
|
config: Config,
|
|
170
207
|
data: Data,
|
|
171
208
|
) -> Environment:
|
|
172
|
-
# If the user-provided ordering of the inputs shall be respected, use this snippet
|
|
173
|
-
# loaders: list[BaseLoader] = []
|
|
174
|
-
# for path in config.inputs:
|
|
175
|
-
# if path.is_dir():
|
|
176
|
-
# loaders.append(FileSystemLoader(path))
|
|
177
|
-
# elif path.is_file():
|
|
178
|
-
# loaders.append(DictLoader({path.name: path.read_text()}))
|
|
179
|
-
|
|
180
209
|
file_loader = DictLoader(
|
|
181
|
-
{
|
|
210
|
+
{
|
|
211
|
+
path.name: path.read_text()
|
|
212
|
+
for path in config.inputs
|
|
213
|
+
if path.is_file() or path == STDIN_PATH
|
|
214
|
+
}
|
|
182
215
|
)
|
|
183
216
|
dir_loader = FileSystemLoader([path for path in config.inputs if path.is_dir()])
|
|
184
217
|
loaders: list[BaseLoader] = [file_loader, dir_loader]
|
|
@@ -285,13 +318,11 @@ def load_data(config: Config) -> dict[str, Any]:
|
|
|
285
318
|
|
|
286
319
|
for path in collect_files(config.data):
|
|
287
320
|
if loader := DATA_LOADERS.get(path.suffix):
|
|
288
|
-
|
|
289
|
-
print(f"Load data '{path}'")
|
|
321
|
+
log(f"Load data '{path}'", config)
|
|
290
322
|
|
|
291
323
|
data |= loader(path)
|
|
292
324
|
else:
|
|
293
|
-
|
|
294
|
-
print(f"Skip unsupported data '{path}'")
|
|
325
|
+
log(f"Skip unsupported data '{path}'", config)
|
|
295
326
|
|
|
296
327
|
for key, value in config.data_vars.items():
|
|
297
328
|
dict_nested_set(data, key, value)
|
|
@@ -299,8 +330,10 @@ def load_data(config: Config) -> dict[str, Any]:
|
|
|
299
330
|
return data
|
|
300
331
|
|
|
301
332
|
|
|
302
|
-
def
|
|
303
|
-
|
|
333
|
+
def load_plugin(
|
|
334
|
+
plugin_name: str, env: Environment, data: Data, config: Config
|
|
335
|
+
) -> Plugin:
|
|
336
|
+
cls: type[Plugin] = import_string(plugin_name)
|
|
304
337
|
sig_params = signature(cls).parameters
|
|
305
338
|
params: dict[str, Any] = {}
|
|
306
339
|
|
|
@@ -310,38 +343,40 @@ def process_loader(loader_name: str, env: Environment, data: Data):
|
|
|
310
343
|
params["environment"] = env
|
|
311
344
|
if sig_params.get("data"):
|
|
312
345
|
params["data"] = data
|
|
346
|
+
if sig_params.get("config"):
|
|
347
|
+
params["config"] = config
|
|
313
348
|
|
|
314
|
-
|
|
349
|
+
plugin = cls(**params)
|
|
315
350
|
|
|
316
|
-
if hasattr(
|
|
317
|
-
env.globals.update({func.__name__: func for func in
|
|
351
|
+
if hasattr(plugin, "globals"):
|
|
352
|
+
env.globals.update({func.__name__: func for func in plugin.globals()})
|
|
318
353
|
|
|
319
|
-
if hasattr(
|
|
320
|
-
env.globals.update({func.__name__: func for func in
|
|
354
|
+
if hasattr(plugin, "functions"):
|
|
355
|
+
env.globals.update({func.__name__: func for func in plugin.functions()})
|
|
321
356
|
|
|
322
|
-
if hasattr(
|
|
323
|
-
env.globals.update(
|
|
357
|
+
if hasattr(plugin, "data"):
|
|
358
|
+
env.globals.update(plugin.data())
|
|
324
359
|
|
|
325
|
-
if hasattr(
|
|
326
|
-
load_extensions(env,
|
|
360
|
+
if hasattr(plugin, "extensions"):
|
|
361
|
+
load_extensions(env, plugin.extensions())
|
|
327
362
|
|
|
328
|
-
if hasattr(
|
|
329
|
-
env.filters.update({func.__name__: func for func in
|
|
363
|
+
if hasattr(plugin, "filters"):
|
|
364
|
+
env.filters.update({func.__name__: func for func in plugin.filters()})
|
|
330
365
|
|
|
331
|
-
if hasattr(
|
|
332
|
-
env.tests.update({func.__name__: func for func in
|
|
366
|
+
if hasattr(plugin, "tests"):
|
|
367
|
+
env.tests.update({func.__name__: func for func in plugin.tests()})
|
|
333
368
|
|
|
334
|
-
if hasattr(
|
|
335
|
-
env.policies.update(
|
|
369
|
+
if hasattr(plugin, "policies"):
|
|
370
|
+
env.policies.update(plugin.policies())
|
|
371
|
+
|
|
372
|
+
return plugin
|
|
336
373
|
|
|
337
374
|
|
|
338
375
|
def render_dir(input: Path, output: Path, config: Config) -> None:
|
|
339
376
|
if output.exists() and not config.force:
|
|
340
|
-
|
|
341
|
-
print(f"Skip existing dir '{output}'")
|
|
377
|
+
log(f"Skip existing dir '{output}'", config)
|
|
342
378
|
else:
|
|
343
|
-
|
|
344
|
-
print(f"Create dir '{input}' -> '{output}'")
|
|
379
|
+
log(f"Create dir '{input}' -> '{output}'", config)
|
|
345
380
|
|
|
346
381
|
output.mkdir(exist_ok=True)
|
|
347
382
|
|
|
@@ -354,9 +389,8 @@ def render_file(
|
|
|
354
389
|
env: Environment,
|
|
355
390
|
enforce_jinja_suffix: bool,
|
|
356
391
|
) -> None:
|
|
357
|
-
if output.exists() and not config.force:
|
|
358
|
-
|
|
359
|
-
print(f"Skip existing file '{output}'")
|
|
392
|
+
if output.exists() and not config.force and output != STDOUT_PATH:
|
|
393
|
+
log(f"Skip existing file '{output}'", config)
|
|
360
394
|
|
|
361
395
|
elif input.suffix == config.jinja_suffix or not enforce_jinja_suffix:
|
|
362
396
|
template = env.get_template(template_name)
|
|
@@ -365,11 +399,9 @@ def render_file(
|
|
|
365
399
|
# Write the rendered template if it has content
|
|
366
400
|
# Prevents empty macro definitions
|
|
367
401
|
if rendered.strip() == "" and not config.keep_empty:
|
|
368
|
-
|
|
369
|
-
print(f"Skip empty file '{input}'")
|
|
402
|
+
log(f"Skip empty file '{input}'", config)
|
|
370
403
|
else:
|
|
371
|
-
|
|
372
|
-
print(f"Render file '{input}' -> '{output}'")
|
|
404
|
+
log(f"Render file '{input}' -> '{output}'", config)
|
|
373
405
|
|
|
374
406
|
with output.open("w") as fp:
|
|
375
407
|
fp.write(rendered)
|
|
@@ -378,7 +410,6 @@ def render_file(
|
|
|
378
410
|
shutil.copystat(input, output)
|
|
379
411
|
|
|
380
412
|
else:
|
|
381
|
-
|
|
382
|
-
print(f"Copy file '{input}' -> '{output}'")
|
|
413
|
+
log(f"Copy file '{input}' -> '{output}'", config)
|
|
383
414
|
|
|
384
415
|
shutil.copy2(input, output)
|
|
@@ -18,7 +18,6 @@ from jinja2.defaults import (
|
|
|
18
18
|
BLOCK_START_STRING,
|
|
19
19
|
COMMENT_END_STRING,
|
|
20
20
|
COMMENT_START_STRING,
|
|
21
|
-
KEEP_TRAILING_NEWLINE,
|
|
22
21
|
LINE_COMMENT_PREFIX,
|
|
23
22
|
LINE_STATEMENT_PREFIX,
|
|
24
23
|
NEWLINE_SEQUENCE,
|
|
@@ -31,6 +30,8 @@ frozendict = immutables.Map
|
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
class Undefined(Enum):
|
|
33
|
+
"""How to handle undefined variables."""
|
|
34
|
+
|
|
34
35
|
default = DefaultUndefined
|
|
35
36
|
chainable = ChainableUndefined
|
|
36
37
|
debug = DebugUndefined
|
|
@@ -153,8 +154,8 @@ class Whitespace:
|
|
|
153
154
|
""",
|
|
154
155
|
)
|
|
155
156
|
keep_trailing_newline: bool = ts.option(
|
|
156
|
-
default=
|
|
157
|
-
click={"param_decls": "--keep-trailing-newline"},
|
|
157
|
+
default=True,
|
|
158
|
+
click={"param_decls": "--keep-trailing-newline/--strip-trailing-newline"},
|
|
158
159
|
help="""
|
|
159
160
|
Preserve the trailing newline when rendering templates.
|
|
160
161
|
The default is `False`, which causes a single newline, if present, to be stripped from the end of the template.
|
|
@@ -219,7 +220,7 @@ class Config:
|
|
|
219
220
|
default=False,
|
|
220
221
|
click={"param_decls": "--keep-jinja-suffix"},
|
|
221
222
|
help="""
|
|
222
|
-
Decide whether the specified `jinja-suffix` is removed from the file after rendering.
|
|
223
|
+
Decide whether the specified `jinja-suffix` is removed from the file name after rendering.
|
|
223
224
|
""",
|
|
224
225
|
)
|
|
225
226
|
keep_empty: bool = ts.option(
|
|
@@ -269,6 +270,14 @@ class Config:
|
|
|
269
270
|
default=tuple(),
|
|
270
271
|
click={
|
|
271
272
|
"param_decls": ("--loader", "-l"),
|
|
273
|
+
"hidden": True,
|
|
274
|
+
},
|
|
275
|
+
help="Deprecated, use `--plugin` instead.",
|
|
276
|
+
)
|
|
277
|
+
plugins: tuple[str, ...] = ts.option(
|
|
278
|
+
default=tuple(),
|
|
279
|
+
click={
|
|
280
|
+
"param_decls": ("--plugin", "-p"),
|
|
272
281
|
},
|
|
273
282
|
help="""
|
|
274
283
|
Use custom Python code to adjust the used Jinja environment to your needs.
|
|
@@ -277,8 +286,8 @@ class Config:
|
|
|
277
286
|
In addition, you may add an `__init__` function that receives two positional arguments:
|
|
278
287
|
the created Jinja environment and the data parsed from the files supplied to makejinja's `data` option.
|
|
279
288
|
This allows you to apply arbitrary logic to makejinja.
|
|
280
|
-
An import path can be specified either in dotted notation (`your.custom.
|
|
281
|
-
or with a colon as object delimiter (`your.custom:
|
|
289
|
+
An import path can be specified either in dotted notation (`your.custom.Plugin`)
|
|
290
|
+
or with a colon as object delimiter (`your.custom:Plugin`).
|
|
282
291
|
**Note:** This option may be passed multiple times to pass a list of values.
|
|
283
292
|
""",
|
|
284
293
|
)
|
|
@@ -290,7 +299,7 @@ class Config:
|
|
|
290
299
|
"show_default": "current working directory",
|
|
291
300
|
},
|
|
292
301
|
help="""
|
|
293
|
-
In order to load
|
|
302
|
+
In order to load plugins or Jinja extensions, the PYTHONPATH variable needs to be patched.
|
|
294
303
|
The default value works for most use cases, but you may load other paths as well.
|
|
295
304
|
""",
|
|
296
305
|
)
|
|
@@ -313,6 +322,18 @@ class Config:
|
|
|
313
322
|
"""
|
|
314
323
|
),
|
|
315
324
|
)
|
|
325
|
+
exec_pre: tuple[str, ...] = ts.option(
|
|
326
|
+
default=tuple(),
|
|
327
|
+
help="""
|
|
328
|
+
Shell commands to execute before rendering.
|
|
329
|
+
""",
|
|
330
|
+
)
|
|
331
|
+
exec_post: tuple[str, ...] = ts.option(
|
|
332
|
+
default=tuple(),
|
|
333
|
+
help="""
|
|
334
|
+
Shell commands to execute after rendering.
|
|
335
|
+
""",
|
|
336
|
+
)
|
|
316
337
|
clean: bool = ts.option(
|
|
317
338
|
default=False,
|
|
318
339
|
click={"param_decls": ("--clean", "-c")},
|
|
@@ -360,7 +381,7 @@ OPTION_GROUPS = {
|
|
|
360
381
|
"options": [
|
|
361
382
|
"--data",
|
|
362
383
|
"--data-var",
|
|
363
|
-
"--
|
|
384
|
+
"--plugin",
|
|
364
385
|
"--import-path",
|
|
365
386
|
"--extension",
|
|
366
387
|
"--undefined",
|
|
@@ -393,5 +414,12 @@ OPTION_GROUPS = {
|
|
|
393
414
|
"--prefix-line-comment",
|
|
394
415
|
],
|
|
395
416
|
},
|
|
417
|
+
{
|
|
418
|
+
"name": "Shell Hooks",
|
|
419
|
+
"options": [
|
|
420
|
+
"--exec-pre",
|
|
421
|
+
"--exec-post",
|
|
422
|
+
],
|
|
423
|
+
},
|
|
396
424
|
]
|
|
397
425
|
}
|
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
from collections import abc
|
|
2
|
-
from
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Protocol
|
|
3
4
|
|
|
4
5
|
from jinja2 import Environment
|
|
5
6
|
from jinja2.ext import Extension
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"Extension",
|
|
11
|
-
"Extensions",
|
|
12
|
-
"Filter",
|
|
13
|
-
"Filters",
|
|
14
|
-
"Function",
|
|
15
|
-
"Functions",
|
|
16
|
-
"Test",
|
|
17
|
-
"Tests",
|
|
18
|
-
"Policies",
|
|
19
|
-
"MutableData",
|
|
20
|
-
"Data",
|
|
21
|
-
)
|
|
8
|
+
from makejinja.config import Config
|
|
9
|
+
|
|
10
|
+
__all__ = ["Plugin"]
|
|
22
11
|
|
|
23
12
|
Extensions = abc.Sequence[type[Extension]]
|
|
24
13
|
Filter = abc.Callable[[Any], Any]
|
|
@@ -30,10 +19,14 @@ Tests = abc.Sequence[Test]
|
|
|
30
19
|
Policies = abc.Mapping[str, Any]
|
|
31
20
|
MutableData = abc.MutableMapping[str, Any]
|
|
32
21
|
Data = abc.Mapping[str, Any]
|
|
22
|
+
PathFilter = abc.Callable[[Path], bool]
|
|
23
|
+
PathFilters = abc.Sequence[PathFilter]
|
|
24
|
+
|
|
33
25
|
|
|
26
|
+
class Plugin(Protocol):
|
|
27
|
+
"""Extend the functionality of makejinja with a plugin implementing a subset of this protocol."""
|
|
34
28
|
|
|
35
|
-
|
|
36
|
-
def __init__(self, *, env: Environment, data: Data) -> None:
|
|
29
|
+
def __init__(self, *, env: Environment, data: Data, config: Config) -> None:
|
|
37
30
|
pass
|
|
38
31
|
|
|
39
32
|
def functions(self) -> Functions:
|
|
@@ -54,6 +47,12 @@ class AbstractLoader:
|
|
|
54
47
|
def extensions(self) -> Extensions:
|
|
55
48
|
return []
|
|
56
49
|
|
|
50
|
+
def path_filters(self) -> PathFilters:
|
|
51
|
+
return []
|
|
52
|
+
|
|
57
53
|
# Deprecated: Use functions() and data() instead
|
|
58
54
|
def globals(self) -> Functions:
|
|
59
55
|
return []
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
AbstractLoader = Plugin
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "makejinja"
|
|
3
|
-
version = "2.
|
|
4
|
-
description = "
|
|
3
|
+
version = "2.5.0"
|
|
4
|
+
description = "Generate entire directory structures using Jinja templates with support for external data and custom plugins."
|
|
5
5
|
authors = ["Mirko Lenz <mirko@mirkolenz.com>"]
|
|
6
6
|
license = "MIT"
|
|
7
7
|
readme = "README.md"
|
|
@@ -49,7 +49,11 @@ python = "^3.11"
|
|
|
49
49
|
jinja2 = "^3.1"
|
|
50
50
|
pyyaml = "^6.0"
|
|
51
51
|
rich-click = "^1.7"
|
|
52
|
-
typed-settings = { version = "
|
|
52
|
+
typed-settings = { version = ">=23.1, <25", extras = [
|
|
53
|
+
"attrs",
|
|
54
|
+
"cattrs",
|
|
55
|
+
"click",
|
|
56
|
+
] }
|
|
53
57
|
immutables = "^0.20"
|
|
54
58
|
|
|
55
59
|
[tool.poetry.group.dev.dependencies]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|