Source code for kajiki.ir

import re
from itertools import chain

from kajiki.util import default_alias_for, flattener, gen_name, window


def generate_python(ir):
    cur_indent = 0
    for node in flattener(ir):
        if isinstance(node, IndentNode):
            cur_indent += 4
        elif isinstance(node, DedentNode):
            cur_indent -= 4
        for line in node.py():
            if isinstance(line, IndentNode):
                cur_indent += 4
            elif isinstance(line, DedentNode):
                cur_indent -= 4
            else:
                yield line.indent(cur_indent)


class Node:
    def __init__(self):
        self.filename = "<string>"
        self.lineno = 0

    def py(self):  # pragma no cover
        return []

    def __iter__(self):
        yield self

    def line(self, text):
        return PyLine(self.filename, self.lineno, text)


class PassNode(Node):
    def py(self):
        # 'pass' would result in: TypeError: 'NoneType' object is not iterable
        yield self.line('yield ""')


class HierNode(Node):
    """Base for nodes that contain an indented Python block (def, for, if etc.)"""

    def __init__(self, body):
        super().__init__()
        self.body = tuple(x for x in body if x is not None)

    def body_iter(self):
        yield from optimize(flattener(map(flattener, self.body)))

    def __iter__(self):
        yield self
        yield IndentNode()
        yield from self.body_iter()
        yield DedentNode()


class IndentNode(Node):
    pass


class DedentNode(Node):
    pass


