blob: f3a2fcd772d0b4d2b0a3c2c9a3242df77b3f63d2 [file] [log] [blame]
# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import sys
import mako.runtime
import mako.template
import mako.util
_MAKO_TEMPLATE_PASS_KEY = object()
class MakoTemplate(object):
"""Represents a compiled template object."""
_mako_template_cache = {}
def __init__(self, template_text):
assert isinstance(template_text, str)
template_params = {
"strict_undefined": True,
}
template = self._mako_template_cache.get(template_text)
if template is None:
template = mako.template.Template(
text=template_text, **template_params)
self._mako_template_cache[template_text] = template
self._template = template
def mako_template(self, pass_key=None):
assert pass_key is _MAKO_TEMPLATE_PASS_KEY
return self._template
class MakoRenderer(object):
"""Represents a renderer object implemented with Mako templates."""
def __init__(self):
self._text_buffer = None
self._is_invalidated = False
self._caller_stack = []
self._caller_stack_on_error = []
def reset(self):
"""
Resets the rendering states of this object. Must be called before
the first call to |render| or |render_text|.
"""
self._text_buffer = mako.util.FastEncodingBuffer()
self._is_invalidated = False
def is_rendering_complete(self):
return not (self._is_invalidated or self._text_buffer is None
or self._caller_stack)
def invalidate_rendering_result(self):
self._is_invalidated = True
def to_text(self):
"""Returns the rendering result."""
assert self._text_buffer is not None
return self._text_buffer.getvalue()
def render(self, caller, template, template_vars):
"""
Renders the template with variable bindings.
It's okay to invoke |render| method recursively and |caller| is pushed
onto the call stack, which is accessible via
|callers_from_first_to_last| method, etc.
Args:
caller: An object to be pushed onto the call stack.
template: A MakoTemplate.
template_vars: A dict of template variable bindings.
"""
assert caller is not None
assert isinstance(template, MakoTemplate)
assert isinstance(template_vars, dict)
self._caller_stack.append(caller)
try:
mako_template = template.mako_template(
pass_key=_MAKO_TEMPLATE_PASS_KEY)
mako_context = mako.runtime.Context(self._text_buffer,
**template_vars)
mako_template.render_context(mako_context)
except:
# Print stacktrace of template rendering.
sys.stderr.write("\n")
sys.stderr.write("==== template rendering error ====\n")
sys.stderr.write(" * name: {}, type: {}\n".format(
_guess_caller_name(self.last_caller), type(self.last_caller)))
sys.stderr.write(" * depth: {}, module_id: {}\n".format(
len(self._caller_stack), mako_template.module_id))
sys.stderr.write("---- template source ----\n")
sys.stderr.write(mako_template.source)
# Save the error state at the deepest call.
current = self._caller_stack
on_error = self._caller_stack_on_error
if (len(current) <= len(on_error)
and all(current[i] == on_error[i]
for i in range(len(current)))):
pass # Error happened in a deeper caller.
else:
self._caller_stack_on_error = list(self._caller_stack)
raise
finally:
self._caller_stack.pop()
def render_text(self, text):
"""Renders a plain text as is."""
assert isinstance(text, str)
self._text_buffer.write(text)
def push_caller(self, caller):
self._caller_stack.append(caller)
def pop_caller(self):
self._caller_stack.pop()
@property
def callers_from_first_to_last(self):
"""
Returns the callers of this renderer in the order from the first caller
to the last caller.
"""
return iter(self._caller_stack)
@property
def callers_from_last_to_first(self):
"""
Returns the callers of this renderer in the order from the last caller
to the first caller.
"""
return reversed(self._caller_stack)
@property
def last_caller(self):
"""Returns the last caller in the call stack of this renderer."""
return self._caller_stack[-1]
@property
def callers_on_error(self):
"""
Returns the callers of this renderer in the order from the last caller
to the first caller at the moment when an exception was thrown.
"""
return reversed(self._caller_stack_on_error)
@property
def last_caller_on_error(self):
"""
Returns the deepest caller at the moment when an exception was thrown.
"""
return self._caller_stack_on_error[-1]
def _guess_caller_name(caller):
"""Returns the best-guessed name of |caller|."""
try:
# Outer CodeNode may have a binding to the caller.
for name, value in caller.outer.template_vars.items():
if value is caller:
return name
try:
# Outer ListNode may contain the caller.
for index, value in enumerate(caller.outer, 1):
if value is caller:
return "{}-of-{}-in-list".format(index, len(caller.outer))
except:
pass
return "<no name>"
except:
return "<unknown>"