codekingpro/portable-devtools
115k
1import sys2import typing as t3from types import CodeType4from types import TracebackType5 6from .exceptions import TemplateSyntaxError7from .utils import internal_code8from .utils import missing9 10if t.TYPE_CHECKING:11 from .runtime import Context12 13 14def rewrite_traceback_stack(source: t.Optional[str] = None) -> BaseException:15 """Rewrite the current exception to replace any tracebacks from16 within compiled template code with tracebacks that look like they17 came from the template source.18 19 This must be called within an ``except`` block.20 21 :param source: For ``TemplateSyntaxError``, the original source if22 known.23 :return: The original exception with the rewritten traceback.24 """25 _, exc_value, tb = sys.exc_info()26 exc_value = t.cast(BaseException, exc_value)27 tb = t.cast(TracebackType, tb)28 29 if isinstance(exc_value, TemplateSyntaxError) and not exc_value.translated:30 exc_value.translated = True31 exc_value.source = source32 # Remove the old traceback, otherwise the frames from the33 # compiler still show up.34 exc_value.with_traceback(None)35 # Outside of runtime, so the frame isn't executing template36 # code, but it still needs to point at the template.37 tb = fake_traceback(38 exc_value, None, exc_value.filename or "<unknown>", exc_value.lineno39 )40 else:41 # Skip the frame for the render function.42 tb = tb.tb_next43 44 stack = []45 46 # Build the stack of traceback object, replacing any in template47 # code with the source file and line information.48 while tb is not None:49 # Skip frames decorated with @internalcode. These are internal50 # calls that aren't useful in template debugging output.51 if tb.tb_frame.f_code in internal_code:52 tb = tb.tb_next53 continue54 55 template = tb.tb_frame.f_globals.get("__jinja_template__")56 57 if template is not None:58 lineno = template.get_corresponding_lineno(tb.tb_lineno)59 fake_tb = fake_traceback(exc_value, tb, template.filename, lineno)60 stack.append(fake_tb)61 else:62 stack.append(tb)63 64 tb = tb.tb_next65 66 tb_next = None67 68 # Assign tb_next in reverse to avoid circular references.69 for tb in reversed(stack):70 tb.tb_next = tb_next71 tb_next = tb72 73 return exc_value.with_traceback(tb_next)74 75 76def fake_traceback( # type: ignore77 exc_value: BaseException, tb: t.Optional[TracebackType], filename: str, lineno: int78) -> TracebackType:79 """Produce a new traceback object that looks like it came from the80 template source instead of the compiled code. The filename, line81 number, and location name will point to the template, and the local82 variables will be the current template context.83 84 :param exc_value: The original exception to be re-raised to create85 the new traceback.86 :param tb: The original traceback to get the local variables and87 code info from.88 :param filename: The template filename.89 :param lineno: The line number in the template source.90 """91 if tb is not None:92 # Replace the real locals with the context that would be93 # available at that point in the template.94 locals = get_template_locals(tb.tb_frame.f_locals)95 locals.pop("__jinja_exception__", None)96 else:97 locals = {}98 99 globals = {100 "__name__": filename,101 "__file__": filename,102 "__jinja_exception__": exc_value,103 }104 # Raise an exception at the correct line number.105 code: CodeType = compile(106 "\n" * (lineno - 1) + "raise __jinja_exception__", filename, "exec"107 )108 109 # Build a new code object that points to the template file and110 # replaces the location with a block name.111 location = "template"112 113 if tb is not None:114 function = tb.tb_frame.f_code.co_name115 116 if function == "root":117 location = "top-level template code"118 elif function.startswith("block_"):119 location = f"block {function[6:]!r}"120 121 if sys.version_info >= (3, 8):122 code = code.replace(co_name=location)123 else:124 code = CodeType(125 code.co_argcount,126 code.co_kwonlyargcount,127 code.co_nlocals,128 code.co_stacksize,129 code.co_flags,130 code.co_code,131 code.co_consts,132 code.co_names,133 code.co_varnames,134 code.co_filename,135 location,136 code.co_firstlineno,137 code.co_lnotab,138 code.co_freevars,139 code.co_cellvars,140 )141 142 # Execute the new code, which is guaranteed to raise, and return143 # the new traceback without this frame.144 try:145 exec(code, globals, locals)146 except BaseException:147 return sys.exc_info()[2].tb_next # type: ignore148 149 150def get_template_locals(real_locals: t.Mapping[str, t.Any]) -> t.Dict[str, t.Any]:151 """Based on the runtime locals, get the context that would be152 available at that point in the template.153 """154 # Start with the current template context.155 ctx: t.Optional[Context] = real_locals.get("context")156 157 if ctx is not None:158 data: t.Dict[str, t.Any] = ctx.get_all().copy()159 else:160 data = {}161 162 # Might be in a derived context that only sets local variables163 # rather than pushing a context. Local variables follow the scheme164 # l_depth_name. Find the highest-depth local that has a value for165 # each name.166 local_overrides: t.Dict[str, t.Tuple[int, t.Any]] = {}167 168 for name, value in real_locals.items():169 if not name.startswith("l_") or value is missing:170 # Not a template variable, or no longer relevant.171 continue172 173 try:174 _, depth_str, name = name.split("_", 2)175 depth = int(depth_str)176 except ValueError:177 continue178 179 cur_depth = local_overrides.get(name, (-1,))[0]180 181 if cur_depth < depth:182 local_overrides[name] = (depth, value)183 184 # Modify the context with any derived context.185 for name, (_, value) in local_overrides.items():186 if value is missing:187 data.pop(name, None)188 else:189 data[name] = value190 191 return data192 