1import sys 2from types import CodeType 3 4from . import TemplateSyntaxError 5from ._compat import PYPY 6from .utils import internal_code 7from .utils import missing 8 9 10def rewrite_traceback_stack(source=None): 11 """Rewrite the current exception to replace any tracebacks from 12 within compiled template code with tracebacks that look like they 13 came from the template source. 14 15 This must be called within an ``except`` block. 16 17 :param exc_info: A :meth:`sys.exc_info` tuple. If not provided, 18 the current ``exc_info`` is used. 19 :param source: For ``TemplateSyntaxError``, the original source if 20 known. 21 :return: A :meth:`sys.exc_info` tuple that can be re-raised. 22 """ 23 exc_type, exc_value, tb = sys.exc_info() 24 25 if isinstance(exc_value, TemplateSyntaxError) and not exc_value.translated: 26 exc_value.translated = True 27 exc_value.source = source 28 29 try: 30 # Remove the old traceback on Python 3, otherwise the frames 31 # from the compiler still show up. 32 exc_value.with_traceback(None) 33 except AttributeError: 34 pass 35 36 # Outside of runtime, so the frame isn't executing template 37 # code, but it still needs to point at the template. 38 tb = fake_traceback( 39 exc_value, None, exc_value.filename or "<unknown>", exc_value.lineno 40 ) 41 else: 42 # Skip the frame for the render function. 43 tb = tb.tb_next 44 45 stack = [] 46 47 # Build the stack of traceback object, replacing any in template 48 # code with the source file and line information. 49 while tb is not None: 50 # Skip frames decorated with @internalcode. These are internal 51 # calls that aren't useful in template debugging output. 52 if tb.tb_frame.f_code in internal_code: 53 tb = tb.tb_next 54 continue 55 56 template = tb.tb_frame.f_globals.get("__jinja_template__") 57 58 if template is not None: 59 lineno = template.get_corresponding_lineno(tb.tb_lineno) 60 fake_tb = fake_traceback(exc_value, tb, template.filename, lineno) 61 stack.append(fake_tb) 62 else: 63 stack.append(tb) 64 65 tb = tb.tb_next 66 67 tb_next = None 68 69 # Assign tb_next in reverse to avoid circular references. 70 for tb in reversed(stack): 71 tb_next = tb_set_next(tb, tb_next) 72 73 return exc_type, exc_value, tb_next 74 75 76def fake_traceback(exc_value, tb, filename, lineno): 77 """Produce a new traceback object that looks like it came from the 78 template source instead of the compiled code. The filename, line 79 number, and location name will point to the template, and the local 80 variables will be the current template context. 81 82 :param exc_value: The original exception to be re-raised to create 83 the new traceback. 84 :param tb: The original traceback to get the local variables and 85 code info from. 86 :param filename: The template filename. 87 :param lineno: The line number in the template source. 88 """ 89 if tb is not None: 90 # Replace the real locals with the context that would be 91 # available at that point in the template. 92 locals = get_template_locals(tb.tb_frame.f_locals) 93 locals.pop("__jinja_exception__", None) 94 else: 95 locals = {} 96 97 globals = { 98 "__name__": filename, 99 "__file__": filename, 100 "__jinja_exception__": exc_value, 101 } 102 # Raise an exception at the correct line number. 103 code = compile("\n" * (lineno - 1) + "raise __jinja_exception__", filename, "exec") 104 105 # Build a new code object that points to the template file and 106 # replaces the location with a block name. 107 try: 108 location = "template" 109 110 if tb is not None: 111 function = tb.tb_frame.f_code.co_name 112 113 if function == "root": 114 location = "top-level template code" 115 elif function.startswith("block_"): 116 location = 'block "%s"' % function[6:] 117 118 # Collect arguments for the new code object. CodeType only 119 # accepts positional arguments, and arguments were inserted in 120 # new Python versions. 121 code_args = [] 122 123 for attr in ( 124 "argcount", 125 "posonlyargcount", # Python 3.8 126 "kwonlyargcount", # Python 3 127 "nlocals", 128 "stacksize", 129 "flags", 130 "code", # codestring 131 "consts", # constants 132 "names", 133 "varnames", 134 ("filename", filename), 135 ("name", location), 136 "firstlineno", 137 "lnotab", 138 "freevars", 139 "cellvars", 140 ): 141 if isinstance(attr, tuple): 142 # Replace with given value. 143 code_args.append(attr[1]) 144 continue 145 146 try: 147 # Copy original value if it exists. 148 code_args.append(getattr(code, "co_" + attr)) 149 except AttributeError: 150 # Some arguments were added later. 151 continue 152 153 code = CodeType(*code_args) 154 except Exception: 155 # Some environments such as Google App Engine don't support 156 # modifying code objects. 157 pass 158 159 # Execute the new code, which is guaranteed to raise, and return 160 # the new traceback without this frame. 161 try: 162 exec(code, globals, locals) 163 except BaseException: 164 return sys.exc_info()[2].tb_next 165 166 167def get_template_locals(real_locals): 168 """Based on the runtime locals, get the context that would be 169 available at that point in the template. 170 """ 171 # Start with the current template context. 172 ctx = real_locals.get("context") 173 174 if ctx: 175 data = ctx.get_all().copy() 176 else: 177 data = {} 178 179 # Might be in a derived context that only sets local variables 180 # rather than pushing a context. Local variables follow the scheme 181 # l_depth_name. Find the highest-depth local that has a value for 182 # each name. 183 local_overrides = {} 184 185 for name, value in real_locals.items(): 186 if not name.startswith("l_") or value is missing: 187 # Not a template variable, or no longer relevant. 188 continue 189 190 try: 191 _, depth, name = name.split("_", 2) 192 depth = int(depth) 193 except ValueError: 194 continue 195 196 cur_depth = local_overrides.get(name, (-1,))[0] 197 198 if cur_depth < depth: 199 local_overrides[name] = (depth, value) 200 201 # Modify the context with any derived context. 202 for name, (_, value) in local_overrides.items(): 203 if value is missing: 204 data.pop(name, None) 205 else: 206 data[name] = value 207 208 return data 209 210 211if sys.version_info >= (3, 7): 212 # tb_next is directly assignable as of Python 3.7 213 def tb_set_next(tb, tb_next): 214 tb.tb_next = tb_next 215 return tb 216 217 218elif PYPY: 219 # PyPy might have special support, and won't work with ctypes. 220 try: 221 import tputil 222 except ImportError: 223 # Without tproxy support, use the original traceback. 224 def tb_set_next(tb, tb_next): 225 return tb 226 227 else: 228 # With tproxy support, create a proxy around the traceback that 229 # returns the new tb_next. 230 def tb_set_next(tb, tb_next): 231 def controller(op): 232 if op.opname == "__getattribute__" and op.args[0] == "tb_next": 233 return tb_next 234 235 return op.delegate() 236 237 return tputil.make_proxy(controller, obj=tb) 238 239 240else: 241 # Use ctypes to assign tb_next at the C level since it's read-only 242 # from Python. 243 import ctypes 244 245 class _CTraceback(ctypes.Structure): 246 _fields_ = [ 247 # Extra PyObject slots when compiled with Py_TRACE_REFS. 248 ("PyObject_HEAD", ctypes.c_byte * object().__sizeof__()), 249 # Only care about tb_next as an object, not a traceback. 250 ("tb_next", ctypes.py_object), 251 ] 252 253 def tb_set_next(tb, tb_next): 254 c_tb = _CTraceback.from_address(id(tb)) 255 256 # Clear out the old tb_next. 257 if tb.tb_next is not None: 258 c_tb_next = ctypes.py_object(tb.tb_next) 259 c_tb.tb_next = ctypes.py_object() 260 ctypes.pythonapi.Py_DecRef(c_tb_next) 261 262 # Assign the new tb_next. 263 if tb_next is not None: 264 c_tb_next = ctypes.py_object(tb_next) 265 ctypes.pythonapi.Py_IncRef(c_tb_next) 266 c_tb.tb_next = c_tb_next 267 268 return tb 269