makejinja 2.8.3__tar.gz → 2.9.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.8.3 → makejinja-2.9.0}/PKG-INFO +2 -2
- {makejinja-2.8.3 → makejinja-2.9.0}/README.md +1 -1
- {makejinja-2.8.3 → makejinja-2.9.0}/pyproject.toml +1 -1
- {makejinja-2.8.3 → makejinja-2.9.0}/pyproject.toml.orig +1 -1
- {makejinja-2.8.3 → makejinja-2.9.0}/src/makejinja/app.py +59 -26
- {makejinja-2.8.3 → makejinja-2.9.0}/src/makejinja/cli.py +4 -1
- {makejinja-2.8.3 → makejinja-2.9.0}/src/makejinja/config.py +32 -2
- {makejinja-2.8.3 → makejinja-2.9.0}/src/makejinja/__init__.py +0 -0
- {makejinja-2.8.3 → makejinja-2.9.0}/src/makejinja/__main__.py +0 -0
- {makejinja-2.8.3 → makejinja-2.9.0}/src/makejinja/plugin.py +0 -0
- {makejinja-2.8.3 → makejinja-2.9.0}/src/makejinja/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: makejinja
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.0
|
|
4
4
|
Summary: Generate entire directory structures using Jinja templates with support for external data and custom plugins
|
|
5
5
|
Keywords: cli,dashboard,generator,hassio,home-assistant,jinja2,library,lovelace,template,tool
|
|
6
6
|
Author: Mirko Lenz
|
|
@@ -77,7 +77,7 @@ Our comprehensive [Home Assistant example](https://github.com/mirkolenz/makejinj
|
|
|
77
77
|
|
|
78
78
|
## Key Features
|
|
79
79
|
|
|
80
|
-
- **Multi-Source Data Integration**: Load variables from YAML, TOML, and Python files, with support for file-specific data sources and runtime variable injection.
|
|
80
|
+
- **Multi-Source Data Integration**: Load variables from YAML, TOML, and Python files, with support for file-specific data sources, per-file namespacing, and runtime variable injection.
|
|
81
81
|
- **Custom Template Delimiters**: Configure Jinja delimiters (e.g., `<% %>` instead of `{{ }}`) to avoid conflicts with target file formats like Home Assistant, Kubernetes, or Terraform.
|
|
82
82
|
- **Flexible Directory Processing**: Process multiple input directories with complex nested structures, preserving hierarchy while applying powerful template transformations.
|
|
83
83
|
- **Extensible Plugin System**: Create custom [plugins](https://mirkolenz.github.io/makejinja/makejinja/plugin.html#Plugin) with filters, functions, data, tests, policies, extensions, and path filtering logic for specialized requirements.
|
|
@@ -36,7 +36,7 @@ Our comprehensive [Home Assistant example](https://github.com/mirkolenz/makejinj
|
|
|
36
36
|
|
|
37
37
|
## Key Features
|
|
38
38
|
|
|
39
|
-
- **Multi-Source Data Integration**: Load variables from YAML, TOML, and Python files, with support for file-specific data sources and runtime variable injection.
|
|
39
|
+
- **Multi-Source Data Integration**: Load variables from YAML, TOML, and Python files, with support for file-specific data sources, per-file namespacing, and runtime variable injection.
|
|
40
40
|
- **Custom Template Delimiters**: Configure Jinja delimiters (e.g., `<% %>` instead of `{{ }}`) to avoid conflicts with target file formats like Home Assistant, Kubernetes, or Terraform.
|
|
41
41
|
- **Flexible Directory Processing**: Process multiple input directories with complex nested structures, preserving hierarchy while applying powerful template transformations.
|
|
42
42
|
- **Extensible Plugin System**: Create custom [plugins](https://mirkolenz.github.io/makejinja/makejinja/plugin.html#Plugin) with filters, functions, data, tests, policies, extensions, and path filtering logic for specialized requirements.
|
|
@@ -14,8 +14,9 @@ import rich_click as click
|
|
|
14
14
|
import yaml
|
|
15
15
|
from jinja2 import BaseLoader, ChoiceLoader, DictLoader, Environment, FileSystemLoader
|
|
16
16
|
from jinja2.utils import import_string
|
|
17
|
+
from typed_settings.dict_utils import set_path
|
|
17
18
|
|
|
18
|
-
from makejinja.config import Config
|
|
19
|
+
from makejinja.config import Config, DataNamespace
|
|
19
20
|
from makejinja.plugin import Data, Functions, MutableData, PathFilter, Plugin
|
|
20
21
|
|
|
21
22
|
__all__ = ["makejinja"]
|
|
@@ -288,57 +289,89 @@ DATA_LOADERS: dict[str, abc.Callable[[Path], dict[str, Any]]] = {
|
|
|
288
289
|
}
|
|
289
290
|
|
|
290
291
|
|
|
291
|
-
def collect_files(paths: abc.Iterable[Path]
|
|
292
|
-
files
|
|
292
|
+
def collect_files(paths: abc.Iterable[Path]) -> abc.Iterator[tuple[Path, Path]]:
|
|
293
|
+
"""Find all files in the given paths, yielding them together with their relative path."""
|
|
293
294
|
|
|
294
295
|
for path in paths:
|
|
295
296
|
if path.is_dir():
|
|
296
|
-
|
|
297
|
-
file
|
|
298
|
-
for file in sorted(path.glob(
|
|
297
|
+
yield from (
|
|
298
|
+
(file, file.relative_to(path))
|
|
299
|
+
for file in sorted(path.glob("**/*"))
|
|
299
300
|
if not file.name.startswith(".") and file.is_file()
|
|
300
301
|
)
|
|
302
|
+
|
|
301
303
|
elif path.is_file():
|
|
302
|
-
|
|
304
|
+
yield path, Path(path.name)
|
|
303
305
|
|
|
304
|
-
return files
|
|
305
306
|
|
|
307
|
+
def to_identifier(name: str) -> str:
|
|
308
|
+
"""Replace all characters that are invalid in a Python identifier with underscores.
|
|
306
309
|
|
|
307
|
-
|
|
308
|
-
"""Given `foo`, 'key1.key2.key3', 'something', set foo['key1']['key2']['key3'] = 'something'
|
|
310
|
+
Jinja looks up globals by name, so only identifiers are reachable from templates.
|
|
309
311
|
|
|
310
|
-
|
|
312
|
+
>>> to_identifier("my-hosting")
|
|
313
|
+
'my_hosting'
|
|
314
|
+
>>> to_identifier("2nd")
|
|
315
|
+
'_2nd'
|
|
311
316
|
"""
|
|
312
317
|
|
|
313
|
-
#
|
|
314
|
-
|
|
318
|
+
# The `_` prefix isolates the rules for the first character, leaving a continuation check
|
|
319
|
+
identifier = "".join(char if f"_{char}".isidentifier() else "_" for char in name)
|
|
320
|
+
|
|
321
|
+
return identifier if identifier.isidentifier() else f"_{identifier}"
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def namespace_keys(
|
|
325
|
+
relative_path: Path, data_namespace: DataNamespace
|
|
326
|
+
) -> tuple[str, ...]:
|
|
327
|
+
"""Determine the nested keys under which the contents of a data file are stored."""
|
|
328
|
+
|
|
329
|
+
match data_namespace:
|
|
330
|
+
case DataNamespace.flat:
|
|
331
|
+
return ()
|
|
332
|
+
|
|
333
|
+
case DataNamespace.stem:
|
|
334
|
+
parts = (relative_path.stem,)
|
|
335
|
+
|
|
336
|
+
case DataNamespace.path:
|
|
337
|
+
parts = relative_path.with_suffix("").parts
|
|
315
338
|
|
|
316
|
-
|
|
317
|
-
keys = dotted_key.split(".")
|
|
339
|
+
return tuple(to_identifier(part) for part in parts)
|
|
318
340
|
|
|
319
|
-
# For every key *before* the last one, we concentrate on navigating through the dictionary.
|
|
320
|
-
for key in keys[:-1]:
|
|
321
|
-
# Try to find here[key]. If it doesn't exist, create it with an empty dictionary. Then,
|
|
322
|
-
# update our `here` pointer to refer to the thing we just found (or created).
|
|
323
|
-
here = here.setdefault(key, {})
|
|
324
341
|
|
|
325
|
-
|
|
326
|
-
|
|
342
|
+
def dict_nested_setdefault(data: MutableData, keys: abc.Iterable[str]) -> MutableData:
|
|
343
|
+
"""Traverse nested dicts along `keys`, inserting empty dicts for missing entries.
|
|
344
|
+
|
|
345
|
+
>>> data = {}
|
|
346
|
+
>>> dict_nested_setdefault(data, ["key1", "key2"])["key3"] = "value"
|
|
347
|
+
>>> data
|
|
348
|
+
{'key1': {'key2': {'key3': 'value'}}}
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
for key in keys:
|
|
352
|
+
data = data.setdefault(key, {})
|
|
353
|
+
|
|
354
|
+
return data
|
|
327
355
|
|
|
328
356
|
|
|
329
357
|
def load_data(config: Config) -> dict[str, Any]:
|
|
330
358
|
data: dict[str, Any] = {}
|
|
331
359
|
|
|
332
|
-
for path in collect_files(config.data):
|
|
360
|
+
for path, relative_path in collect_files(config.data):
|
|
333
361
|
if loader := DATA_LOADERS.get(path.suffix):
|
|
334
|
-
|
|
362
|
+
keys = namespace_keys(relative_path, config.data_namespace)
|
|
363
|
+
dotted_keys = ".".join(keys)
|
|
364
|
+
suffix = f" as '{dotted_keys}'" if keys else ""
|
|
365
|
+
|
|
366
|
+
log(f"Load data '{path}'{suffix}", config)
|
|
367
|
+
|
|
368
|
+
dict_nested_setdefault(data, keys).update(loader(path))
|
|
335
369
|
|
|
336
|
-
data |= loader(path)
|
|
337
370
|
else:
|
|
338
371
|
log(f"Skip unsupported data '{path}'", config)
|
|
339
372
|
|
|
340
373
|
for key, value in config.data_vars.items():
|
|
341
|
-
|
|
374
|
+
set_path(data, key, value)
|
|
342
375
|
|
|
343
376
|
return data
|
|
344
377
|
|
|
@@ -23,7 +23,10 @@ _ts_loaders = ts.default_loaders(
|
|
|
23
23
|
|
|
24
24
|
@click.command("makejinja", context_settings={"help_option_names": ("--help", "-h")})
|
|
25
25
|
@click.version_option(None, "--version", "-v")
|
|
26
|
-
|
|
26
|
+
# Without reloading, `makejinja.toml` is resolved against the working directory at import
|
|
27
|
+
# time. Collection options stay bound to it either way, as typed-settings injects their
|
|
28
|
+
# import-time defaults as if they were passed on the command line.
|
|
29
|
+
@ts.click_options(Config, _ts_loaders, reload_settings_on_invoke=True)
|
|
27
30
|
def makejinja_cli(config: Config) -> None:
|
|
28
31
|
"""makejinja can be used to automatically generate files from [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/).
|
|
29
32
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from collections import abc
|
|
2
|
-
from enum import Enum
|
|
2
|
+
from enum import Enum, auto
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Literal
|
|
5
5
|
|
|
@@ -25,7 +25,15 @@ from jinja2.defaults import (
|
|
|
25
25
|
)
|
|
26
26
|
from rich_click.utils import OptionGroupDict
|
|
27
27
|
|
|
28
|
-
__all__ = [
|
|
28
|
+
__all__ = [
|
|
29
|
+
"Config",
|
|
30
|
+
"DataNamespace",
|
|
31
|
+
"Delimiter",
|
|
32
|
+
"Internal",
|
|
33
|
+
"Prefix",
|
|
34
|
+
"Undefined",
|
|
35
|
+
"Whitespace",
|
|
36
|
+
]
|
|
29
37
|
|
|
30
38
|
|
|
31
39
|
class Undefined(Enum):
|
|
@@ -37,6 +45,14 @@ class Undefined(Enum):
|
|
|
37
45
|
strict = StrictUndefined
|
|
38
46
|
|
|
39
47
|
|
|
48
|
+
class DataNamespace(Enum):
|
|
49
|
+
"""Where the contents of a data file are placed in the global namespace."""
|
|
50
|
+
|
|
51
|
+
flat = auto()
|
|
52
|
+
stem = auto()
|
|
53
|
+
path = auto()
|
|
54
|
+
|
|
55
|
+
|
|
40
56
|
def _exclude_patterns_validator(instance, attribute, value) -> None:
|
|
41
57
|
if any("**" in pattern for pattern in value):
|
|
42
58
|
# todo: for next major release, raise ValueError instead of printing a warning
|
|
@@ -264,6 +280,19 @@ class Config:
|
|
|
264
280
|
If multiple files are supplied, beware that previous declarations will be overwritten by newer ones.
|
|
265
281
|
""",
|
|
266
282
|
)
|
|
283
|
+
data_namespace: DataNamespace = ts.option(
|
|
284
|
+
default=DataNamespace.flat,
|
|
285
|
+
help="""
|
|
286
|
+
Determine where the contents of each `data` file are placed in the Jinja globals.
|
|
287
|
+
With `flat`, all files are merged into a single namespace, so identical keys are overwritten by files loaded later.
|
|
288
|
+
With `stem`, each file is nested under its name without the suffix, making `data/hosting.toml` available as `{{ hosting.provider }}`.
|
|
289
|
+
With `path`, each file is nested under its path relative to the `data` entry it was found in,
|
|
290
|
+
making `data/catalogs/hosting.toml` available as `{{ catalogs.hosting.provider }}` when passing `-d data`.
|
|
291
|
+
A `data` entry pointing to a file instead of a directory always uses its stem.
|
|
292
|
+
**Note:** Characters that are invalid in a Python identifier are replaced with `_` to keep the keys reachable in templates,
|
|
293
|
+
so `my-hosting.toml` is available as `{{ my_hosting.provider }}`.
|
|
294
|
+
""",
|
|
295
|
+
)
|
|
267
296
|
data_vars: abc.Mapping[str, str] = ts.option(
|
|
268
297
|
default=frozendict(),
|
|
269
298
|
click={
|
|
@@ -405,6 +434,7 @@ OPTION_GROUPS: dict[str, list[OptionGroupDict]] = {
|
|
|
405
434
|
"name": "Jinja Environment",
|
|
406
435
|
"options": [
|
|
407
436
|
"--data",
|
|
437
|
+
"--data-namespace",
|
|
408
438
|
"--data-var",
|
|
409
439
|
"--file-data",
|
|
410
440
|
"--plugin",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|