1# Copyright 2019 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import sys 6 7import mako.runtime 8import mako.template 9import mako.util 10 11_MAKO_TEMPLATE_PASS_KEY = object() 12 13 14class MakoTemplate(object): 15 """Represents a compiled template object.""" 16 17 _mako_template_cache = {} 18 19 def __init__(self, template_text): 20 assert isinstance(template_text, str) 21 22 template_params = { 23 "strict_undefined": True, 24 } 25 26 template = self._mako_template_cache.get(template_text) 27 if template is None: 28 template = mako.template.Template( 29 text=template_text, **template_params) 30 self._mako_template_cache[template_text] = template 31 self._template = template 32 33 def mako_template(self, pass_key=None): 34 assert pass_key is _MAKO_TEMPLATE_PASS_KEY 35 return self._template 36 37 38class MakoRenderer(object): 39 """Represents a renderer object implemented with Mako templates.""" 40 41 def __init__(self): 42 self._text_buffer = None 43 self._is_invalidated = False 44 self._caller_stack = [] 45 self._caller_stack_on_error = [] 46 47 def reset(self): 48 """ 49 Resets the rendering states of this object. Must be called before 50 the first call to |render| or |render_text|. 51 """ 52 self._text_buffer = mako.util.FastEncodingBuffer() 53 self._is_invalidated = False 54 55 def is_rendering_complete(self): 56 return not (self._is_invalidated or self._text_buffer is None 57 or self._caller_stack) 58 59 def invalidate_rendering_result(self): 60 self._is_invalidated = True 61 62 def to_text(self): 63 """Returns the rendering result.""" 64 assert self._text_buffer is not None 65 return self._text_buffer.getvalue() 66 67 def render(self, caller, template, template_vars): 68 """ 69 Renders the template with variable bindings. 70 71 It's okay to invoke |render| method recursively and |caller| is pushed 72 onto the call stack, which is accessible via 73 |callers_from_first_to_last| method, etc. 74 75 Args: 76 caller: An object to be pushed onto the call stack. 77 template: A MakoTemplate. 78 template_vars: A dict of template variable bindings. 79 """ 80 assert caller is not None 81 assert isinstance(template, MakoTemplate) 82 assert isinstance(template_vars, dict) 83 84 self._caller_stack.append(caller) 85 86 try: 87 mako_template = template.mako_template( 88 pass_key=_MAKO_TEMPLATE_PASS_KEY) 89 mako_context = mako.runtime.Context(self._text_buffer, 90 **template_vars) 91 mako_template.render_context(mako_context) 92 except: 93 # Print stacktrace of template rendering. 94 sys.stderr.write("\n") 95 sys.stderr.write("==== template rendering error ====\n") 96 sys.stderr.write(" * name: {}, type: {}\n".format( 97 _guess_caller_name(self.last_caller), type(self.last_caller))) 98 sys.stderr.write(" * depth: {}, module_id: {}\n".format( 99 len(self._caller_stack), mako_template.module_id)) 100 sys.stderr.write("---- template source ----\n") 101 sys.stderr.write(mako_template.source) 102 103 # Save the error state at the deepest call. 104 current = self._caller_stack 105 on_error = self._caller_stack_on_error 106 if (len(current) <= len(on_error) 107 and all(current[i] == on_error[i] 108 for i in xrange(len(current)))): 109 pass # Error happened in a deeper caller. 110 else: 111 self._caller_stack_on_error = list(self._caller_stack) 112 113 raise 114 finally: 115 self._caller_stack.pop() 116 117 def render_text(self, text): 118 """Renders a plain text as is.""" 119 assert isinstance(text, str) 120 self._text_buffer.write(text) 121 122 def push_caller(self, caller): 123 self._caller_stack.append(caller) 124 125 def pop_caller(self): 126 self._caller_stack.pop() 127 128 @property 129 def callers_from_first_to_last(self): 130 """ 131 Returns the callers of this renderer in the order from the first caller 132 to the last caller. 133 """ 134 return iter(self._caller_stack) 135 136 @property 137 def callers_from_last_to_first(self): 138 """ 139 Returns the callers of this renderer in the order from the last caller 140 to the first caller. 141 """ 142 return reversed(self._caller_stack) 143 144 @property 145 def last_caller(self): 146 """Returns the last caller in the call stack of this renderer.""" 147 return self._caller_stack[-1] 148 149 @property 150 def callers_on_error(self): 151 """ 152 Returns the callers of this renderer in the order from the last caller 153 to the first caller at the moment when an exception was thrown. 154 """ 155 return reversed(self._caller_stack_on_error) 156 157 @property 158 def last_caller_on_error(self): 159 """ 160 Returns the deepest caller at the moment when an exception was thrown. 161 """ 162 return self._caller_stack_on_error[-1] 163 164 165def _guess_caller_name(caller): 166 """Returns the best-guessed name of |caller|.""" 167 try: 168 # Outer CodeNode may have a binding to the caller. 169 for name, value in caller.outer.template_vars.items(): 170 if value is caller: 171 return name 172 try: 173 # Outer ListNode may contain the caller. 174 for index, value in enumerate(caller.outer, 1): 175 if value is caller: 176 return "{}-of-{}-in-list".format(index, len(caller.outer)) 177 except: 178 pass 179 return "<no name>" 180 except: 181 return "<unknown>" 182