jinja2.debug

  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
def rewrite_traceback_stack(source: Optional[str] = None) -> BaseException:
15def rewrite_traceback_stack(source: t.Optional[str] = None) -> BaseException:
16    """Rewrite the current exception to replace any tracebacks from
17    within compiled template code with tracebacks that look like they
18    came from the template source.
19
20    This must be called within an ``except`` block.
21
22    :param source: For ``TemplateSyntaxError``, the original source if
23        known.
24    :return: The original exception with the rewritten traceback.
25    """
26    _, exc_value, tb = sys.exc_info()
27    exc_value = t.cast(BaseException, exc_value)
28    tb = t.cast(TracebackType, tb)
29
30    if isinstance(exc_value, TemplateSyntaxError) and not exc_value.translated:
31        exc_value.translated = True
32        exc_value.source = source
33        # Remove the old traceback, otherwise the frames from the
34        # compiler still show up.
35        exc_value.with_traceback(None)
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.tb_next = tb_next
72        tb_next = tb
73
74    return exc_value.with_traceback(tb_next)

Rewrite the current exception to replace any tracebacks from within compiled template code with tracebacks that look like they came from the template source.

This must be called within an except block.

Parameters
  • source: For TemplateSyntaxError, the original source if known.
Returns

The original exception with the rewritten traceback.

def fake_traceback( exc_value: BaseException, tb: Optional[traceback], filename: str, lineno: int) -> traceback:
 77def fake_traceback(  # type: ignore
 78    exc_value: BaseException, tb: t.Optional[TracebackType], filename: str, lineno: int
 79) -> TracebackType:
 80    """Produce a new traceback object that looks like it came from the
 81    template source instead of the compiled code. The filename, line
 82    number, and location name will point to the template, and the local
 83    variables will be the current template context.
 84
 85    :param exc_value: The original exception to be re-raised to create
 86        the new traceback.
 87    :param tb: The original traceback to get the local variables and
 88        code info from.
 89    :param filename: The template filename.
 90    :param lineno: The line number in the template source.
 91    """
 92    if tb is not None:
 93        # Replace the real locals with the context that would be
 94        # available at that point in the template.
 95        locals = get_template_locals(tb.tb_frame.f_locals)
 96        locals.pop("__jinja_exception__", None)
 97    else:
 98        locals = {}
 99
100    globals = {
101        "__name__": filename,
102        "__file__": filename,
103        "__jinja_exception__": exc_value,
104    }
105    # Raise an exception at the correct line number.
106    code: CodeType = compile(
107        "\n" * (lineno - 1) + "raise __jinja_exception__", filename, "exec"
108    )
109
110    # Build a new code object that points to the template file and
111    # replaces the location with a block name.
112    location = "template"
113
114    if tb is not None:
115        function = tb.tb_frame.f_code.co_name
116
117        if function == "root":
118            location = "top-level template code"
119        elif function.startswith("block_"):
120            location = f"block {function[6:]!r}"
121
122    if sys.version_info >= (3, 8):
123        code = code.replace(co_name=location)
124    else:
125        code = CodeType(
126            code.co_argcount,
127            code.co_kwonlyargcount,
128            code.co_nlocals,
129            code.co_stacksize,
130            code.co_flags,
131            code.co_code,
132            code.co_consts,
133            code.co_names,
134            code.co_varnames,
135            code.co_filename,
136            location,
137            code.co_firstlineno,
138            code.co_lnotab,
139            code.co_freevars,
140            code.co_cellvars,
141        )
142
143    # Execute the new code, which is guaranteed to raise, and return
144    # the new traceback without this frame.
145    try:
146        exec(code, globals, locals)
147    except BaseException:
148        return sys.exc_info()[2].tb_next  # type: ignore

Produce a new traceback object that looks like it came from the template source instead of the compiled code. The filename, line number, and location name will point to the template, and the local variables will be the current template context.

Parameters
  • exc_value: The original exception to be re-raised to create the new traceback.
  • tb: The original traceback to get the local variables and code info from.
  • filename: The template filename.
  • lineno: The line number in the template source.
def get_template_locals(real_locals: Mapping[str, Any]) -> Dict[str, Any]:
151def get_template_locals(real_locals: t.Mapping[str, t.Any]) -> t.Dict[str, t.Any]:
152    """Based on the runtime locals, get the context that would be
153    available at that point in the template.
154    """
155    # Start with the current template context.
156    ctx: "t.Optional[Context]" = real_locals.get("context")
157
158    if ctx is not None:
159        data: t.Dict[str, t.Any] = ctx.get_all().copy()
160    else:
161        data = {}
162
163    # Might be in a derived context that only sets local variables
164    # rather than pushing a context. Local variables follow the scheme
165    # l_depth_name. Find the highest-depth local that has a value for
166    # each name.
167    local_overrides: t.Dict[str, t.Tuple[int, t.Any]] = {}
168
169    for name, value in real_locals.items():
170        if not name.startswith("l_") or value is missing:
171            # Not a template variable, or no longer relevant.
172            continue
173
174        try:
175            _, depth_str, name = name.split("_", 2)
176            depth = int(depth_str)
177        except ValueError:
178            continue
179
180        cur_depth = local_overrides.get(name, (-1,))[0]
181
182        if cur_depth < depth:
183            local_overrides[name] = (depth, value)
184
185    # Modify the context with any derived context.
186    for name, (_, value) in local_overrides.items():
187        if value is missing:
188            data.pop(name, None)
189        else:
190            data[name] = value
191
192    return data

Based on the runtime locals, get the context that would be available at that point in the template.