1from _pydevd_bundle.pydevd_breakpoints import LineBreakpoint
2from _pydevd_bundle.pydevd_constants import STATE_SUSPEND, dict_iter_items, dict_keys, JINJA2_SUSPEND, \
3    IS_PY2
4from _pydevd_bundle.pydevd_comm import CMD_SET_BREAK, CMD_ADD_EXCEPTION_BREAK
5from pydevd_file_utils import canonical_normalized_path
6from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, FCode
7from _pydev_bundle import pydev_log
8
9
10class Jinja2LineBreakpoint(LineBreakpoint):
11
12    def __init__(self, canonical_normalized_filename, line, condition, func_name, expression, hit_condition=None, is_logpoint=False):
13        self.canonical_normalized_filename = canonical_normalized_filename
14        LineBreakpoint.__init__(self, line, condition, func_name, expression, hit_condition=hit_condition, is_logpoint=is_logpoint)
15
16    def __str__(self):
17        return "Jinja2LineBreakpoint: %s-%d" % (self.canonical_normalized_filename, self.line)
18
19
20def add_line_breakpoint(plugin, pydb, type, canonical_normalized_filename, line, condition, expression, func_name, hit_condition=None, is_logpoint=False):
21    if type == 'jinja2-line':
22        jinja2_line_breakpoint = Jinja2LineBreakpoint(canonical_normalized_filename, line, condition, func_name, expression, hit_condition=hit_condition, is_logpoint=is_logpoint)
23        if not hasattr(pydb, 'jinja2_breakpoints'):
24            _init_plugin_breaks(pydb)
25        return jinja2_line_breakpoint, pydb.jinja2_breakpoints
26    return None
27
28
29def add_exception_breakpoint(plugin, pydb, type, exception):
30    if type == 'jinja2':
31        if not hasattr(pydb, 'jinja2_exception_break'):
32            _init_plugin_breaks(pydb)
33        pydb.jinja2_exception_break[exception] = True
34        return True
35    return False
36
37
38def _init_plugin_breaks(pydb):
39    pydb.jinja2_exception_break = {}
40    pydb.jinja2_breakpoints = {}
41
42
43def remove_all_exception_breakpoints(plugin, pydb):
44    if hasattr(pydb, 'jinja2_exception_break'):
45        pydb.jinja2_exception_break = {}
46        return True
47    return False
48
49
50def remove_exception_breakpoint(plugin, pydb, type, exception):
51    if type == 'jinja2':
52        try:
53            del pydb.jinja2_exception_break[exception]
54            return True
55        except:
56            pass
57    return False
58
59
60def get_breakpoints(plugin, pydb, type):
61    if type == 'jinja2-line':
62        return pydb.jinja2_breakpoints
63    return None
64
65
66def _is_jinja2_render_call(frame):
67    try:
68        name = frame.f_code.co_name
69        if "__jinja_template__" in frame.f_globals and name in ("root", "loop", "macro") or name.startswith("block_"):
70            return True
71        return False
72    except:
73        pydev_log.exception()
74        return False
75
76
77def _suspend_jinja2(pydb, thread, frame, cmd=CMD_SET_BREAK, message=None):
78    frame = Jinja2TemplateFrame(frame)
79
80    if frame.f_lineno is None:
81        return None
82
83    pydb.set_suspend(thread, cmd)
84
85    thread.additional_info.suspend_type = JINJA2_SUSPEND
86    if cmd == CMD_ADD_EXCEPTION_BREAK:
87        # send exception name as message
88        if message:
89            message = str(message)
90        thread.additional_info.pydev_message = message
91
92    return frame
93
94
95def _is_jinja2_suspended(thread):
96    return thread.additional_info.suspend_type == JINJA2_SUSPEND
97
98
99def _is_jinja2_context_call(frame):
100    return "_Context__obj" in frame.f_locals
101
102
103def _is_jinja2_internal_function(frame):
104    return 'self' in frame.f_locals and frame.f_locals['self'].__class__.__name__ in \
105        ('LoopContext', 'TemplateReference', 'Macro', 'BlockReference')
106
107
108def _find_jinja2_render_frame(frame):
109    while frame is not None and not _is_jinja2_render_call(frame):
110        frame = frame.f_back
111
112    return frame
113
114#=======================================================================================================================
115# Jinja2 Frame
116#=======================================================================================================================
117
118
119class Jinja2TemplateFrame(object):
120
121    IS_PLUGIN_FRAME = True
122
123    def __init__(self, frame, original_filename=None, template_lineno=None):
124
125        if original_filename is None:
126            original_filename = _get_jinja2_template_original_filename(frame)
127
128        if template_lineno is None:
129            template_lineno = _get_jinja2_template_line(frame)
130
131        self.back_context = None
132        if 'context' in frame.f_locals:
133            # sometimes we don't have 'context', e.g. in macros
134            self.back_context = frame.f_locals['context']
135        self.f_code = FCode('template', original_filename)
136        self.f_lineno = template_lineno
137        self.f_back = frame
138        self.f_globals = {}
139        self.f_locals = self.collect_context(frame)
140        self.f_trace = None
141
142    def _get_real_var_name(self, orig_name):
143        # replace leading number for local variables
144        parts = orig_name.split('_')
145        if len(parts) > 1 and parts[0].isdigit():
146            return parts[1]
147        return orig_name
148
149    def collect_context(self, frame):
150        res = {}
151        for k, v in frame.f_locals.items():
152            if not k.startswith('l_'):
153                res[k] = v
154            elif v and not _is_missing(v):
155                res[self._get_real_var_name(k[2:])] = v
156        if self.back_context is not None:
157            for k, v in self.back_context.items():
158                res[k] = v
159        return res
160
161    def _change_variable(self, frame, name, value):
162        in_vars_or_parents = False
163        if 'context' in frame.f_locals:
164            if name in frame.f_locals['context'].parent:
165                self.back_context.parent[name] = value
166                in_vars_or_parents = True
167            if name in frame.f_locals['context'].vars:
168                self.back_context.vars[name] = value
169                in_vars_or_parents = True
170
171        l_name = 'l_' + name
172        if l_name in frame.f_locals:
173            if in_vars_or_parents:
174                frame.f_locals[l_name] = self.back_context.resolve(name)
175            else:
176                frame.f_locals[l_name] = value
177
178
179class Jinja2TemplateSyntaxErrorFrame(object):
180
181    IS_PLUGIN_FRAME = True
182
183    def __init__(self, frame, exception_cls_name, filename, lineno, f_locals):
184        self.f_code = FCode('Jinja2 %s' % (exception_cls_name,), filename)
185        self.f_lineno = lineno
186        self.f_back = frame
187        self.f_globals = {}
188        self.f_locals = f_locals
189        self.f_trace = None
190
191
192def change_variable(plugin, frame, attr, expression):
193    if isinstance(frame, Jinja2TemplateFrame):
194        result = eval(expression, frame.f_globals, frame.f_locals)
195        frame._change_variable(frame.f_back, attr, result)
196        return result
197    return False
198
199
200def _is_missing(item):
201    if item.__class__.__name__ == 'MissingType':
202        return True
203    return False
204
205
206def _find_render_function_frame(frame):
207    # in order to hide internal rendering functions
208    old_frame = frame
209    try:
210        while not ('self' in frame.f_locals and frame.f_locals['self'].__class__.__name__ == 'Template' and \
211                               frame.f_code.co_name == 'render'):
212            frame = frame.f_back
213            if frame is None:
214                return old_frame
215        return frame
216    except:
217        return old_frame
218
219
220def _get_jinja2_template_line(frame):
221    debug_info = None
222    if '__jinja_template__' in frame.f_globals:
223        _debug_info = frame.f_globals['__jinja_template__']._debug_info
224        if _debug_info != '':
225            # sometimes template contains only plain text
226            debug_info = frame.f_globals['__jinja_template__'].debug_info
227
228    if debug_info is None:
229        return None
230
231    lineno = frame.f_lineno
232
233    for pair in debug_info:
234        if pair[1] == lineno:
235            return pair[0]
236
237    return None
238
239
240def _convert_to_str(s):
241    if IS_PY2:
242        if isinstance(s, unicode):
243            s = s.encode('utf-8', 'replace')
244    return s
245
246
247def _get_jinja2_template_original_filename(frame):
248    if '__jinja_template__' in frame.f_globals:
249        return _convert_to_str(frame.f_globals['__jinja_template__'].filename)
250
251    return None
252
253#=======================================================================================================================
254# Jinja2 Step Commands
255#=======================================================================================================================
256
257
258def has_exception_breaks(plugin):
259    if len(plugin.main_debugger.jinja2_exception_break) > 0:
260        return True
261    return False
262
263
264def has_line_breaks(plugin):
265    for _canonical_normalized_filename, breakpoints in dict_iter_items(plugin.main_debugger.jinja2_breakpoints):
266        if len(breakpoints) > 0:
267            return True
268    return False
269
270
271def can_skip(plugin, pydb, frame):
272    if pydb.jinja2_breakpoints and _is_jinja2_render_call(frame):
273        filename = _get_jinja2_template_original_filename(frame)
274        if filename is not None:
275            canonical_normalized_filename = canonical_normalized_path(filename)
276            jinja2_breakpoints_for_file = pydb.jinja2_breakpoints.get(canonical_normalized_filename)
277            if jinja2_breakpoints_for_file:
278                return False
279
280    if pydb.jinja2_exception_break:
281        name = frame.f_code.co_name
282
283        if IS_PY2:
284            if name == 'fail':
285                module_name = frame.f_globals.get('__name__', '')
286                if module_name == 'jinja2.parser':
287                    return False
288        else:
289            # errors in compile time
290            if name in ('template', 'top-level template code', '<module>') or name.startswith('block '):
291                f_back = frame.f_back
292                module_name = ''
293                if f_back is not None:
294                    module_name = f_back.f_globals.get('__name__', '')
295                if module_name.startswith('jinja2.'):
296                    return False
297
298    return True
299
300
301def cmd_step_into(plugin, pydb, frame, event, args, stop_info, stop):
302    info = args[2]
303    thread = args[3]
304    plugin_stop = False
305    stop_info['jinja2_stop'] = False
306    if _is_jinja2_suspended(thread):
307        stop_info['jinja2_stop'] = event in ('call', 'line') and _is_jinja2_render_call(frame)
308        plugin_stop = stop_info['jinja2_stop']
309        stop = False
310        if info.pydev_call_from_jinja2 is not None:
311            if _is_jinja2_internal_function(frame):
312                # if internal Jinja2 function was called, we sould continue debugging inside template
313                info.pydev_call_from_jinja2 = None
314            else:
315                # we go into python code from Jinja2 rendering frame
316                stop = True
317
318        if event == 'call' and _is_jinja2_context_call(frame.f_back):
319            # we called function from context, the next step will be in function
320            info.pydev_call_from_jinja2 = 1
321
322    if event == 'return' and _is_jinja2_context_call(frame.f_back):
323        # we return from python code to Jinja2 rendering frame
324        info.pydev_step_stop = info.pydev_call_from_jinja2
325        info.pydev_call_from_jinja2 = None
326        thread.additional_info.suspend_type = JINJA2_SUSPEND
327        stop = False
328
329        # print "info.pydev_call_from_jinja2", info.pydev_call_from_jinja2, "stop_info", stop_info, \
330        #    "thread.additional_info.suspend_type", thread.additional_info.suspend_type
331        # print "event", event, "farme.locals", frame.f_locals
332    return stop, plugin_stop
333
334
335def cmd_step_over(plugin, pydb, frame, event, args, stop_info, stop):
336    info = args[2]
337    thread = args[3]
338    plugin_stop = False
339    stop_info['jinja2_stop'] = False
340    if _is_jinja2_suspended(thread):
341        stop = False
342
343        if info.pydev_call_inside_jinja2 is None:
344            if _is_jinja2_render_call(frame):
345                if event == 'call':
346                    info.pydev_call_inside_jinja2 = frame.f_back
347                if event in ('line', 'return'):
348                    info.pydev_call_inside_jinja2 = frame
349        else:
350            if event == 'line':
351                if _is_jinja2_render_call(frame) and info.pydev_call_inside_jinja2 is frame:
352                    stop_info['jinja2_stop'] = True
353                    plugin_stop = stop_info['jinja2_stop']
354            if event == 'return':
355                if frame is info.pydev_call_inside_jinja2 and 'event' not in frame.f_back.f_locals:
356                    info.pydev_call_inside_jinja2 = _find_jinja2_render_frame(frame.f_back)
357        return stop, plugin_stop
358    else:
359        if event == 'return' and _is_jinja2_context_call(frame.f_back):
360            # we return from python code to Jinja2 rendering frame
361            info.pydev_call_from_jinja2 = None
362            info.pydev_call_inside_jinja2 = _find_jinja2_render_frame(frame)
363            thread.additional_info.suspend_type = JINJA2_SUSPEND
364            stop = False
365            return stop, plugin_stop
366    # print "info.pydev_call_from_jinja2", info.pydev_call_from_jinja2, "stop", stop, "jinja_stop", jinja2_stop, \
367    #    "thread.additional_info.suspend_type", thread.additional_info.suspend_type
368    # print "event", event, "info.pydev_call_inside_jinja2", info.pydev_call_inside_jinja2
369    # print "frame", frame, "frame.f_back", frame.f_back, "step_stop", info.pydev_step_stop
370    # print "is_context_call", _is_jinja2_context_call(frame)
371    # print "render", _is_jinja2_render_call(frame)
372    # print "-------------"
373    return stop, plugin_stop
374
375
376def stop(plugin, pydb, frame, event, args, stop_info, arg, step_cmd):
377    pydb = args[0]
378    thread = args[3]
379    if 'jinja2_stop' in stop_info and stop_info['jinja2_stop']:
380        frame = _suspend_jinja2(pydb, thread, frame, step_cmd)
381        if frame:
382            pydb.do_wait_suspend(thread, frame, event, arg)
383            return True
384    return False
385
386
387def get_breakpoint(plugin, pydb, pydb_frame, frame, event, args):
388    pydb = args[0]
389    _filename = args[1]
390    info = args[2]
391    new_frame = None
392    jinja2_breakpoint = None
393    flag = False
394    break_type = 'jinja2'
395    if event == 'line' and info.pydev_state != STATE_SUSPEND and \
396            pydb.jinja2_breakpoints and _is_jinja2_render_call(frame):
397        original_filename = _get_jinja2_template_original_filename(frame)
398        if original_filename is not None:
399            pydev_log.debug("Jinja2 is rendering a template: %s", original_filename)
400            canonical_normalized_filename = canonical_normalized_path(original_filename)
401            jinja2_breakpoints_for_file = pydb.jinja2_breakpoints.get(canonical_normalized_filename)
402
403            if jinja2_breakpoints_for_file:
404                template_lineno = _get_jinja2_template_line(frame)
405                if template_lineno is not None:
406                    jinja2_breakpoint = jinja2_breakpoints_for_file.get(template_lineno)
407                    if jinja2_breakpoint is not None:
408                        flag = True
409                        new_frame = Jinja2TemplateFrame(frame, original_filename, template_lineno)
410
411    return flag, jinja2_breakpoint, new_frame, break_type
412
413
414def suspend(plugin, pydb, thread, frame, bp_type):
415    if bp_type == 'jinja2':
416        return _suspend_jinja2(pydb, thread, frame)
417    return None
418
419
420def exception_break(plugin, pydb, pydb_frame, frame, args, arg):
421    pydb = args[0]
422    thread = args[3]
423    exception, value, trace = arg
424    if pydb.jinja2_exception_break and exception is not None:
425        exception_type = dict_keys(pydb.jinja2_exception_break)[0]
426        if exception.__name__ in ('UndefinedError', 'TemplateNotFound', 'TemplatesNotFound'):
427            # errors in rendering
428            render_frame = _find_jinja2_render_frame(frame)
429            if render_frame:
430                suspend_frame = _suspend_jinja2(pydb, thread, render_frame, CMD_ADD_EXCEPTION_BREAK, message=exception_type)
431                if suspend_frame:
432                    add_exception_to_frame(suspend_frame, (exception, value, trace))
433                    suspend_frame.f_back = frame
434                    frame = suspend_frame
435                    return True, frame
436
437        elif exception.__name__ in ('TemplateSyntaxError', 'TemplateAssertionError'):
438            name = frame.f_code.co_name
439
440            if IS_PY2:
441                if name == 'fail':
442                    module_name = frame.f_globals.get('__name__', '')
443                    if module_name == 'jinja2.parser':
444                        filename = value.filename
445                        lineno = value.lineno
446
447                        syntax_error_frame = Jinja2TemplateSyntaxErrorFrame(
448                            frame, exception.__name__, filename, lineno, {'name': value.name, 'exception': value})
449
450                        pydb_frame.set_suspend(thread, CMD_ADD_EXCEPTION_BREAK)
451                        add_exception_to_frame(syntax_error_frame, (exception, value, trace))
452                        thread.additional_info.suspend_type = JINJA2_SUSPEND
453                        thread.additional_info.pydev_message = str(exception_type)
454                        return True, syntax_error_frame
455
456            else:
457                # errors in compile time
458                if name in ('template', 'top-level template code', '<module>') or name.startswith('block '):
459
460                    f_back = frame.f_back
461                    if f_back is not None:
462                        module_name = f_back.f_globals.get('__name__', '')
463
464                    if module_name.startswith('jinja2.'):
465                        # Jinja2 translates exception info and creates fake frame on his own
466                        pydb_frame.set_suspend(thread, CMD_ADD_EXCEPTION_BREAK)
467                        add_exception_to_frame(frame, (exception, value, trace))
468                        thread.additional_info.suspend_type = JINJA2_SUSPEND
469                        thread.additional_info.pydev_message = str(exception_type)
470                        return True, frame
471    return None
472