Skip to content

Commit 3db7756

Browse files
committed
Refactor template resolution and improve performance
1 parent 8c60f1b commit 3db7756

File tree

1 file changed

+32
-26
lines changed

1 file changed

+32
-26
lines changed

src/template_resolver.py

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,42 +13,48 @@ def __init__(self, base_dir: str) -> None:
1313
self._env = NativeEnvironment(
1414
trim_blocks=True, lstrip_blocks=True, autoescape=False
1515
)
16+
self._env.globals["load"] = self._load_file
17+
self._variables: dict[str, Any] = {}
1618

1719
def resolve(
1820
self, data: dict[str, Any], extra_vars: dict[str, Any]
1921
) -> dict[str, Any]:
2022
"""Resolve all Jinja templates in data using iterative passes."""
21-
resolved = {**copy.deepcopy(data), **extra_vars}
22-
context = [resolved] # Mutable wrapper for closure access
23+
self._variables = {**copy.deepcopy(data), **extra_vars}
2324

2425
for _ in range(10):
25-
previous = copy.deepcopy(resolved)
26-
context[0] = resolved
27-
self._env.globals["load"] = self._make_loader(context)
28-
resolved = self._resolve_recursive(resolved, context)
29-
if resolved == previous:
30-
return resolved
26+
self._variables, changed = self._resolve_value(self._variables)
27+
if not changed:
28+
return self._variables
3129

3230
raise RuntimeError("Template resolution did not converge")
3331

34-
def _resolve_recursive(self, obj: Any, context: list[dict]) -> Any:
32+
def _resolve_value(self, obj: Any) -> tuple[Any, bool]:
3533
if isinstance(obj, dict):
36-
return {k: self._resolve_recursive(v, context) for k, v in obj.items()}
34+
changed = False
35+
result: dict[str, Any] = {}
36+
for k, v in obj.items():
37+
new_v, v_changed = self._resolve_value(v)
38+
result[k] = new_v
39+
changed = changed or v_changed
40+
return result, changed
3741
if isinstance(obj, list):
38-
return [self._resolve_recursive(i, context) for i in obj]
42+
changed = False
43+
items: list[Any] = []
44+
for item in obj:
45+
new_item, item_changed = self._resolve_value(item)
46+
items.append(new_item)
47+
changed = changed or item_changed
48+
return items, changed
3949
if isinstance(obj, str) and "{{" in obj and "}}" in obj:
40-
template = self._env.from_string(obj)
41-
return template.render(**context[0])
42-
return obj
43-
44-
def _make_loader(self, context: list[dict]):
45-
def load(filepath: str) -> str | Any:
46-
full_path = os.path.abspath(os.path.join(self._base_dir, filepath))
47-
with open(full_path) as f:
48-
if filepath.endswith((".yml", ".yaml")):
49-
return yaml.load(f)
50-
content = f.read()
51-
template = self._env.from_string(content)
52-
return template.render(**context[0])
53-
54-
return load
50+
rendered = self._env.from_string(obj).render(**self._variables)
51+
return rendered, rendered != obj
52+
return obj, False
53+
54+
def _load_file(self, filepath: str) -> str | Any:
55+
full_path = os.path.abspath(os.path.join(self._base_dir, filepath))
56+
with open(full_path) as f:
57+
if filepath.endswith((".yml", ".yaml")):
58+
return yaml.load(f)
59+
content = f.read()
60+
return self._env.from_string(content).render(**self._variables)

0 commit comments

Comments
 (0)