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