Template compiler and interpreter

Import Template, TemplateProgram, render_template, TemplateFilter, and TEMPLATE_FILTERS from rmote.templates. The classes below implement the compiled representation; the package exports the same class objects.

See Templating for the restricted language, trust boundaries, and examples. Filters are documented in Template filters.

Template tokens, parsing, and compiled intermediate representation.

class rmote.templates.engine.AssignmentTarget(names, unpack=False)[source]

Bases: object

assign(context, value)[source]
Return type:

None

names: tuple[str, ...]
unpack: bool = False
class rmote.templates.engine.AttributeExpression(value, name)[source]

Bases: ResolvableExpression

evaluate(context)[source]

Return a value, raising on missing variables or lookups.

Return type:

Any

name: str
resolve(context)[source]

Allow lookup expressions to retain missing values for default.

Return type:

Any

value: Expression
class rmote.templates.engine.BinaryExpression(operator, left, right)[source]

Bases: Expression

evaluate(context)[source]

Return a value, raising on missing variables or lookups.

Return type:

Any

left: Expression
operator: str
right: Expression
class rmote.templates.engine.Branch(condition, body)[source]

Bases: object

body: tuple[TemplateNode, ...]
condition: Expression | None
class rmote.templates.engine.BreakNode(position)[source]

Bases: TemplateNode

invoke(context)[source]
Return type:

None

class rmote.templates.engine.CodeToken(type, string, position)[source]

Bases: object

One code lexeme with its token type and absolute source offset.

position: int
string: str
type: int
class rmote.templates.engine.CollectionExpression(kind, items)[source]

Bases: Expression

evaluate(context)[source]

Return a value, raising on missing variables or lookups.

Return type:

Any

items: tuple[Expression, ...]
kind: str
class rmote.templates.engine.CommandToken(value, position, name, tokens)[source]

Bases: TemplateToken

A command name and argument tokens, separated once by the lexer.

name: str
tokens: tuple[CodeToken, ...]
class rmote.templates.engine.CommentToken(value, position)[source]

Bases: TemplateToken

A comment retained for inspection but omitted from execution.

class rmote.templates.engine.ComparisonExpression(operands, operators)[source]

Bases: Expression

evaluate(context)[source]

Return a value, raising on missing variables or lookups.

Return type:

Any

operands: tuple[Expression, ...]
operators: tuple[str, ...]
class rmote.templates.engine.ConditionalExpression(condition, positive, negative)[source]

Bases: Expression

condition: Expression
evaluate(context)[source]

Return a value, raising on missing variables or lookups.

Return type:

Any

negative: Expression
positive: Expression
class rmote.templates.engine.ContinueNode(position)[source]

Bases: TemplateNode

invoke(context)[source]
Return type:

None

class rmote.templates.engine.DictionaryExpression(items)[source]

Bases: Expression

evaluate(context)[source]

Return a value, raising on missing variables or lookups.

Return type:

Any

items: tuple[tuple[Expression, Expression], ...]
class rmote.templates.engine.Expression[source]

Bases: object

A compiled expression with no access to Python globals or callables.

evaluate(context)[source]

Return a value, raising on missing variables or lookups.

Return type:

Any

resolve(context)[source]

Allow lookup expressions to retain missing values for default.

Return type:

Any

class rmote.templates.engine.ExpressionParser(tokens, filters)[source]

Bases: object

Pratt parser for the expression allowlist, including postfix filters.

COMPARISON_PRECEDENCE = 40
CONDITIONAL_PRECEDENCE = 5
LITERALS = {'False': False, 'None': None, 'True': True, 'false': False, 'none': None, 'true': True}
PRECEDENCE = {'!=': 40, '%': 60, '*': 60, '**': 80, '+': 50, '-': 50, '.': 100, '/': 60, '//': 60, '<': 40, '<=': 40, '==': 40, '>': 40, '>=': 40, '[': 100, 'and': 20, 'if': 5, 'in': 40, 'is': 40, 'is not': 40, 'not in': 40, 'or': 10, '|': 90}
PREFIX_PRECEDENCE = {'+': 95, '-': 95, 'not': 35}
assignment()[source]
Return type:

tuple[AssignmentTarget, Expression]

dictionary_or_set()[source]
Return type:

