from __future__ import annotations
from collections.abc import Mapping
from typing import Any
class SimpleTemplateError(Exception):
pass
class SimpleTemplateSyntaxError(SimpleTemplateError):
pass
class SimpleTemplateUndefinedError(SimpleTemplateError):
pass
def _is_ident(s: str) -> bool:
if not s:
return False
if not (s[0].isalpha() or s[0] == "_"):
return False
for ch in s[1:]:
if not (ch.isalnum() or ch == "_"):
return False
return True
def _resolve_path(path: str, ctx: Mapping[str, Any]) -> Any:
# Supports dotted dict paths like "vars.file_path".
# Intentionally *no* attribute access, *no* calls, *no* expression evaluation.
parts = path.split(".")
cur: Any = ctx
cur_path = ""
for key in parts:
if not _is_ident(key):
raise SimpleTemplateSyntaxError(f"Invalid placeholder name: {path!r}")
if not isinstance(cur, Mapping):
where = cur_path or "<root>"
raise SimpleTemplateUndefinedError(
f"Missing variable path {path!r}: {where} is not a dict (cannot access {key!r})"
)
if key not in cur:
available = ", ".join(sorted(str(k) for k in cur.keys()))
where = cur_path or "<root>"
raise SimpleTemplateUndefinedError(
f"Missing variable {path!r}: key {key!r} not found at {where}. Available keys: {available}"
)
cur = cur[key]
cur_path = key if not cur_path else f"{cur_path}.{key}"
return cur
def validate_simple_template(template: str, *, template_name: str) -> None:
"""Validate template syntax without needing any variables.
Supported:
- Escapes: '{{{{' -> '{{', '}}}}' -> '}}'
- Placeholders: '{{ name }}' or '{{ vars.file_path }}' (ident / dotted ident only)
"""
if not isinstance(template, str):
raise SimpleTemplateSyntaxError(f"{template_name} must be a string")
i = 0
n = len(template)
while i < n:
if template.startswith("{{{{", i) or template.startswith("}}}}", i):
i += 4
continue
if template.startswith("}}", i):
raise SimpleTemplateSyntaxError(
f"{template_name} has stray '}}' at index {i}. Use '}}}}' to output a literal '}}'."
)
if template.startswith("{{", i):
end = template.find("}}", i + 2)
if end == -1:
raise SimpleTemplateSyntaxError(
f"{template_name} has unclosed '{{{{' starting at index {i}. Use '{{{{' to output a literal '{{'."
)
expr = template[i + 2 : end].strip()
if not expr:
raise SimpleTemplateSyntaxError(
f"{template_name} contains empty placeholder '{{{{ }}}}' at index {i}"
)
parts = expr.split(".")
if not all(_is_ident(p) for p in parts):
raise SimpleTemplateSyntaxError(
f"{template_name} placeholder {expr!r} at index {i} is invalid. "
"Only names like {{ var }} or dotted dict paths like {{ vars.file_path }} are supported."
)
i = end + 2
continue
i += 1
def render_simple_template(template: str, ctx: Mapping[str, Any]) -> str:
"""Render template using ctx.
- Escapes: '{{{{' -> '{{', '}}}}' -> '}}'
- Placeholders: '{{ name }}' or '{{ vars.file_path }}'
- Missing variables raise SimpleTemplateUndefinedError
"""
out: list[str] = []
i = 0
n = len(template)
while i < n:
if template.startswith("{{{{", i):
out.append("{{")
i += 4
continue
if template.startswith("}}}}", i):
out.append("}}")
i += 4
continue
if template.startswith("}}", i):
raise SimpleTemplateSyntaxError(
f"Stray '}}' at index {i}. Use '}}}}' to output a literal '}}'."
)
if template.startswith("{{", i):
end = template.find("}}", i + 2)
if end == -1:
raise SimpleTemplateSyntaxError(
f"Unclosed '{{{{' starting at index {i}. Use '{{{{' to output a literal '{{'."
)
expr = template[i + 2 : end].strip()
if not expr:
raise SimpleTemplateSyntaxError(f"Empty placeholder '{{{{ }}}}' at index {i}")
value = _resolve_path(expr, ctx)
out.append("" if value is None else str(value))
i = end + 2
continue
out.append(template[i])
i += 1
return "".join(out)