makejinja 2.3.4__tar.gz → 2.4.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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: makejinja
3
- Version: 2.3.4
4
- Summary: makejinja is a CLI tool and Python library to automatically generate files from Jinja2 templates. Use it to easily generate complex Home Assistant dashboards!
3
+ Version: 2.4.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,<24.0)
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">Templating</a>
51
+ <a href="https://jinja.palletsprojects.com/en/3.1.x/templates">Jinja reference</a>
50
52
  </p>
51
53
  <p align="center">
52
- makejinja is a CLI tool and Python library to automatically generate files from Jinja2 templates.
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
- This allows you to load variables from external files or create repeating patterns via loops.
62
- It is conceptually similar to [gomplate](https://github.com/hairyhenderson/gomplate), but is built on Python and Jinja instead of Go.
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
- ## Features
68
+ ## Highlights
68
69
 
69
- - Recursively convert nested directories containing template files. One can even specify a pattern to specify relevant files in a folder.
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
- - Use custom functions in your Jinja templates by loading custom filters and/or globals.
72
- - Easily load bundled as well as custom Jinja extensions.
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,36 +79,34 @@ It can be used as a CLI tool or as a Python library.
82
79
 
83
80
  ### PIP
84
81
 
85
- makejinja is available via `pip` and can be installed via
82
+ makejinja is available on [PyPI](https://pypi.org/project/makejinja/) and can be installed via `pip`:
86
83
 
87
- `pip install makejinja`
88
-
89
- Beware that depending on other packages installed on your system via pip, there may be incompatibilities.
90
- Thus, we advise leveraging [`pipx`](https://github.com/pypa/pipx) instead:
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
- `nix run github:mirkolenz/makejinja -- -i ./input -o ./output`
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 at `ghcr.io/mirkolenz/makejinja`.
108
- To use it, mount a folder 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
- `docker run --rm -v $(pwd)/data:/data ghcr.io/mirkolenz/makejinja:latest -i /data/input -o /data/output`
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
 
114
- In its default configuration, makejinja searches the input folder recursively for files ending in `.jinja`.
115
- It then renders these files and writes them to the output folder, preserving the directory structure.
109
+ In its default configuration, makejinja searches the input directory recursively for files ending in `.jinja`.
110
+ It then renders these files and writes them to the output directory, preserving the directory structure.
116
111
  Our [documentation](https://mirkolenz.github.io/makejinja/makejinja/cli.html) contains a detailed description of all options and can also be accessed via `makejinja --help`.
117
112
 
@@ -0,0 +1,71 @@
1
+ <!-- markdownlint-disable MD033 MD041 -->
2
+ <h2><p align="center">makejinja</p></h2>
3
+ <p align="center">
4
+ <img width="256px" alt="makejinja logo" src="https://raw.githubusercontent.com/mirkolenz/makejinja/main/assets/logo.png" />
5
+ </p>
6
+ <p align="center">
7
+ <a href="https://pypi.org/project/makejinja/">PyPI</a> |
8
+ <a href="https://github.com/users/mirkolenz/packages/container/package/makejinja">Docker</a> |
9
+ <a href="https://mirkolenz.github.io/makejinja">Docs</a> |
10
+ <a href="https://github.com/mirkolenz/makejinja/tree/main/tests/data">Example</a> |
11
+ <a href="https://jinja.palletsprojects.com/en/3.1.x/templates">Jinja reference</a>
12
+ </p>
13
+ <p align="center">
14
+ Generate entire directory structures using Jinja templates with support for external data and custom plugins.
15
+ </p>
16
+
17
+ ---
18
+
19
+ # makejinja
20
+
21
+ makejinja can be used to automatically generate files from [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates).
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.
24
+ A use case for this tool is generating config files for [Home Assistant](https://www.home-assistant.io/):
25
+ Using the same language that the built-in templates use, you can greatly simplify your configuration.
26
+ An [example for Home Assistant](https://github.com/mirkolenz/makejinja/tree/main/tests/data) can be found in the tests directory.
27
+
28
+ ## Highlights
29
+
30
+ - Recursively render nested directories containing template files to a common output directory.
31
+ - Load data files containing variables to use in your Jinja templates from YAML, TOML, and Python files.
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.
34
+
35
+ ## Installation
36
+
37
+ The tool is written in Python and can be installed via pip, nix, and docker.
38
+ It can be used as a CLI tool or as a Python library.
39
+
40
+ ### PIP
41
+
42
+ makejinja is available on [PyPI](https://pypi.org/project/makejinja/) and can be installed via `pip`:
43
+
44
+ ```shell
45
+ pip install makejinja
46
+ makejinja -i ./input -o ./output
47
+ ```
48
+
49
+ ### Nix
50
+
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`.
52
+ You can also run it directly as follows:
53
+
54
+ ```shell
55
+ nix run github:mirkolenz/makejinja -- -i ./input -o ./output
56
+ ```
57
+
58
+ ### Docker
59
+
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:
62
+
63
+ ```shell
64
+ docker run --rm -v $(pwd)/data:/data ghcr.io/mirkolenz/makejinja:latest -i /data/input -o /data/output
65
+ ```
66
+
67
+ ## Usage in Terminal / Command Line
68
+
69
+ In its default configuration, makejinja searches the input directory recursively for files ending in `.jinja`.
70
+ It then renders these files and writes them to the output directory, preserving the directory structure.
71
+ Our [documentation](https://mirkolenz.github.io/makejinja/makejinja/cli.html) contains a detailed description of all options and can also be accessed via `makejinja --help`.
@@ -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, loader
13
+ from . import cli, config, plugin
14
14
  from .app import makejinja
15
15
 
16
- __all__ = ["makejinja", "config", "loader", "cli"]
16
+ loader = plugin
17
+
18
+ __all__ = ["makejinja", "config", "plugin", "loader", "cli"]
@@ -1,3 +1,4 @@
1
+ import itertools
1
2
  import json
2
3
  import os
3
4
  import shutil
@@ -15,7 +16,7 @@ from jinja2.utils import import_string
15
16
  from rich import print
16
17
 
17
18
  from makejinja.config import Config
18
- from makejinja.loader import AbstractLoader, Data, MutableData
19
+ from makejinja.plugin import Data, MutableData, PathFilter, Plugin
19
20
 
20
21
  __all__ = ["makejinja"]
21
22
 
@@ -38,48 +39,60 @@ def makejinja(config: Config):
38
39
  config.output.mkdir(exist_ok=True, parents=True)
39
40
 
40
41
  env = init_jinja_env(config, data)
42
+ plugins: list[Plugin] = []
41
43
 
42
- for loader in config.loaders:
43
- process_loader(loader, env, data)
44
+ for plugin_name in itertools.chain(config.plugins, config.loaders):
45
+ plugins.append(load_plugin(plugin_name, env, data, config))
46
+
47
+ plugin_path_filters: list[PathFilter] = []
48
+
49
+ for plugin in plugins:
50
+ if hasattr(plugin, "path_filters"):
51
+ plugin_path_filters.extend(plugin.path_filters())
44
52
 
45
53
  # Save rendered files to avoid duplicate work
46
- # Even if two files are in two separate folders, they will have the same template name (i.e., relative path)
54
+ # Even if two files are in two separate dirs, they will have the same template name (i.e., relative path)
47
55
  # and thus only the first one will be rendered every time
48
56
  # Key: output_path, Value: input_path
49
57
  rendered_files: dict[Path, Path] = {}
50
58
 
51
- # Save rendered folders to later copy metadata
59
+ # Save rendered dirs to later copy metadata
52
60
  # Key: output_path, Value: input_path
53
- rendered_folders: dict[Path, Path] = {}
61
+ rendered_dirs: dict[Path, Path] = {}
54
62
 
55
63
  for user_input_path in config.inputs:
56
64
  if user_input_path.is_file():
57
65
  handle_input_file(user_input_path, config, env, rendered_files)
58
66
  elif user_input_path.is_dir():
59
- handle_input_folder(
60
- user_input_path, config, env, rendered_files, rendered_folders
67
+ handle_input_dir(
68
+ user_input_path,
69
+ config,
70
+ env,
71
+ rendered_files,
72
+ rendered_dirs,
73
+ plugin_path_filters,
61
74
  )
62
75
 
63
- postprocess_rendered_folders(config, rendered_folders)
76
+ postprocess_rendered_dirs(config, rendered_dirs)
64
77
 
65
78
 
66
- def postprocess_rendered_folders(
79
+ def postprocess_rendered_dirs(
67
80
  config: Config,
68
- rendered_folders: abc.Mapping[Path, Path],
81
+ rendered_dirs: abc.Mapping[Path, Path],
69
82
  ) -> None:
70
- # Start with the deepest folder and work our way up, otherwise the statistics could be modified after copying
83
+ # Start with the deepest directory and work our way up, otherwise the statistics could be modified after copying
71
84
  for output_path, input_path in sorted(
72
- rendered_folders.items(), key=lambda x: x[0], reverse=True
85
+ rendered_dirs.items(), key=lambda x: x[0], reverse=True
73
86
  ):
74
87
  if not config.keep_empty and not any(output_path.iterdir()):
75
88
  if not config.quiet:
76
- print(f"Remove empty '{output_path}'")
89
+ print(f"Remove empty dir '{output_path}'")
77
90
 
78
91
  shutil.rmtree(output_path)
79
92
 
80
93
  elif config.copy_metadata:
81
94
  if not config.quiet:
82
- print(f"Copy metadata '{input_path}' -> '{output_path}'")
95
+ print(f"Copy dir metadata '{input_path}' -> '{output_path}'")
83
96
 
84
97
  shutil.copystat(input_path, output_path)
85
98
 
@@ -104,7 +117,7 @@ def handle_input_file(
104
117
  output_path = generate_output_path(config, relative_path)
105
118
 
106
119
  if output_path not in rendered_files:
107
- render_path(
120
+ render_file(
108
121
  input_path,
109
122
  str(relative_path),
110
123
  output_path,
@@ -116,12 +129,13 @@ def handle_input_file(
116
129
  rendered_files[output_path] = input_path
117
130
 
118
131
 
119
- def handle_input_folder(
132
+ def handle_input_dir(
120
133
  user_input_path: Path,
121
134
  config: Config,
122
135
  env: Environment,
123
136
  rendered_files: abc.MutableMapping[Path, Path],
124
- rendered_folders: abc.MutableMapping[Path, Path],
137
+ rendered_dirs: abc.MutableMapping[Path, Path],
138
+ plugin_path_filters: abc.Sequence[abc.Callable[[Path], bool]],
125
139
  ) -> None:
126
140
  input_paths = (
127
141
  input_path
@@ -133,12 +147,18 @@ def handle_input_folder(
133
147
  relative_path = input_path.relative_to(user_input_path)
134
148
  output_path = generate_output_path(config, relative_path)
135
149
 
136
- if any(input_path.match(x) for x in config.exclude_patterns):
150
+ exclude_pattern_match = any(
151
+ input_path.match(x) for x in config.exclude_patterns
152
+ )
153
+ path_filter_match = any(
154
+ not path_filter(input_path) for path_filter in plugin_path_filters
155
+ )
156
+ if exclude_pattern_match or path_filter_match:
137
157
  if not config.quiet:
138
- print(f"Skip excluded '{input_path}'")
158
+ print(f"Skip excluded path '{input_path}'")
139
159
 
140
160
  elif input_path.is_file() and output_path not in rendered_files:
141
- render_path(
161
+ render_file(
142
162
  input_path,
143
163
  str(relative_path),
144
164
  output_path,
@@ -148,12 +168,9 @@ def handle_input_folder(
148
168
  )
149
169
  rendered_files[output_path] = input_path
150
170
 
151
- elif input_path.is_dir() and output_path not in rendered_folders:
152
- if not config.quiet:
153
- print(f"Create folder '{input_path}' -> '{output_path}'")
154
-
155
- output_path.mkdir()
156
- rendered_folders[output_path] = input_path
171
+ elif input_path.is_dir() and output_path not in rendered_dirs:
172
+ render_dir(input_path, output_path, config)
173
+ rendered_dirs[output_path] = input_path
157
174
 
158
175
 
159
176
  def generate_output_path(config: Config, relative_path: Path) -> Path:
@@ -172,19 +189,11 @@ def init_jinja_env(
172
189
  config: Config,
173
190
  data: Data,
174
191
  ) -> Environment:
175
- # If the user-provided ordering of the inputs shall be respected, use this snippet
176
- # loaders: list[BaseLoader] = []
177
- # for path in config.inputs:
178
- # if path.is_dir():
179
- # loaders.append(FileSystemLoader(path))
180
- # elif path.is_file():
181
- # loaders.append(DictLoader({path.name: path.read_text()}))
182
-
183
192
  file_loader = DictLoader(
184
193
  {path.name: path.read_text() for path in config.inputs if path.is_file()}
185
194
  )
186
- folder_loader = FileSystemLoader([path for path in config.inputs if path.is_dir()])
187
- loaders: list[BaseLoader] = [file_loader, folder_loader]
195
+ dir_loader = FileSystemLoader([path for path in config.inputs if path.is_dir()])
196
+ loaders: list[BaseLoader] = [file_loader, dir_loader]
188
197
 
189
198
  env = Environment(
190
199
  loader=ChoiceLoader(loaders),
@@ -302,8 +311,10 @@ def load_data(config: Config) -> dict[str, Any]:
302
311
  return data
303
312
 
304
313
 
305
- def process_loader(loader_name: str, env: Environment, data: Data):
306
- cls: type[AbstractLoader] = import_string(loader_name)
314
+ def load_plugin(
315
+ plugin_name: str, env: Environment, data: Data, config: Config
316
+ ) -> Plugin:
317
+ cls: type[Plugin] = import_string(plugin_name)
307
318
  sig_params = signature(cls).parameters
308
319
  params: dict[str, Any] = {}
309
320
 
@@ -313,32 +324,47 @@ def process_loader(loader_name: str, env: Environment, data: Data):
313
324
  params["environment"] = env
314
325
  if sig_params.get("data"):
315
326
  params["data"] = data
327
+ if sig_params.get("config"):
328
+ params["config"] = config
316
329
 
317
- loader = cls(**params)
330
+ plugin = cls(**params)
318
331
 
319
- if hasattr(loader, "globals"):
320
- env.globals.update({func.__name__: func for func in loader.globals()})
332
+ if hasattr(plugin, "globals"):
333
+ env.globals.update({func.__name__: func for func in plugin.globals()})
321
334
 
322
- if hasattr(loader, "functions"):
323
- env.globals.update({func.__name__: func for func in loader.functions()})
335
+ if hasattr(plugin, "functions"):
336
+ env.globals.update({func.__name__: func for func in plugin.functions()})
324
337
 
325
- if hasattr(loader, "data"):
326
- env.globals.update(loader.data())
338
+ if hasattr(plugin, "data"):
339
+ env.globals.update(plugin.data())
327
340
 
328
- if hasattr(loader, "extensions"):
329
- load_extensions(env, loader.extensions())
341
+ if hasattr(plugin, "extensions"):
342
+ load_extensions(env, plugin.extensions())
330
343
 
331
- if hasattr(loader, "filters"):
332
- env.filters.update({func.__name__: func for func in loader.filters()})
344
+ if hasattr(plugin, "filters"):
345
+ env.filters.update({func.__name__: func for func in plugin.filters()})
333
346
 
334
- if hasattr(loader, "tests"):
335
- env.tests.update({func.__name__: func for func in loader.tests()})
347
+ if hasattr(plugin, "tests"):
348
+ env.tests.update({func.__name__: func for func in plugin.tests()})
349
+
350
+ if hasattr(plugin, "policies"):
351
+ env.policies.update(plugin.policies())
352
+
353
+ return plugin
354
+
355
+
356
+ def render_dir(input: Path, output: Path, config: Config) -> None:
357
+ if output.exists() and not config.force:
358
+ if not config.quiet:
359
+ print(f"Skip existing dir '{output}'")
360
+ else:
361
+ if not config.quiet:
362
+ print(f"Create dir '{input}' -> '{output}'")
336
363
 
337
- if hasattr(loader, "policies"):
338
- env.policies.update(loader.policies())
364
+ output.mkdir(exist_ok=True)
339
365
 
340
366
 
341
- def render_path(
367
+ def render_file(
342
368
  input: Path,
343
369
  template_name: str,
344
370
  output: Path,
@@ -348,7 +374,7 @@ def render_path(
348
374
  ) -> None:
349
375
  if output.exists() and not config.force:
350
376
  if not config.quiet:
351
- print(f"Skip existing '{output}'")
377
+ print(f"Skip existing file '{output}'")
352
378
 
353
379
  elif input.suffix == config.jinja_suffix or not enforce_jinja_suffix:
354
380
  template = env.get_template(template_name)
@@ -358,7 +384,7 @@ def render_path(
358
384
  # Prevents empty macro definitions
359
385
  if rendered.strip() == "" and not config.keep_empty:
360
386
  if not config.quiet:
361
- print(f"Skip empty '{input}'")
387
+ print(f"Skip empty file '{input}'")
362
388
  else:
363
389
  if not config.quiet:
364
390
  print(f"Render file '{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=KEEP_TRAILING_NEWLINE,
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.
@@ -170,7 +171,7 @@ class Config:
170
171
  "param_decls": ("--input", "-i"),
171
172
  },
172
173
  help="""
173
- Path to a folder containing template files or a single template file.
174
+ Path to a directory containing template files or a single template file.
174
175
  It is passed to Jinja's [FileSystemLoader](https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.FileSystemLoader) when creating the environment.
175
176
  **Note:** This option may be passed multiple times to pass a list of values.
176
177
  If a template exists in multiple inputs, the last value with be used.
@@ -182,7 +183,7 @@ class Config:
182
183
  "param_decls": ("--output", "-o"),
183
184
  },
184
185
  help="""
185
- Path to a folder where the rendered templates are stored.
186
+ Path to a directory where the rendered templates are stored.
186
187
  makejinja preserves the relative paths in the process, meaning that you can even use it on nested directories.
187
188
  """,
188
189
  )
@@ -192,7 +193,7 @@ class Config:
192
193
  help="""
193
194
  Glob patterns to search for files in `inputs`.
194
195
  Accepts all pattern supported by [`fnmatch`](https://docs.python.org/3/library/fnmatch.html#module-fnmatch).
195
- If a file is matched by this pattern and does not end with the specified `jinja-suffix`, it is copied over to the `output_folder`.
196
+ If a file is matched by this pattern and does not end with the specified `jinja-suffix`, it is copied over to `output`.
196
197
  Multiple can be provided.
197
198
  **Note:** Do not add a special suffix used by your template files here, instead use the `jinja-suffix` option.
198
199
  """,
@@ -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(
@@ -247,7 +248,7 @@ class Config:
247
248
  help="""
248
249
  Load variables from yaml/yml/toml/json files for use in your Jinja templates.
249
250
  The definitions are passed to Jinja as globals.
250
- Can either be a file or a folder containing files.
251
+ Can either be a file or a directory containing files.
251
252
  **Note:** This option may be passed multiple times to pass a list of values.
252
253
  If multiple files are supplied, beware that previous declarations will be overwritten by newer ones.
253
254
  """,
@@ -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.Loader`)
281
- or with a colon as object delimiter (`your.custom:Loader`).
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 custom loaders or Jinja extensions, the PYTHONPATH variable needs to be patched.
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
  )
@@ -317,14 +326,14 @@ class Config:
317
326
  default=False,
318
327
  click={"param_decls": ("--clean", "-c")},
319
328
  help="""
320
- Whether to remove the output folder if it exists.
329
+ Whether to remove the output directory if it exists.
321
330
  """,
322
331
  )
323
332
  force: bool = ts.option(
324
333
  default=False,
325
334
  click={"param_decls": ("--force", "-f")},
326
335
  help="""
327
- Whether to overwrite existing files in the output folder.
336
+ Whether to overwrite existing files in the output directory.
328
337
  """,
329
338
  )
330
339
  quiet: bool = ts.option(
@@ -360,7 +369,7 @@ OPTION_GROUPS = {
360
369
  "options": [
361
370
  "--data",
362
371
  "--data-var",
363
- "--loader",
372
+ "--plugin",
364
373
  "--import-path",
365
374
  "--extension",
366
375
  "--undefined",
@@ -1,24 +1,13 @@
1
1
  from collections import abc
2
- from typing import Any
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
- __all__ = (
8
- "AbstractLoader",
9
- "Environment",
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
- class AbstractLoader:
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.3.4"
4
- description = "makejinja is a CLI tool and Python library to automatically generate files from Jinja2 templates. Use it to easily generate complex Home Assistant dashboards!"
3
+ version = "2.4.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 = "^23.1", extras = ["attrs", "cattrs", "click"] }
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]
makejinja-2.3.4/README.md DELETED
@@ -1,76 +0,0 @@
1
- <!-- markdownlint-disable MD033 MD041 -->
2
- <p align="center">
3
- <img width="256px" alt="makejinja logo" src="https://raw.githubusercontent.com/mirkolenz/makejinja/main/assets/logo.png" />
4
- </p>
5
- <p align="center">
6
- <a href="https://pypi.org/project/makejinja/">PyPI</a> |
7
- <a href="https://mirkolenz.github.io/makejinja">Docs</a> |
8
- <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">Templating</a>
10
- </p>
11
- <p align="center">
12
- makejinja is a CLI tool and Python library to automatically generate files from Jinja2 templates.
13
- Use it to easily generate complex Home Assistant dashboards!
14
- </p>
15
-
16
- ---
17
-
18
- # makejinja
19
-
20
- makejinja can be used to automatically generate files from [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates).
21
- This allows you to load variables from external files or create repeating patterns via loops.
22
- It is conceptually similar to [gomplate](https://github.com/hairyhenderson/gomplate), but is built on Python and Jinja instead of Go.
23
- A use case for this tool is generating config files for [Home Assistant](https://www.home-assistant.io/):
24
- Using the same language that the built-in templates use, you can greatly simplify your configuration.
25
- An [example for Home Assistant](https://github.com/mirkolenz/makejinja/tree/main/tests/data) can be found in the tests directory.
26
-
27
- ## Features
28
-
29
- - Recursively convert nested directories containing template files. One can even specify a pattern to specify relevant files in a folder.
30
- - Load data files containing variables to use in your Jinja templates from YAML, TOML, and Python files.
31
- - Use custom functions in your Jinja templates by loading custom filters and/or globals.
32
- - Easily load bundled as well as custom Jinja extensions.
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)
37
-
38
- ## Installation
39
-
40
- The tool is written in Python and can be installed via pip, nix, and docker.
41
- It can be used as a CLI tool or as a Python library.
42
-
43
- ### PIP
44
-
45
- makejinja is available via `pip` and can be installed via
46
-
47
- `pip install makejinja`
48
-
49
- Beware that depending on other packages installed on your system via pip, there may be incompatibilities.
50
- Thus, we advise leveraging [`pipx`](https://github.com/pypa/pipx) instead:
51
-
52
- `pipx install makejinja`
53
-
54
- You can then directly invoke the app as follows:
55
-
56
- `makejinja -i ./input -o ./output`
57
-
58
- ### Nix
59
-
60
- 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
62
-
63
- `nix run github:mirkolenz/makejinja -- -i ./input -o ./output`
64
-
65
- ### Docker
66
-
67
- We automatically publish an image at `ghcr.io/mirkolenz/makejinja`.
68
- To use it, mount a folder to the container and pass the options as the command.
69
-
70
- `docker run --rm -v $(pwd)/data:/data ghcr.io/mirkolenz/makejinja:latest -i /data/input -o /data/output`
71
-
72
- ## Usage in Terminal / Command Line
73
-
74
- In its default configuration, makejinja searches the input folder recursively for files ending in `.jinja`.
75
- It then renders these files and writes them to the output folder, preserving the directory structure.
76
- Our [documentation](https://mirkolenz.github.io/makejinja/makejinja/cli.html) contains a detailed description of all options and can also be accessed via `makejinja --help`.
File without changes
File without changes
File without changes