Expression

expression(minimum=0)[source]
Return type:

Expression

filter_call(left)[source]
Return type:

Expression

finish()[source]
Return type:

None

for_header()[source]
Return type:

tuple[AssignmentTarget, Expression]

infix(left, op)[source]
Return type:

Expression

operation()[source]
Return type:

Text

parse()[source]
Return type:

Expression

peek(distance=0)[source]
Return type:

Text

primary()[source]
Return type:

Expression

sequence(closing, parse_item, first=())[source]

Read comma-separated items with an optional trailing comma.

Return type:

tuple[TypeVar(SequenceItem), ...]

subscript(left)[source]
Return type:

Expression

take(expected=None)[source]
Return type:

CodeToken

take_operation(op)[source]
Return type:

None

target(until)[source]
Return type:

AssignmentTarget

class rmote.templates.engine.ExpressionToken(value, position, tokens)[source]

Bases: TemplateToken

An interpolation and its code tokens, ready for the expression parser.

tokens: tuple[CodeToken, ...]
class rmote.templates.engine.FilterExpression(name, value, arguments, keywords)[source]

Bases: ResolvableExpression

arguments: tuple[Expression, ...]
keywords: tuple[tuple[str, Expression], ...]
name: str
resolve(context)[source]

Allow lookup expressions to retain missing values for default.

Return type:

Any

value: Expression
class rmote.templates.engine.ForNode(position, target, iterable, body, otherwise)[source]

Bases: TemplateNode

body: tuple[TemplateNode, ...]
invoke(context)[source]
Return type:

None

iterable: Expression
otherwise: tuple[TemplateNode, ...]
target: AssignmentTarget
class rmote.templates.engine.IfNode(position, branches)[source]

Bases: TemplateNode

branches: tuple[Branch, ...]
invoke(context)[source]
Return type:

None

class rmote.templates.engine.LiteralExpression(value)[source]

Bases: Expression

evaluate(context)[source]

Return a value, raising on missing variables or lookups.

Return type:

Any

value: Any
class rmote.templates.engine.LookupExpression(value, key)[source]

Bases: ResolvableExpression

evaluate(context)[source]

Return a value, raising on missing variables or lookups.

Return type:

Any

key: Expression
resolve(context)[source]

Allow lookup expressions to retain missing values for default.

Return type:

Any

value: Expression
class rmote.templates.engine.OutputNode(position, expression)[source]

Bases: TemplateNode

expression: Expression
invoke(context)[source]
Return type:

None

class rmote.templates.engine.RenderContext(values, filters)[source]

Bases: object

Per-render values, trusted filter instances, and output buffer.

add(values)[source]

Add values that still need a check to a context already built.

Return type:

None

check(name)[source]

Check one context value, once, before the template reads it.

Return type:

None

invoke(nodes)[source]
Return type:

None

static normalise(value, *, trusted=False, seen=None)[source]

Validate context without copying; materialize trusted iterator results.

Return type:

Any

output: list[str]
static validate(value, seen)[source]

Walk only containers recursively; scalar children need no function call.

Return type:

None

class rmote.templates.engine.ResolvableExpression[source]

Bases: Expression

A lookup or filter whose missing result can be consumed by default.

evaluate(context)[source]

Return a value, raising on missing variables or lookups.

Return type:

Any

resolve(context)[source]

Allow lookup expressions to retain missing values for default.

Return type:

Any

class rmote.templates.engine.SetNode(position, target, expression)[source]

Bases: TemplateNode

expression: Expression
invoke(context)[source]
Return type:

None

target: AssignmentTarget
class rmote.templates.engine.SliceExpression(start, stop, step)[source]

Bases: Expression

evaluate(context)[source]

Return a value, raising on missing variables or lookups.

Return type:

Any

