Skip to content

Commit 05cb2c1

Browse files
committed
feat: Don't fail just warn about undefined Jinja variables
Stumbled over this when I used an existing template for --create-from. Maybe introduce a --fail-undefined-vars option later.
1 parent 70f956c commit 05cb2c1

File tree

2 files changed

+101
-19
lines changed

2 files changed

+101
-19
lines changed

src/pdfbaker/page.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from pathlib import Path
1010

11+
import jinja2
1112
from jinja2.exceptions import TemplateError, TemplateNotFound
1213

1314
from .config import PathSpec
@@ -30,29 +31,22 @@ def __init__(self, config_path: PathSpec, page_number: int, **kwargs):
3031
)
3132
self.log_trace(self.config.readable())
3233

33-
def process(self) -> Path:
34-
"""Render SVG template and convert to PDF."""
35-
self.log_debug_subsection(
36-
"Processing page %d: %s", self.config.page_number, self.config.name
37-
)
38-
39-
self.log_debug("Loading template: %s", self.config.template.name)
40-
if self.logger.isEnabledFor(TRACE):
41-
with open(self.config.template.path, encoding="utf-8") as f:
42-
self.log_trace_preview(f.read())
43-
34+
def _load_jinja_template(self, undefined_vars) -> jinja2.Template:
4435
try:
45-
jinja_extensions = self.config.jinja2_extensions
46-
if jinja_extensions:
47-
self.log_debug("Using Jinja2 extensions: %s", jinja_extensions)
36+
if self.config.jinja2_extensions:
37+
self.log_debug(
38+
"Using Jinja2 extensions: %s", self.config.jinja2_extensions
39+
)
4840
jinja_env = create_env(
4941
templates_dir=self.config.template.path.parent,
50-
extensions=jinja_extensions,
42+
extensions=self.config.jinja2_extensions,
5143
template_filters=[
5244
filter.value for filter in self.config.template_filters
5345
],
46+
undefined_vars=undefined_vars,
47+
template_file=str(self.config.template.path),
5448
)
55-
template = jinja_env.get_template(self.config.template.path.name)
49+
return jinja_env.get_template(self.config.template.path.name)
5650
except TemplateNotFound as exc:
5751
raise SVGTemplateError(
5852
"Failed to load template for page "
@@ -64,16 +58,31 @@ def process(self) -> Path:
6458
f"{self.config.page_number} ({self.config.name}): {exc}"
6559
) from exc
6660

61+
def process(self) -> Path:
62+
"""Render SVG template and convert to PDF."""
63+
self.log_debug_subsection(
64+
"Processing page %d: %s", self.config.page_number, self.config.name
65+
)
66+
67+
self.log_debug("Loading template: %s", self.config.template.name)
68+
if self.logger.isEnabledFor(TRACE):
69+
with open(self.config.template.path, encoding="utf-8") as f:
70+
self.log_trace_preview(f.read())
71+
72+
undefined_vars = set()
73+
template = self._load_jinja_template(undefined_vars)
74+
6775
context = self.config.resolve_variables().model_dump()
6876
template_context = prepare_template_context(
6977
context=context,
7078
images_dir=self.config.directories.images,
7179
)
7280

7381
build_dir = self.config.directories.build
74-
name = self.config.name
7582
if self.config.is_variant:
76-
name = f'{name}_{self.config.variant["name"]}'
83+
name = f'{self.config.name}_{self.config.variant["name"]}'
84+
else:
85+
name = self.config.name
7786
output_svg = build_dir / f"{self.config.page_number:03}_{name}.svg"
7887
output_pdf = build_dir / f"{self.config.page_number:03}_{name}.pdf"
7988

@@ -85,6 +94,13 @@ def process(self) -> Path:
8594
renderer.value for renderer in self.config.template_renderers
8695
],
8796
)
97+
if undefined_vars:
98+
for var, template_file in sorted(undefined_vars):
99+
self.log_warning(
100+
'Undefined variable "%s" in template %s',
101+
var,
102+
template_file,
103+
)
88104
if self.config.dry_run:
89105
self.log_debug(
90106
"👀 [DRY RUN] Not writing rendered template to %s", output_svg

src/pdfbaker/render.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,19 +71,85 @@ def replacer(match: re.Match[str]) -> str:
7171
return rendered
7272

7373

74+
class PDFBakerUndefined(jinja2.Undefined):
75+
"""Custom Undefined that collects undefined variable names and template file."""
76+
77+
def __init__(self, *args, undefined_vars=None, template_file=None, **kwargs):
78+
super().__init__(*args, **kwargs)
79+
self._undefined_vars = undefined_vars
80+
self._template_file = template_file
81+
if self._undefined_vars is not None and self._undefined_name is not None:
82+
self._undefined_vars.add((self._undefined_name, self._template_file))
83+
84+
def _fail_with_undefined_error(self, *args, **kwargs):
85+
if self._undefined_vars is not None and self._undefined_name is not None:
86+
self._undefined_vars.add((self._undefined_name, self._template_file))
87+
return ""
88+
89+
def __str__(self):
90+
return self._fail_with_undefined_error()
91+
92+
def __getattr__(self, name):
93+
return self._fail_with_undefined_error()
94+
95+
def __call__(self, *args, **kwargs):
96+
return self._fail_with_undefined_error()
97+
98+
def __iter__(self):
99+
# Allows {% for ... in ... %} to not fail
100+
self._fail_with_undefined_error()
101+
return iter([])
102+
103+
def __bool__(self):
104+
# Allows {% if ... %} to not fail
105+
self._fail_with_undefined_error()
106+
return False
107+
108+
def __len__(self):
109+
self._fail_with_undefined_error()
110+
return 0
111+
112+
def __getitem__(self, key):
113+
# Allows {{ ...[0].something }} to not fail
114+
return self._fail_with_undefined_error()
115+
116+
74117
def create_env(
75118
templates_dir: Path | None = None,
76119
extensions: list[str] | None = None,
77120
template_filters: list[str] | None = None,
121+
undefined_vars: set | None = None,
122+
template_file: str | None = None,
78123
) -> jinja2.Environment:
79-
"""Create and configure the Jinja environment."""
124+
"""Create and configure the Jinja environment.
125+
126+
Args:
127+
templates_dir: Directory containing templates
128+
extensions: List of Jinja2 extensions
129+
template_filters: List of template filter names
130+
undefined_vars: Set to collect (var, template_file) tuples for undefined vars
131+
template_file: Name of the template file being rendered (for error reporting)
132+
"""
80133
if templates_dir is None:
81134
raise ValueError("templates_dir is required")
82135

136+
# pylint: disable=too-few-public-methods
137+
class CustomUndefined(PDFBakerUndefined):
138+
"""Undefined class that collects undefined variables per template."""
139+
140+
def __init__(self, *args, **kwargs):
141+
super().__init__(
142+
*args,
143+
undefined_vars=undefined_vars,
144+
template_file=template_file,
145+
**kwargs,
146+
)
147+
83148
env = jinja2.Environment(
84149
loader=jinja2.FileSystemLoader(str(templates_dir)),
85150
autoescape=jinja2.select_autoescape(),
86151
extensions=extensions or [],
152+
undefined=CustomUndefined,
87153
)
88154
env.template_class = PDFBakerTemplate
89155

0 commit comments

Comments
 (0)