Source code for grunnur.template

from __future__ import annotations

import inspect
import os.path
from typing import Callable, Iterable, Tuple, Dict, Sequence, Any, Mapping, Optional
import re
import warnings

import mako.template


[docs]class RenderError(Exception): """ A custom wrapper for Mako template render errors, to facilitate debugging. """ exception: Exception """The original exception thrown by Mako's `render()`.""" args: Tuple[Any, ...] """The arguments used to render the template.""" globals: Dict[str, Any] """The globals used to render the template.""" source: str """The source of the template.""" def __init__( self, exception: Exception, args: Sequence[Any], globals_: Mapping[str, Any], source: str ): super().__init__() self.exception = exception self.args = tuple(args) self.globals = dict(globals_) self.source = source def __str__(self) -> str: return ( "Failed to render a template with\n" f"* args: {self.args}\n* globals: {self.globals}\n* source:\n{self.source}\n" f"* Mako error: ({type(self.exception).__name__}) {self.exception}" )
def _extract_def_source(source: str, name: str) -> str: """ Attempts to extract the source of a single def from Mako template. This makes error messages much more readable. """ match = re.search( r"(<%def\s+name\s*=\s*[\"']" + name + r"\(.*?>.*?</%def>)", source, flags=re.DOTALL ) if not match: warnings.warn(f"Could not find the template definition '{name}'", SyntaxWarning) return source return match.group(1) def _make_template( filename: Optional[str] = None, text: Optional[str] = None ) -> mako.template.Template: return mako.template.Template( text=text, filename=filename, strict_undefined=True, imports=["import numpy"] )
[docs]class Template: """ A wrapper for mako ``Template`` objects. """
[docs] @classmethod def from_associated_file(cls, filename: str) -> "Template": """ Returns a :py:class:`Template` object created from the file which has the same name as ``filename`` and the extension ``.mako``. Typically used in computation modules as ``Template.from_associated_file(__file__)``. """ path, _ext = os.path.splitext(os.path.abspath(filename)) template_path = path + ".mako" mako_template = _make_template(filename=template_path) return cls(mako_template)
[docs] @classmethod def from_string(cls, template_source: str) -> "Template": """ Returns a :py:class:`Template` object created from source. """ mako_template = _make_template(text=template_source) return cls(mako_template)
def __init__(self, mako_template: "mako.template.Template"): self._mako_template = mako_template self._defs: Dict[str, DefTemplate] = {}
[docs] def get_def(self, name: str) -> "DefTemplate": """ Returns the template def with the name ``name``. """ if name in self._defs: return self._defs[name] if name not in self._mako_template.list_defs(): raise AttributeError(f"Template has no def '{name}'") def_source = _extract_def_source(self._mako_template.source, name) def_template = DefTemplate(name, self._mako_template.get_def(name), def_source) self._defs[name] = def_template return def_template
[docs]class DefTemplate: """ A wrapper for Mako ``DefTemplate`` objects. """
[docs] @classmethod def from_callable(cls, name: str, callable_obj: Callable[..., str]) -> "DefTemplate": """ Creates a template def from a callable returning a string. The parameter list of the callable is used to create the pararameter list of the resulting template def; the callable should return the body of a Mako template def regardless of the arguments it receives. """ signature = inspect.signature(callable_obj) # pass mock values to extract the value args = [None] * len(signature.parameters) return cls._from_signature_and_body(name, signature, callable_obj(*args))
[docs] @classmethod def from_string(cls, name: str, argnames: Iterable[str], source: str) -> "DefTemplate": """ Creates a template def from a string with its body and a list of argument names. """ kind = inspect.Parameter.POSITIONAL_OR_KEYWORD parameters = [inspect.Parameter(name, kind=kind) for name in argnames] signature = inspect.Signature(parameters) return cls._from_signature_and_body(name, signature, source)
@classmethod def _from_signature_and_body( cls, name: str, signature: inspect.Signature, body: str ) -> "DefTemplate": src = "<%def name='" + name + str(signature) + "'>\n" + body + "\n</%def>" mako_def_template = _make_template(text=src).get_def(name) return cls(name, mako_def_template, src) def __init__(self, name: str, mako_def_template: "mako.template.DefTemplate", source: str): self.name = name self._mako_def_template = mako_def_template self.source = source
[docs] def render(self, *args: Any, **globals_: Any) -> str: """ Renders the template def with given arguments and globals. """ try: return self._mako_def_template.render(*args, **globals_) except RenderError as e: # _render() can be called by a chain of templates which call each other; # passing the original render error to the top so that it could be handled there. raise except Exception as e: # TODO: we could collect mako.exceptions.text_error_template().render() here, # because ideally it should point to the line where the error occurred, # but for some reason it doesn't. So we don't bother for now. raise RenderError(e, args, globals_, self.source)