start: Expression | None
step: Expression | None
stop: Expression | None
class rmote.templates.engine.Template(template, filters=mappingproxy({'lower': <class 'rmote.templates.filters.LowerFilter'>, 'upper': <class 'rmote.templates.filters.UpperFilter'>, 'capitalize': <class 'rmote.templates.filters.CapitalizeFilter'>, 'trim': <class 'rmote.templates.filters.TrimFilter'>, 'replace': <class 'rmote.templates.filters.ReplaceFilter'>, 'join': <class 'rmote.templates.filters.JoinFilter'>, 'length': <class 'rmote.templates.filters.LengthFilter'>, 'count': <class 'rmote.templates.filters.LengthFilter'>, 'default': <class 'rmote.templates.filters.DefaultFilter'>, 'd': <class 'rmote.templates.filters.DefaultFilter'>, 'sort': <class 'rmote.templates.filters.SortFilter'>, 'unique': <class 'rmote.templates.filters.UniqueFilter'>, 'first': <class 'rmote.templates.filters.FirstFilter'>, 'last': <class 'rmote.templates.filters.LastFilter'>, 'reverse': <class 'rmote.templates.filters.ReverseFilter'>, 'list': <class 'rmote.templates.filters.ListFilter'>, 'int': <class 'rmote.templates.filters.IntFilter'>, 'float': <class 'rmote.templates.filters.FloatFilter'>, 'string': <class 'rmote.templates.filters.StringFilter'>, 'tojson': <class 'rmote.templates.filters.ToJSONFilter'>, 'ipaddress': <class 'rmote.templates.filters.IPAddressFilter'>}))[source]

Bases: object

Compile a restricted template language to reusable intermediate nodes.

Parameters:
  • template (Text) – Jinja-like source with expressions, if/for/set and filters.

  • filters (Mapping[Text, type[TemplateFilter]]) – Complete name-to-class mapping; defaults to TEMPLATE_FILTERS.

Construction tokenizes and compiles the template once. program exposes the resulting immutable TemplateProgram. Rendering only invokes that program with fresh variable bindings and filter instances. Context containers are checked without copying, and a container is checked when the template first reads its name, so a value the template never reads costs nothing. A whole frozen snapshot, passed as the only positional argument, is accepted as it is. Pickle transfers the program and filter classes; remote rendering does not reparse the source.

There are no Python statements, arbitrary calls, or private attributes. Context values must be plain data. Registered filters and pickle inputs are trusted code; this language is not an OS or resource-isolation sandbox.

>>> Template("{{ names | unique | sort | join(', ') }}").render(names=["b", "a", "b"])
'a, b'
>>> Template("listen {{ port }}{% if ssl %} ssl{% endif %};").render(port=443, ssl=True)
'listen 443 ssl;'
__reduce__()[source]

Helper for pickle.

Return type:

tuple[Any, tuple[TemplateProgram]]

static compile(template, filters=(('capitalize', <class 'rmote.templates.filters.CapitalizeFilter'>), ('count', <class 'rmote.templates.filters.LengthFilter'>), ('d', <class 'rmote.templates.filters.DefaultFilter'>), ('default', <class 'rmote.templates.filters.DefaultFilter'>), ('first', <class 'rmote.templates.filters.FirstFilter'>), ('float', <class 'rmote.templates.filters.FloatFilter'>), ('int', <class 'rmote.templates.filters.IntFilter'>), ('ipaddress', <class 'rmote.templates.filters.IPAddressFilter'>), ('join', <class 'rmote.templates.filters.JoinFilter'>), ('last', <class 'rmote.templates.filters.LastFilter'>), ('length', <class 'rmote.templates.filters.LengthFilter'>), ('list', <class 'rmote.templates.filters.ListFilter'>), ('lower', <class 'rmote.templates.filters.LowerFilter'>), ('replace', <class 'rmote.templates.filters.ReplaceFilter'>), ('reverse', <class 'rmote.templates.filters.ReverseFilter'>), ('sort', <class 'rmote.templates.filters.SortFilter'>), ('string', <class 'rmote.templates.filters.StringFilter'>), ('tojson', <class 'rmote.templates.filters.ToJSONFilter'>), ('trim', <class 'rmote.templates.filters.TrimFilter'>), ('unique', <class 'rmote.templates.filters.UniqueFilter'>), ('upper', <class 'rmote.templates.filters.UpperFilter'>)))[source]

Cache compilation by source and filter classes, returning callable IR.

Return type:

TemplateProgram

classmethod from_program(program)[source]

Restore an already compiled program without lexing or parsing.

Return type:

Template

render(context=None, /, **ctx)[source]

Invoke the compiled program using plain context data.

