• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import sys
2import typing as t
3from types import CodeType
4from types import TracebackType
5
6from .exceptions import TemplateSyntaxError
7from .utils import internal_code
8from .utils import missing
9
10if t.TYPE_CHECKING:
11    from .runtime import Context
12
13
14def rewrite_traceback_stack(source: t.Optional[str] = None) -> BaseException:
15    """Rewrite the current exception to replace any tracebacks from
16    within compiled template code with tracebacks that look like they
17    came from the template source.
18
19    This must be called within an ``except`` block.
20
21    :param source: For ``TemplateSyntaxError``, the original source if
22        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 = True
31        exc_value.source = source
32        # Remove the old traceback, otherwise the frames from the
33        # compiler still show up.
34        exc_value.with_traceback(None)
35        # Outside of runtime, so the frame isn't executing template
36        # 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.lineno
39        )
40    else:
41        # Skip the frame for the render function.
42        tb = tb.tb_next
43
44    stack = []
45
46    # Build the stack of traceback object, replacing any in template
47    # code with the source file and line information.
48    while tb is not None:
49        # Skip frames decorated with @internalcode. These are internal
50        # calls that aren't useful in template debugging output.
51        if tb.tb_frame.f_code in internal_code:
52            tb = tb.tb_next
53            continue
54
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_next
65
66    tb_next = None
67
68    # Assign tb_next in reverse to avoid circular references.
69    for tb in reversed(stack):
70        tb.tb_next = tb_next
71        tb_next = tb
72
73    return exc_value.with_traceback(tb_next)
74
75
76def fake_traceback(  # type: ignore
77    exc_value: BaseException, tb: t.Optional[TracebackType], filename: str, lineno: int
78) -> TracebackType:
79    """Produce a new traceback object that looks like it came from the
80    template source instead of the compiled code. The filename, line
81    number, and location name will point to the template, and the local
82    variables will be the current template context.
83
84    :param exc_value: The original exception to be re-raised to create
85        the new traceback.
86    :param tb: The original traceback to get the local variables and
87        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 be
93        # 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 and
110    # 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_name
115
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 return
143    # 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: ignore
148
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 be
152    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 variables
163    # rather than pushing a context. Local variables follow the scheme
164    # l_depth_name. Find the highest-depth local that has a value for
165    # 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            continue
172
173        try:
174            _, depth_str, name = name.split("_", 2)
175            depth = int(depth_str)
176        except ValueError:
177            continue
178
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] = value
190
191    return data
192