Source code for anza.engine.file.load

"""Template loading: read, size-check, parse and digest ``.html`` files."""

import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Union

from ...crypto import digest
from ...errors import AnzaError
from ..slot import Chunk, extract
from ..slot.bind import Renderer

#: Default cap on a template file's size: 1 MiB.
DEFAULT_MAX_TEMPLATE_BYTES = 1024 * 1024


[docs] @dataclass class Template: """One parsed template, compiled once for fast binding. Templates are built by :meth:`Setup.run() <anza.Setup.run>` and looked up with :meth:`Engine.get_template() <anza.Engine.get_template>`. Example: >>> from anza import Setup >>> tpl = Setup(root="templates").run().get_template("feed/card.html") >>> tpl.name, tpl.raw ('feed/card.html', '<article>{{title}}</article>') >>> tpl.render({"title": "Hi"}) '<article>Hi</article>' """ #: Name relative to the root, ``/``-separated (``"feed/card.html"``). name: str #: Path of the file it was loaded from. path: Path #: The template source. raw: str #: Lowercase-hex SHA-256 of :attr:`raw`. digest: str #: The parsed static text and slots (see :func:`~anza.extract`). chunks: list[Chunk] #: HTML-escape ``{{slot}}`` values (``Setup.escape``). escape: bool = False renderer: Renderer = field(init=False, repr=False, compare=False) def __post_init__(self) -> None: self.renderer = Renderer(self.chunks, self.escape)
[docs] def bind(self, params: Any = None) -> str: """Bind ``params`` into the template; same as :meth:`render`. Args: params: A dict, ``(key, value)`` pairs, or an object whose public, non-callable attributes are read. ``None`` renders every slot empty. Returns: str: The rendered HTML. """ return self.renderer(params)
[docs] def render(self, params: Any = None) -> str: """Bind ``params`` into the template (see :meth:`bind`).""" return self.renderer(params)
def _inside(root: Path, path: Path) -> bool: try: return path.resolve(strict=True).is_relative_to(root.resolve(strict=True)) except (OSError, RuntimeError): return False def one( root_dir: Union[str, Path], rel_path: Union[str, Path], escape: bool = False, max_bytes: int = DEFAULT_MAX_TEMPLATE_BYTES, ) -> Template: """Load one template. Errors name the template, never the full path: a file that resolves (through a symlink) outside ``root_dir``, or that is larger than ``max_bytes``, is refused.""" root = Path(root_dir) name = Path(rel_path).as_posix().lstrip("/") full_path = root / rel_path if not _inside(root, full_path): raise AnzaError.not_found(f"Template '{name}' is outside the template root") try: with open(full_path, "rb") as f: data = f.read(max_bytes + 1) except OSError as e: raise AnzaError.not_found(f"Failed to read template '{name}': {type(e).__name__}") from None if len(data) > max_bytes: raise AnzaError.template(f"Template '{name}' is larger than {max_bytes} bytes") try: raw = data.decode("utf-8") except UnicodeDecodeError: raise AnzaError.template(f"Template '{name}' is not valid UTF-8") from None return Template( name=name, path=full_path, raw=raw, digest=digest.hex(raw), chunks=extract(raw), escape=escape, ) def files(root_dir: Union[str, Path]) -> list[Path]: """Every ``.html`` file under ``root_dir``, relative to it. Symlinked directories are not followed; symlinked files that resolve outside the root are skipped.""" root = Path(root_dir) out: list[Path] = [] for current, _dirs, names in os.walk(root, followlinks=False): for fname in names: if not fname.endswith(".html"): continue path = Path(current) / fname if path.is_symlink() and not _inside(root, path): continue if path.is_file(): out.append(path.relative_to(root)) return out def all( root_dir: Union[str, Path], escape: bool = False, max_bytes: int = DEFAULT_MAX_TEMPLATE_BYTES, ) -> dict[str, Template]: """Load every ``.html`` file under ``root_dir`` (see ``files``).""" root = Path(root_dir) templates: dict[str, Template] = {} if not root.exists(): return templates for rel_path in files(root): tpl = one(root, rel_path, escape=escape, max_bytes=max_bytes) templates[tpl.name] = tpl return templates