Keyword values are checked when the template first reads them. A mapping passed as the only positional argument is used as the whole context; a frozen snapshot of rmote.immutable.freeze() is then accepted as it is, because it holds plain data by construction.

Return type:

Text

exception rmote.templates.engine.TemplateBreak[source]

Bases: Exception

Internal control-flow signal for a loop break.

exception rmote.templates.engine.TemplateContinue[source]

Bases: Exception

Internal control-flow signal for a loop continue.

class rmote.templates.engine.TemplateLexer(source)[source]

Bases: object

Split template text into typed tokens without interpreting expressions.

code(start, opening, kind, closing)[source]

Tokenize one tag through its closing delimiter, retaining code tokens.

Return type:

tuple[tuple[CodeToken, ...], getint, getint]

tokenize()[source]
Return type:

tuple[TemplateToken, ...]

class rmote.templates.engine.TemplateNode(position)[source]

Bases: object

An executable instruction in the immutable template program.

invoke(context)[source]
Return type:

None

position: int
class rmote.templates.engine.TemplateParser(source, tokens, filters)[source]

Bases: object

Compile typed source tokens into nested executable instructions.

TERMINATORS = frozenset({'end', 'endfor', 'endif'})
assignment(token)[source]
Return type:

SetNode

body(stops=frozenset({}))[source]
Return type:

tuple[TemplateNode, ...]

condition(opening)[source]
Return type:

IfNode

end(opening, expected)[source]
Return type:

None

expression(token)[source]
Return type:

Expression

location(token)[source]

Attach source diagnostics once, retaining nested command locations.

Return type:

Iterator[None]

loop(opening)[source]
Return type:

ForNode

statement(token)[source]
Return type:

TemplateNode | None

class rmote.templates.engine.TemplateProgram(source, nodes, filters)[source]

Bases: object

Immutable, pickleable IR. Invocation never parses or compiles source.

__call__(context=None, /, **values)[source]

Render the program. A frozen snapshot passed whole is not re-checked.

Return type:

Text

property active: tuple[tuple[str, type[TemplateFilter]], ...]

The filters this template names, found once for the program.

A render builds an instance of each of these and of no other. The whole mapping stays in filters for transfer and for pickle, including a program pickled before this answer existed.

filters: tuple[tuple[str, type[TemplateFilter]], ...]
nodes: tuple[TemplateNode, ...]
source: TemplateSource
class rmote.templates.engine.TemplateSource(text)[source]

Bases: object

Source text and diagnostics at an exact character offset.

error(message, position)[source]
Return type:

SyntaxError

text: str
exception rmote.templates.engine.TemplateSyntaxError[source]

Bases: SyntaxError

A syntax error already located in the original template source.

class rmote.templates.engine.TemplateToken(value, position)[source]

Bases: object

A lexical token retaining its source position and unmodified contents.

position: int
value: str
class rmote.templates.engine.TextNode(position, text)[source]

Bases: TemplateNode

invoke(context)[source]
Return type:

None

text: str
class rmote.templates.engine.TextToken(value, position)[source]

Bases: TemplateToken

Literal output, after processing source whitespace and escaped delimiters.

class rmote.templates.engine.UnaryExpression(operator, value)[source]

Bases: Expression

evaluate(context)[source]

Return a value, raising on missing variables or lookups.

Return type:

Any

operator: str
value: Expression
class rmote.templates.engine.VariableExpression(name)[source]

Bases: ResolvableExpression

name: str
resolve(context)[source]

Allow lookup expressions to retain missing values for default.

Return type:

Any

rmote.templates.engine.checked_filters(filters)[source]

Check filter names and classes once for one set of pairs.

The answer depends only on the pairs, so it is cached by them. A refused set raises instead of entering the cache, and is refused again next time.

Return type:

tuple[tuple[Text, type[TemplateFilter]], ...]

rmote.templates.engine.named_filters(nodes)[source]

Collect the filters a program names, by walking its nodes once.

Return type:

frozenset[Text]

rmote.templates.engine.render_template(template, context=None, /, **ctx)[source]

Compile a template to IR and render it with the supplied data.

Return type:

Text

rmote.templates.engine.template_identifier(value)[source]

Validate a public name in the restricted language.

Return type:

Text