[docs] class TemplateNode(HierNode): """Represents the root Intermediate Representation node of a template. Iterating over this will generate the Python code for the class that provides all the functions that are part of the template including the ``__main__`` function that represents the template code itself. The generated class will then be passed to :meth:`kajiki.template.Template` to create a :class:`kajiki.template._Template` subclass that has the ``render`` method to render the template. """ class TemplateTail(Node): def py(self): yield self.line("template = kajiki.Template(template)") def __init__(self, mod_py=None, defs=None): super().__init__(defs) if mod_py is None: mod_py = [] if defs is None: defs = [] self.mod_py = [x for x in mod_py if x is not None] def py(self): yield self.line("class template:") def __iter__(self): for x in flattener(self.mod_py): yield x for x in super().__iter__(): yield x yield self.TemplateTail()
class ImportNode(Node): def __init__(self, tpl_name, alias=None): super().__init__() self.tpl_name = tpl_name if alias is None: alias = default_alias_for(tpl_name) self.alias = alias def py(self): yield self.line(f"{self.alias} = local.__kj__.import_({self.tpl_name!r}, {self.alias!r}, self.__globals__)") class IncludeNode(Node): def __init__(self, tpl_name): super().__init__() self.tpl_name = tpl_name def py(self): yield self.line(f"yield local.__kj__.import_({self.tpl_name!r}, None, self.__globals__).__main__()") class ExtendNode(Node): def __init__(self, tpl_name): super().__init__() self.tpl_name = tpl_name def py(self): yield self.line(f"yield local.__kj__.extend({self.tpl_name!r}).__main__()") class DefNode(HierNode): prefix = "@kajiki.expose" def __init__(self, decl, *body): super().__init__(body) self.decl = decl def py(self): yield self.line(self.prefix) yield self.line(f"def {self.decl}:") def __iter__(self): yield self yield IndentNode() is_empty = True for x in self.body_iter(): yield x is_empty = False if is_empty: # In Python, a function without a body is a SyntaxError. yield PassNode() # Prevent creation of a function without a body yield DedentNode() class InnerDefNode(DefNode): prefix = "@__kj__.flattener.decorate" class CallNode(HierNode): class CallTail(Node): def __init__(self, call): super().__init__() self.call = call def py(self): yield self.line("yield " + self.call) def __init__(self, caller, callee, *body): super().__init__(body) fname = gen_name() self.decl = caller.replace("$caller", fname) self.call = callee.replace("$caller", fname) def py(self): yield self.line("@__kj__.flattener.decorate") yield self.line(f"def {self.decl}:") def __iter__(self): yield self yield IndentNode() yield from self.body_iter() yield DedentNode() yield self.CallTail(self.call) class ForNode(HierNode): def __init__(self, decl, *body): super().__init__(body) self.decl = decl def py(self): yield self.line(f"for {self.decl}:") class WithNode(HierNode): assignment_pattern = re.compile(r"(?:^|;)\s*([^;=]+)=(?!=)", re.M) class WithTail(Node): def __init__(self, var_names): super().__init__() self.var_names = var_names def py(self): yield self.line("({},) = local.__kj__.pop_with()".format(",".join(self.var_names))) # yield self.line('if %s == (): del %s' % (v, v)) def __init__(self, vars, *body): # noqa: A002 super().__init__(body) assignments = [] matches = self.assignment_pattern.finditer(vars) for m1, m2 in window(chain(matches, [None]), 2): lhs = m1.group(1).strip() rhs = vars[m1.end() : (m2.start() if m2 else len(vars))] assignments.append((lhs, rhs)) self.vars = assignments self.var_names = [lhs for lhs, _ in assignments] def py(self): yield self.line("local.__kj__.push_with(locals(), [{}])".format(",".join(f'"{k}"' for k in self.var_names))) for k, v in self.vars: yield self.line(f"{k} = {v}") def __iter__(self): yield self yield from self.body_iter() yield self.WithTail(self.var_names) class SwitchNode(HierNode): class SwitchTail(Node): def py(self): yield self.line("local.__kj__.pop_switch()") def __init__(self, decl, *body): super().__init__(body) self.decl = decl def py(self): yield self.line(f"local.__kj__.push_switch({self.decl})") yield self.line("if False: pass") def __iter__(self): yield self yield from self.body_iter() yield self.SwitchTail() class CaseNode(HierNode): def __init__(self, decl, *body): super().__init__(body) self.decl = decl def py(self): yield self.line(f"elif local.__kj__.case({self.decl}):") class MatchNode(HierNode): """Structural Pattern Matching Node""" def __init__(self, decl, *body): super().__init__(body) self.decl = decl def py(self): yield self.line(f"match ({self.decl}):") yield IndentNode() def __iter__(self): yield self yield from self.body_iter() yield DedentNode() class MatchCaseNode(HierNode): """Structural Pattern Matching Case Node""" def __init__(self, decl, *body): super().__init__(body) self.decl = decl def py(self): yield self.line(f"case {self.decl}:") yield IndentNode() def __iter__(self): yield self yield from self.body_iter() yield DedentNode() class IfNode(HierNode): def __init__(self, decl, *body): super().__init__(body) self.decl = decl def py(self): yield self.line(f"if {self.decl}:") class ElseNode(HierNode): def __init__(self, *body): super().__init__(body) def py(self): yield self.line("else:") class TextNode(Node): """Node that outputs Python literals.""" def __init__(self, text, guard=None): super().__init__() self.text = text self.guard = guard def py(self): s = f"yield {self.text!r}" if self.guard: yield self.line(f"if {self.guard}: {s}") else: yield self.line(s) class TranslatableTextNode(TextNode): def py(self): text = self.text.strip() s = f"yield local.__kj__.gettext({self.text!r})" if text else f"yield {self.text!r}" if self.guard: yield self.line(f"if {self.guard}: {s}") else: yield self.line(s) class ExprNode(Node): """Node that contains a Python expression to be evaluated when the template is executed. """ def __init__(self, text, safe=False): # noqa: FBT002 super().__init__() self.text = text self.safe = safe def py(self): if self.safe: yield self.line(f"yield {self.text}") else: yield self.line(f"yield self.__kj__.escape({self.text})") class AttrNode(HierNode): """Node that renders HTML/XML attributes.""" class AttrTail(Node): def __init__(self, parent): super().__init__() self.p = parent def py(self): gen = self.p.genname x = gen_name() yield self.line(f"{gen} = self.__kj__.collect({gen}())") yield self.line(f"for {x} in self.__kj__.render_attrs({{{self.p.attr!r}:{gen}}}, {self.p.mode!r}):") yield self.line(f" yield {x}") def __init__(self, attr, value, guard=None, mode="xml"): super().__init__(value) self.attr = attr self.guard = guard self.mode = mode self.genname = gen_name() def py(self): yield self.line(f"def {self.genname}():") def __iter__(self): if self.guard: new_body = IfNode(self.guard, AttrNode(self.attr, value=self.body, mode=self.mode)) yield from new_body else: yield self yield IndentNode() if self.body: yield from self.body_iter() else: yield TextNode("") yield DedentNode() yield self.AttrTail(self) class AttrsNode(Node): def __init__(self, attrs, guard=None, mode="xml"): super().__init__() self.attrs = attrs self.guard = guard self.mode = mode def py(self): x = gen_name() def _body(): yield self.line(f"for {x} in self.__kj__.render_attrs({self.attrs}, {self.mode!r}):") yield self.line(f" yield {x}") if self.guard: yield self.line(f"if {self.guard}:") for line in _body(): yield line.indent() else: for line in _body(): yield line class PythonNode(Node): def __init__(self, *body): super().__init__() self.module_level = False blocks = [] for b in body: assert isinstance(b, TextNode) # noqa: S101 blocks.append(b.text) text = "".join(blocks) if text[0] == "%": self.module_level = True text = text[1:] self.lines = list(self._normalize(text)) def py(self): for line in self.lines: yield self.line(line) def _normalize(self, text): if text.startswith("#\n"): text = text[2:] prefix = None for line in text.splitlines(): if prefix is None: rest = line.lstrip() prefix = line[: len(line) - len(rest)] assert line.startswith(prefix) # noqa: S101 yield line[len(prefix) :] def optimize(iter_node): last_node = None for node in iter_node: if type(node) == TextNode and type(last_node) == TextNode and last_node.guard == node.guard: last_node.text += node.text # Erase this node by not yielding it. continue if last_node is not None: yield last_node last_node = node if last_node is not None: yield last_node class PyLine: def __init__(self, filename, lineno, text, indent=0): self._filename = filename self._lineno = lineno self._text = text self._indent = indent def indent(self, sz=4): return PyLine(self._filename, self._lineno, self._text, self._indent + sz) def __str__(self): return (" " * self._indent) + self._text if self._lineno: return (" " * self._indent) + self._text + "\t# %s:%d" % (self._filename, self._lineno) return (" " * self._indent) + self._text def __repr__(self): return f"{self._filename}:{self._lineno} {self}"