makejinja 2.8.2__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.2 → makejinja-2.9.0}/PKG-INFO +9 -10
- {makejinja-2.8.2 → makejinja-2.9.0}/README.md +2 -2
- {makejinja-2.8.2 → makejinja-2.9.0}/pyproject.toml +33 -22
- makejinja-2.9.0/pyproject.toml.orig +76 -0
- {makejinja-2.8.2 → makejinja-2.9.0}/src/makejinja/__init__.py +1 -1
- {makejinja-2.8.2 → makejinja-2.9.0}/src/makejinja/app.py +74 -44
- {makejinja-2.8.2 → makejinja-2.9.0}/src/makejinja/cli.py +6 -3
- {makejinja-2.8.2 → makejinja-2.9.0}/src/makejinja/config.py +46 -14
- makejinja-2.9.0/src/makejinja/plugin.py +76 -0
- makejinja-2.8.2/src/makejinja/plugin.py +0 -58
- {makejinja-2.8.2 → makejinja-2.9.0}/src/makejinja/__main__.py +0 -0
- {makejinja-2.8.2 → makejinja-2.9.0}/src/makejinja/py.typed +0 -0
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
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
|
-
Keywords:
|
|
5
|
+
Keywords: cli,dashboard,generator,hassio,home-assistant,jinja2,library,lovelace,template,tool
|
|
6
6
|
Author: Mirko Lenz
|
|
7
7
|
Author-email: Mirko Lenz <mirko@mirkolenz.com>
|
|
8
|
+
License-Expression: MIT
|
|
8
9
|
Classifier: Development Status :: 5 - Production/Stable
|
|
9
10
|
Classifier: Environment :: Console
|
|
10
11
|
Classifier: Framework :: Pytest
|
|
11
12
|
Classifier: Intended Audience :: Developers
|
|
12
13
|
Classifier: Intended Audience :: End Users/Desktop
|
|
13
14
|
Classifier: Intended Audience :: System Administrators
|
|
14
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
15
15
|
Classifier: Natural Language :: English
|
|
16
16
|
Classifier: Operating System :: OS Independent
|
|
17
|
-
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
-
Classifier: Programming Language :: Python :: 3
|
|
21
20
|
Classifier: Topic :: File Formats
|
|
22
21
|
Classifier: Topic :: Home Automation
|
|
23
22
|
Classifier: Topic :: Software Development :: Code Generators
|
|
@@ -31,8 +30,8 @@ Requires-Dist: frozendict>=2,<3
|
|
|
31
30
|
Requires-Dist: jinja2>=3,<4
|
|
32
31
|
Requires-Dist: pyyaml>=6,<7
|
|
33
32
|
Requires-Dist: rich-click>=1,<2
|
|
34
|
-
Requires-Dist: typed-settings[attrs,cattrs,click]>=
|
|
35
|
-
Requires-Python: >=3.
|
|
33
|
+
Requires-Dist: typed-settings[attrs,cattrs,click]>=25,<27
|
|
34
|
+
Requires-Python: >=3.13, <4
|
|
36
35
|
Project-URL: Changelog, https://github.com/mirkolenz/makejinja/releases
|
|
37
36
|
Project-URL: Documentation, https://mirkolenz.github.io/makejinja/makejinja/cli.html
|
|
38
37
|
Project-URL: Homepage, https://mirkolenz.github.io/makejinja/
|
|
@@ -78,10 +77,10 @@ Our comprehensive [Home Assistant example](https://github.com/mirkolenz/makejinj
|
|
|
78
77
|
|
|
79
78
|
## Key Features
|
|
80
79
|
|
|
81
|
-
- **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.
|
|
82
81
|
- **Custom Template Delimiters**: Configure Jinja delimiters (e.g., `<% %>` instead of `{{ }}`) to avoid conflicts with target file formats like Home Assistant, Kubernetes, or Terraform.
|
|
83
82
|
- **Flexible Directory Processing**: Process multiple input directories with complex nested structures, preserving hierarchy while applying powerful template transformations.
|
|
84
|
-
- **Extensible Plugin System**: Create custom [plugins](https://mirkolenz.github.io/makejinja/makejinja/plugin.html#Plugin) with filters, functions, and path filtering logic for specialized requirements.
|
|
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.
|
|
85
84
|
- **Production-Ready**: Comprehensive CLI interface, configuration file support, and Python library API for seamless workflow integration.
|
|
86
85
|
|
|
87
86
|
## Use Cases
|
|
@@ -36,10 +36,10 @@ 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
|
-
- **Extensible Plugin System**: Create custom [plugins](https://mirkolenz.github.io/makejinja/makejinja/plugin.html#Plugin) with filters, functions, and path filtering logic for specialized requirements.
|
|
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.
|
|
43
43
|
- **Production-Ready**: Comprehensive CLI interface, configuration file support, and Python library API for seamless workflow integration.
|
|
44
44
|
|
|
45
45
|
## Use Cases
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "makejinja"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.9.0"
|
|
4
4
|
description = "Generate entire directory structures using Jinja templates with support for external data and custom plugins"
|
|
5
|
-
authors = [{ name = "Mirko Lenz", email = "mirko@mirkolenz.com" }]
|
|
6
5
|
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13,<4"
|
|
7
|
+
license = "MIT"
|
|
7
8
|
keywords = [
|
|
8
|
-
"
|
|
9
|
-
"home-assistant",
|
|
10
|
-
"hassio",
|
|
9
|
+
"cli",
|
|
11
10
|
"dashboard",
|
|
11
|
+
"generator",
|
|
12
|
+
"hassio",
|
|
13
|
+
"home-assistant",
|
|
14
|
+
"jinja2",
|
|
15
|
+
"library",
|
|
12
16
|
"lovelace",
|
|
13
17
|
"template",
|
|
14
|
-
"generator",
|
|
15
|
-
"cli",
|
|
16
18
|
"tool",
|
|
17
|
-
"library",
|
|
18
19
|
]
|
|
19
20
|
classifiers = [
|
|
20
21
|
"Development Status :: 5 - Production/Stable",
|
|
@@ -23,13 +24,11 @@ classifiers = [
|
|
|
23
24
|
"Intended Audience :: Developers",
|
|
24
25
|
"Intended Audience :: End Users/Desktop",
|
|
25
26
|
"Intended Audience :: System Administrators",
|
|
26
|
-
"License :: OSI Approved :: MIT License",
|
|
27
27
|
"Natural Language :: English",
|
|
28
28
|
"Operating System :: OS Independent",
|
|
29
|
-
"Programming Language :: Python :: 3
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
30
|
"Programming Language :: Python :: 3.13",
|
|
31
31
|
"Programming Language :: Python :: 3.14",
|
|
32
|
-
"Programming Language :: Python :: 3",
|
|
33
32
|
"Topic :: File Formats",
|
|
34
33
|
"Topic :: Home Automation",
|
|
35
34
|
"Topic :: Software Development :: Code Generators",
|
|
@@ -40,38 +39,50 @@ classifiers = [
|
|
|
40
39
|
"Topic :: Utilities",
|
|
41
40
|
"Typing :: Typed",
|
|
42
41
|
]
|
|
43
|
-
requires-python = ">=3.12,<4"
|
|
44
42
|
dependencies = [
|
|
45
43
|
"frozendict>=2,<3",
|
|
46
44
|
"jinja2>=3,<4",
|
|
47
45
|
"pyyaml>=6,<7",
|
|
48
46
|
"rich-click>=1,<2",
|
|
49
|
-
"typed-settings[attrs,cattrs,click]>=
|
|
47
|
+
"typed-settings[attrs,cattrs,click]>=25,<27",
|
|
50
48
|
]
|
|
51
49
|
|
|
50
|
+
[[project.authors]]
|
|
51
|
+
name = "Mirko Lenz"
|
|
52
|
+
email = "mirko@mirkolenz.com"
|
|
53
|
+
|
|
52
54
|
[project.urls]
|
|
53
|
-
|
|
54
|
-
Homepage = "https://mirkolenz.github.io/makejinja/"
|
|
55
|
+
Changelog = "https://github.com/mirkolenz/makejinja/releases"
|
|
55
56
|
Documentation = "https://mirkolenz.github.io/makejinja/makejinja/cli.html"
|
|
57
|
+
Homepage = "https://mirkolenz.github.io/makejinja/"
|
|
56
58
|
Issues = "https://github.com/mirkolenz/makejinja/issues"
|
|
57
|
-
|
|
59
|
+
Repository = "https://github.com/mirkolenz/makejinja"
|
|
58
60
|
|
|
59
61
|
[project.scripts]
|
|
60
62
|
makejinja = "makejinja.cli:makejinja_cli"
|
|
61
63
|
|
|
62
64
|
[dependency-groups]
|
|
63
|
-
test = ["pytest>=9,<10", "pytest-cov>=7,<8"]
|
|
64
65
|
docs = ["pdoc>=16,<17"]
|
|
66
|
+
test = [
|
|
67
|
+
"pytest>=9,<10",
|
|
68
|
+
"pytest-cov>=7,<8",
|
|
69
|
+
]
|
|
65
70
|
|
|
66
71
|
[build-system]
|
|
67
|
-
requires = ["uv-build>=0.
|
|
72
|
+
requires = ["uv-build>=0.12,<1"]
|
|
68
73
|
build-backend = "uv_build"
|
|
69
74
|
|
|
70
|
-
[tool.uv]
|
|
71
|
-
default-groups = ["test", "docs"]
|
|
72
|
-
|
|
73
75
|
[tool.pytest]
|
|
74
|
-
addopts = [
|
|
76
|
+
addopts = [
|
|
77
|
+
"--cov=makejinja",
|
|
78
|
+
"--cov-report=term-missing",
|
|
79
|
+
]
|
|
75
80
|
|
|
76
81
|
[tool.ruff.lint.pydocstyle]
|
|
77
82
|
convention = "google"
|
|
83
|
+
|
|
84
|
+
[tool.uv]
|
|
85
|
+
default-groups = [
|
|
86
|
+
"test",
|
|
87
|
+
"docs",
|
|
88
|
+
]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "makejinja"
|
|
3
|
+
version = "2.9.0"
|
|
4
|
+
description = "Generate entire directory structures using Jinja templates with support for external data and custom plugins"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13,<4"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [{ name = "Mirko Lenz", email = "mirko@mirkolenz.com" }]
|
|
9
|
+
keywords = [
|
|
10
|
+
"cli",
|
|
11
|
+
"dashboard",
|
|
12
|
+
"generator",
|
|
13
|
+
"hassio",
|
|
14
|
+
"home-assistant",
|
|
15
|
+
"jinja2",
|
|
16
|
+
"library",
|
|
17
|
+
"lovelace",
|
|
18
|
+
"template",
|
|
19
|
+
"tool",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 5 - Production/Stable",
|
|
23
|
+
"Environment :: Console",
|
|
24
|
+
"Framework :: Pytest",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"Intended Audience :: End Users/Desktop",
|
|
27
|
+
"Intended Audience :: System Administrators",
|
|
28
|
+
"Natural Language :: English",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3.13",
|
|
32
|
+
"Programming Language :: Python :: 3.14",
|
|
33
|
+
"Topic :: File Formats",
|
|
34
|
+
"Topic :: Home Automation",
|
|
35
|
+
"Topic :: Software Development :: Code Generators",
|
|
36
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
37
|
+
"Topic :: System :: Software Distribution",
|
|
38
|
+
"Topic :: System :: Systems Administration",
|
|
39
|
+
"Topic :: Text Processing :: Markup",
|
|
40
|
+
"Topic :: Utilities",
|
|
41
|
+
"Typing :: Typed",
|
|
42
|
+
]
|
|
43
|
+
dependencies = [
|
|
44
|
+
"frozendict>=2,<3",
|
|
45
|
+
"jinja2>=3,<4",
|
|
46
|
+
"pyyaml>=6,<7",
|
|
47
|
+
"rich-click>=1,<2",
|
|
48
|
+
"typed-settings[attrs,cattrs,click]>=25,<27",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[project.urls]
|
|
52
|
+
Changelog = "https://github.com/mirkolenz/makejinja/releases"
|
|
53
|
+
Documentation = "https://mirkolenz.github.io/makejinja/makejinja/cli.html"
|
|
54
|
+
Homepage = "https://mirkolenz.github.io/makejinja/"
|
|
55
|
+
Issues = "https://github.com/mirkolenz/makejinja/issues"
|
|
56
|
+
Repository = "https://github.com/mirkolenz/makejinja"
|
|
57
|
+
|
|
58
|
+
[project.scripts]
|
|
59
|
+
makejinja = "makejinja.cli:makejinja_cli"
|
|
60
|
+
|
|
61
|
+
[dependency-groups]
|
|
62
|
+
docs = ["pdoc>=16,<17"]
|
|
63
|
+
test = ["pytest>=9,<10", "pytest-cov>=7,<8"]
|
|
64
|
+
|
|
65
|
+
[build-system]
|
|
66
|
+
requires = ["uv-build>=0.12,<1"]
|
|
67
|
+
build-backend = "uv_build"
|
|
68
|
+
|
|
69
|
+
[tool.pytest]
|
|
70
|
+
addopts = ["--cov=makejinja", "--cov-report=term-missing"]
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint.pydocstyle]
|
|
73
|
+
convention = "google"
|
|
74
|
+
|
|
75
|
+
[tool.uv]
|
|
76
|
+
default-groups = ["test", "docs"]
|
|
@@ -8,16 +8,16 @@ import tomllib
|
|
|
8
8
|
from collections import abc
|
|
9
9
|
from inspect import signature
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any, cast
|
|
12
12
|
|
|
13
13
|
import rich_click as click
|
|
14
14
|
import yaml
|
|
15
15
|
from jinja2 import BaseLoader, ChoiceLoader, DictLoader, Environment, FileSystemLoader
|
|
16
|
-
from jinja2.environment import load_extensions
|
|
17
16
|
from jinja2.utils import import_string
|
|
17
|
+
from typed_settings.dict_utils import set_path
|
|
18
18
|
|
|
19
|
-
from makejinja.config import Config
|
|
20
|
-
from makejinja.plugin import Data, MutableData, PathFilter, Plugin
|
|
19
|
+
from makejinja.config import Config, DataNamespace
|
|
20
|
+
from makejinja.plugin import Data, Functions, MutableData, PathFilter, Plugin
|
|
21
21
|
|
|
22
22
|
__all__ = ["makejinja"]
|
|
23
23
|
|
|
@@ -244,7 +244,7 @@ def init_jinja_env(
|
|
|
244
244
|
)
|
|
245
245
|
|
|
246
246
|
env.globals.update(data)
|
|
247
|
-
env.globals["env"] = os.environ
|
|
247
|
+
cast(dict[str, Any], env.globals)["env"] = os.environ
|
|
248
248
|
|
|
249
249
|
return env
|
|
250
250
|
|
|
@@ -266,14 +266,7 @@ def from_yaml(path: Path) -> dict[str, Any]:
|
|
|
266
266
|
|
|
267
267
|
def from_toml(path: Path) -> dict[str, Any]:
|
|
268
268
|
with path.open("rb") as fp:
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if isinstance(data, abc.Mapping):
|
|
272
|
-
return dict(data)
|
|
273
|
-
|
|
274
|
-
raise TypeError(
|
|
275
|
-
f"Expected TOML documents in '{path}' to be mappings but found {type(data).__name__}"
|
|
276
|
-
)
|
|
269
|
+
return tomllib.load(fp)
|
|
277
270
|
|
|
278
271
|
|
|
279
272
|
def from_json(path: Path) -> dict[str, Any]:
|
|
@@ -281,7 +274,7 @@ def from_json(path: Path) -> dict[str, Any]:
|
|
|
281
274
|
data = json.load(fp)
|
|
282
275
|
|
|
283
276
|
if isinstance(data, abc.Mapping):
|
|
284
|
-
return data
|
|
277
|
+
return dict(data)
|
|
285
278
|
|
|
286
279
|
raise TypeError(
|
|
287
280
|
f"Expected JSON documents in '{path}' to be mappings but found {type(data).__name__}"
|
|
@@ -296,57 +289,89 @@ DATA_LOADERS: dict[str, abc.Callable[[Path], dict[str, Any]]] = {
|
|
|
296
289
|
}
|
|
297
290
|
|
|
298
291
|
|
|
299
|
-
def collect_files(paths: abc.Iterable[Path]
|
|
300
|
-
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."""
|
|
301
294
|
|
|
302
295
|
for path in paths:
|
|
303
296
|
if path.is_dir():
|
|
304
|
-
|
|
305
|
-
file
|
|
306
|
-
for file in sorted(path.glob(
|
|
297
|
+
yield from (
|
|
298
|
+
(file, file.relative_to(path))
|
|
299
|
+
for file in sorted(path.glob("**/*"))
|
|
307
300
|
if not file.name.startswith(".") and file.is_file()
|
|
308
301
|
)
|
|
302
|
+
|
|
309
303
|
elif path.is_file():
|
|
310
|
-
|
|
304
|
+
yield path, Path(path.name)
|
|
311
305
|
|
|
312
|
-
return files
|
|
313
306
|
|
|
307
|
+
def to_identifier(name: str) -> str:
|
|
308
|
+
"""Replace all characters that are invalid in a Python identifier with underscores.
|
|
314
309
|
|
|
315
|
-
|
|
316
|
-
"""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.
|
|
317
311
|
|
|
318
|
-
|
|
312
|
+
>>> to_identifier("my-hosting")
|
|
313
|
+
'my_hosting'
|
|
314
|
+
>>> to_identifier("2nd")
|
|
315
|
+
'_2nd'
|
|
319
316
|
"""
|
|
320
317
|
|
|
321
|
-
#
|
|
322
|
-
|
|
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 ()
|
|
323
332
|
|
|
324
|
-
|
|
325
|
-
|
|
333
|
+
case DataNamespace.stem:
|
|
334
|
+
parts = (relative_path.stem,)
|
|
326
335
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
# Try to find here[key]. If it doesn't exist, create it with an empty dictionary. Then,
|
|
330
|
-
# update our `here` pointer to refer to the thing we just found (or created).
|
|
331
|
-
here = here.setdefault(key, {})
|
|
336
|
+
case DataNamespace.path:
|
|
337
|
+
parts = relative_path.with_suffix("").parts
|
|
332
338
|
|
|
333
|
-
|
|
334
|
-
|
|
339
|
+
return tuple(to_identifier(part) for part in parts)
|
|
340
|
+
|
|
341
|
+
|
|
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
|
|
335
355
|
|
|
336
356
|
|
|
337
357
|
def load_data(config: Config) -> dict[str, Any]:
|
|
338
358
|
data: dict[str, Any] = {}
|
|
339
359
|
|
|
340
|
-
for path in collect_files(config.data):
|
|
360
|
+
for path, relative_path in collect_files(config.data):
|
|
341
361
|
if loader := DATA_LOADERS.get(path.suffix):
|
|
342
|
-
|
|
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))
|
|
343
369
|
|
|
344
|
-
data |= loader(path)
|
|
345
370
|
else:
|
|
346
371
|
log(f"Skip unsupported data '{path}'", config)
|
|
347
372
|
|
|
348
373
|
for key, value in config.data_vars.items():
|
|
349
|
-
|
|
374
|
+
set_path(data, key, value)
|
|
350
375
|
|
|
351
376
|
return data
|
|
352
377
|
|
|
@@ -371,6 +396,10 @@ def load_file_data(template_name: str, config: Config) -> dict[str, Any]:
|
|
|
371
396
|
return file_data
|
|
372
397
|
|
|
373
398
|
|
|
399
|
+
def _named_functions(functions: Functions) -> dict[str, Any]:
|
|
400
|
+
return {cast(Any, func).__name__: func for func in functions}
|
|
401
|
+
|
|
402
|
+
|
|
374
403
|
def load_plugin(
|
|
375
404
|
plugin_name: str, env: Environment, data: Data, config: Config
|
|
376
405
|
) -> Plugin:
|
|
@@ -390,22 +419,23 @@ def load_plugin(
|
|
|
390
419
|
plugin = cls(**params)
|
|
391
420
|
|
|
392
421
|
if hasattr(plugin, "globals"):
|
|
393
|
-
env.globals.update(
|
|
422
|
+
env.globals.update(_named_functions(plugin.globals()))
|
|
394
423
|
|
|
395
424
|
if hasattr(plugin, "functions"):
|
|
396
|
-
env.globals.update(
|
|
425
|
+
env.globals.update(_named_functions(plugin.functions()))
|
|
397
426
|
|
|
398
427
|
if hasattr(plugin, "data"):
|
|
399
428
|
env.globals.update(plugin.data())
|
|
400
429
|
|
|
401
430
|
if hasattr(plugin, "extensions"):
|
|
402
|
-
|
|
431
|
+
for extension in plugin.extensions():
|
|
432
|
+
env.add_extension(extension)
|
|
403
433
|
|
|
404
434
|
if hasattr(plugin, "filters"):
|
|
405
|
-
env.filters.update(
|
|
435
|
+
env.filters.update(_named_functions(plugin.filters()))
|
|
406
436
|
|
|
407
437
|
if hasattr(plugin, "tests"):
|
|
408
|
-
env.tests.update(
|
|
438
|
+
env.tests.update(_named_functions(plugin.tests()))
|
|
409
439
|
|
|
410
440
|
if hasattr(plugin, "policies"):
|
|
411
441
|
env.policies.update(plugin.policies())
|
|
@@ -13,7 +13,7 @@ from .app import makejinja
|
|
|
13
13
|
|
|
14
14
|
__all__: list[str] = []
|
|
15
15
|
|
|
16
|
-
click.rich_click.
|
|
16
|
+
click.rich_click.TEXT_MARKUP = "markdown"
|
|
17
17
|
click.rich_click.OPTION_GROUPS = OPTION_GROUPS
|
|
18
18
|
|
|
19
19
|
_ts_loaders = ts.default_loaders(
|
|
@@ -23,8 +23,11 @@ _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
|
-
|
|
27
|
-
|
|
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)
|
|
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
|
|
|
30
33
|
Instead of passing CLI options, you can also write them to a file called `makejinja.toml` in your working directory.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from collections import abc
|
|
2
|
-
from enum import Enum
|
|
2
|
+
from enum import Enum, auto
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import Literal
|
|
4
5
|
|
|
5
6
|
import rich_click as click
|
|
6
7
|
import typed_settings as ts
|
|
@@ -24,7 +25,15 @@ from jinja2.defaults import (
|
|
|
24
25
|
)
|
|
25
26
|
from rich_click.utils import OptionGroupDict
|
|
26
27
|
|
|
27
|
-
__all__ = [
|
|
28
|
+
__all__ = [
|
|
29
|
+
"Config",
|
|
30
|
+
"DataNamespace",
|
|
31
|
+
"Delimiter",
|
|
32
|
+
"Internal",
|
|
33
|
+
"Prefix",
|
|
34
|
+
"Undefined",
|
|
35
|
+
"Whitespace",
|
|
36
|
+
]
|
|
28
37
|
|
|
29
38
|
|
|
30
39
|
class Undefined(Enum):
|
|
@@ -36,6 +45,14 @@ class Undefined(Enum):
|
|
|
36
45
|
strict = StrictUndefined
|
|
37
46
|
|
|
38
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
|
+
|
|
39
56
|
def _exclude_patterns_validator(instance, attribute, value) -> None:
|
|
40
57
|
if any("**" in pattern for pattern in value):
|
|
41
58
|
# todo: for next major release, raise ValueError instead of printing a warning
|
|
@@ -152,7 +169,7 @@ class Whitespace:
|
|
|
152
169
|
If this is set to `True`, leading spaces and tabs are stripped from the start of a line to a block.
|
|
153
170
|
""",
|
|
154
171
|
)
|
|
155
|
-
newline_sequence:
|
|
172
|
+
newline_sequence: Literal["\n", "\r\n", "\r"] = ts.option(
|
|
156
173
|
default=NEWLINE_SEQUENCE,
|
|
157
174
|
click={"param_decls": "--newline-sequence"},
|
|
158
175
|
help="""
|
|
@@ -206,7 +223,7 @@ class Config:
|
|
|
206
223
|
""",
|
|
207
224
|
)
|
|
208
225
|
exclude_patterns: tuple[str, ...] = ts.option(
|
|
209
|
-
default=
|
|
226
|
+
default=(),
|
|
210
227
|
click={"param_decls": ("--exclude-pattern", "--exclude", "-E")},
|
|
211
228
|
validator=_exclude_patterns_validator,
|
|
212
229
|
help="""
|
|
@@ -250,7 +267,7 @@ class Config:
|
|
|
250
267
|
""",
|
|
251
268
|
)
|
|
252
269
|
data: tuple[Path, ...] = ts.option(
|
|
253
|
-
default=
|
|
270
|
+
default=(),
|
|
254
271
|
click={
|
|
255
272
|
"type": click.Path(exists=True, path_type=Path),
|
|
256
273
|
"param_decls": ("--data", "-d"),
|
|
@@ -263,6 +280,19 @@ class Config:
|
|
|
263
280
|
If multiple files are supplied, beware that previous declarations will be overwritten by newer ones.
|
|
264
281
|
""",
|
|
265
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
|
+
)
|
|
266
296
|
data_vars: abc.Mapping[str, str] = ts.option(
|
|
267
297
|
default=frozendict(),
|
|
268
298
|
click={
|
|
@@ -290,7 +320,7 @@ class Config:
|
|
|
290
320
|
},
|
|
291
321
|
)
|
|
292
322
|
loaders: tuple[str, ...] = ts.option(
|
|
293
|
-
default=
|
|
323
|
+
default=(),
|
|
294
324
|
click={
|
|
295
325
|
"param_decls": ("--loader", "-l"),
|
|
296
326
|
"hidden": True,
|
|
@@ -298,17 +328,18 @@ class Config:
|
|
|
298
328
|
help="Deprecated, use `--plugin` instead.",
|
|
299
329
|
)
|
|
300
330
|
plugins: tuple[str, ...] = ts.option(
|
|
301
|
-
default=
|
|
331
|
+
default=(),
|
|
302
332
|
click={
|
|
303
333
|
"param_decls": ("--plugin", "-p"),
|
|
304
334
|
},
|
|
305
335
|
help="""
|
|
306
336
|
Use custom Python code to adjust the used Jinja environment to your needs.
|
|
307
337
|
The specified Python file should export a **class** containing a subset of the following functions:
|
|
308
|
-
`filters`, `
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
338
|
+
`filters`, `functions`, `data`, `tests`, `policies`, `extensions`, and `path_filters`.
|
|
339
|
+
The `globals` method is deprecated, use `functions` and `data` instead.
|
|
340
|
+
An optional `__init__` method can receive any subset of the keyword arguments `env` (or `environment`), `data`, and `config`.
|
|
341
|
+
These provide the Jinja environment, loaded global data, and makejinja configuration.
|
|
342
|
+
Before templates are loaded, the constructor can configure environment features such as `finalize`, custom loaders, and bytecode caching.
|
|
312
343
|
An import path can be specified either in dotted notation (`your.custom.Plugin`)
|
|
313
344
|
or with a colon as object delimiter (`your.custom:Plugin`).
|
|
314
345
|
**Note:** This option may be passed multiple times to pass a list of values.
|
|
@@ -327,7 +358,7 @@ class Config:
|
|
|
327
358
|
""",
|
|
328
359
|
)
|
|
329
360
|
extensions: tuple[str, ...] = ts.option(
|
|
330
|
-
default=
|
|
361
|
+
default=(),
|
|
331
362
|
click={"param_decls": ("--extension", "-e")},
|
|
332
363
|
help="""
|
|
333
364
|
List of Jinja extensions to use as strings of import paths.
|
|
@@ -346,13 +377,13 @@ class Config:
|
|
|
346
377
|
),
|
|
347
378
|
)
|
|
348
379
|
exec_pre: tuple[str, ...] = ts.option(
|
|
349
|
-
default=
|
|
380
|
+
default=(),
|
|
350
381
|
help="""
|
|
351
382
|
Shell commands to execute before rendering.
|
|
352
383
|
""",
|
|
353
384
|
)
|
|
354
385
|
exec_post: tuple[str, ...] = ts.option(
|
|
355
|
-
default=
|
|
386
|
+
default=(),
|
|
356
387
|
help="""
|
|
357
388
|
Shell commands to execute after rendering.
|
|
358
389
|
""",
|
|
@@ -403,6 +434,7 @@ OPTION_GROUPS: dict[str, list[OptionGroupDict]] = {
|
|
|
403
434
|
"name": "Jinja Environment",
|
|
404
435
|
"options": [
|
|
405
436
|
"--data",
|
|
437
|
+
"--data-namespace",
|
|
406
438
|
"--data-var",
|
|
407
439
|
"--file-data",
|
|
408
440
|
"--plugin",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from collections import abc
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Protocol
|
|
4
|
+
|
|
5
|
+
from jinja2 import Environment
|
|
6
|
+
from jinja2.ext import Extension
|
|
7
|
+
|
|
8
|
+
from makejinja.config import Config
|
|
9
|
+
|
|
10
|
+
__all__ = ["Plugin"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
type Extensions = abc.Sequence[type[Extension]]
|
|
14
|
+
type Filter = abc.Callable[..., Any]
|
|
15
|
+
type Filters = abc.Sequence[Filter]
|
|
16
|
+
type Function = abc.Callable[..., Any]
|
|
17
|
+
type Functions = abc.Sequence[Function]
|
|
18
|
+
type Test = abc.Callable[..., Any]
|
|
19
|
+
type Tests = abc.Sequence[Test]
|
|
20
|
+
type Policies = abc.Mapping[str, Any]
|
|
21
|
+
type MutableData = abc.MutableMapping[str, Any]
|
|
22
|
+
type Data = abc.Mapping[str, Any]
|
|
23
|
+
type PathFilter = abc.Callable[[Path], bool]
|
|
24
|
+
type PathFilters = abc.Sequence[PathFilter]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Plugin(Protocol):
|
|
28
|
+
"""Extend makejinja by implementing any subset of the plugin methods.
|
|
29
|
+
|
|
30
|
+
The optional constructor receives supported parameters by keyword.
|
|
31
|
+
It may request `env` (or `environment`), `data`, and `config`.
|
|
32
|
+
Configure the environment before loading any templates, including changes to
|
|
33
|
+
`finalize`, custom loaders, or bytecode caching.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, *, env: Environment, data: Data, config: Config) -> None:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
def functions(self) -> Functions:
|
|
40
|
+
"""Return global template callables registered by their function names."""
|
|
41
|
+
return []
|
|
42
|
+
|
|
43
|
+
def data(self) -> Data:
|
|
44
|
+
"""Return global template values that override existing values."""
|
|
45
|
+
return {}
|
|
46
|
+
|
|
47
|
+
def filters(self) -> Filters:
|
|
48
|
+
"""Return template filters registered by their function names."""
|
|
49
|
+
return []
|
|
50
|
+
|
|
51
|
+
def tests(self) -> Tests:
|
|
52
|
+
"""Return template tests registered by their function names."""
|
|
53
|
+
return []
|
|
54
|
+
|
|
55
|
+
def policies(self) -> Policies:
|
|
56
|
+
"""Return overrides for Jinja environment policies."""
|
|
57
|
+
return {}
|
|
58
|
+
|
|
59
|
+
def extensions(self) -> Extensions:
|
|
60
|
+
"""Return Jinja extension classes to register on the environment."""
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
def path_filters(self) -> PathFilters:
|
|
64
|
+
"""Return predicates that exclude discovered paths when they return false."""
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
def globals(self) -> Functions:
|
|
68
|
+
"""Return global callables.
|
|
69
|
+
|
|
70
|
+
Deprecated:
|
|
71
|
+
Use `functions()` and `data()` instead.
|
|
72
|
+
"""
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
AbstractLoader = Plugin
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
from collections import abc
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from typing import Any, Protocol
|
|
4
|
-
|
|
5
|
-
from jinja2 import Environment
|
|
6
|
-
from jinja2.ext import Extension
|
|
7
|
-
|
|
8
|
-
from makejinja.config import Config
|
|
9
|
-
|
|
10
|
-
__all__ = ["Plugin"]
|
|
11
|
-
|
|
12
|
-
Extensions = abc.Sequence[type[Extension]]
|
|
13
|
-
Filter = abc.Callable[[Any], Any]
|
|
14
|
-
Filters = abc.Sequence[Filter]
|
|
15
|
-
Function = abc.Callable[..., Any]
|
|
16
|
-
Functions = abc.Sequence[Function]
|
|
17
|
-
Test = abc.Callable[..., Any]
|
|
18
|
-
Tests = abc.Sequence[Test]
|
|
19
|
-
Policies = abc.Mapping[str, Any]
|
|
20
|
-
MutableData = abc.MutableMapping[str, Any]
|
|
21
|
-
Data = abc.Mapping[str, Any]
|
|
22
|
-
PathFilter = abc.Callable[[Path], bool]
|
|
23
|
-
PathFilters = abc.Sequence[PathFilter]
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class Plugin(Protocol):
|
|
27
|
-
"""Extend the functionality of makejinja with a plugin implementing a subset of this protocol."""
|
|
28
|
-
|
|
29
|
-
def __init__(self, *, env: Environment, data: Data, config: Config) -> None:
|
|
30
|
-
pass
|
|
31
|
-
|
|
32
|
-
def functions(self) -> Functions:
|
|
33
|
-
return []
|
|
34
|
-
|
|
35
|
-
def data(self) -> Data:
|
|
36
|
-
return {}
|
|
37
|
-
|
|
38
|
-
def filters(self) -> Filters:
|
|
39
|
-
return []
|
|
40
|
-
|
|
41
|
-
def tests(self) -> Tests:
|
|
42
|
-
return []
|
|
43
|
-
|
|
44
|
-
def policies(self) -> Policies:
|
|
45
|
-
return {}
|
|
46
|
-
|
|
47
|
-
def extensions(self) -> Extensions:
|
|
48
|
-
return []
|
|
49
|
-
|
|
50
|
-
def path_filters(self) -> PathFilters:
|
|
51
|
-
return []
|
|
52
|
-
|
|
53
|
-
# Deprecated: Use functions() and data() instead
|
|
54
|
-
def globals(self) -> Functions:
|
|
55
|
-
return []
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
AbstractLoader = Plugin
|
|
File without changes
|
|
File without changes
|