import traceback from _pydevd_bundle.pydevd_breakpoints import LineBreakpoint, get_exception_name from _pydevd_bundle.pydevd_constants import get_current_thread_id, STATE_SUSPEND, dict_iter_items, dict_keys, JINJA2_SUSPEND from _pydevd_bundle.pydevd_comm import CMD_SET_BREAK, CMD_ADD_EXCEPTION_BREAK from _pydevd_bundle import pydevd_vars from pydevd_file_utils import get_abs_path_real_path_and_base_from_file from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, FCode class Jinja2LineBreakpoint(LineBreakpoint): def __init__(self, file, line, condition, func_name, expression, hit_condition=None, is_logpoint=False): self.file = file LineBreakpoint.__init__(self, line, condition, func_name, expression, hit_condition=hit_condition, is_logpoint=is_logpoint) def is_triggered(self, template_frame_file, template_frame_line): return self.file == template_frame_file and self.line == template_frame_line def __str__(self): return "Jinja2LineBreakpoint: %s-%d" % (self.file, self.line) def __repr__(self): return '' % (self.file, self.line, self.condition, self.func_name, self.expression) def add_line_breakpoint(plugin, pydb, type, file, line, condition, expression, func_name, hit_condition=None, is_logpoint=False): result = None if type == 'jinja2-line': breakpoint = Jinja2LineBreakpoint(file, line, condition, func_name, expression, hit_condition=hit_condition, is_logpoint=is_logpoint) if not hasattr(pydb, 'jinja2_breakpoints'): _init_plugin_breaks(pydb) result = breakpoint, pydb.jinja2_breakpoints return result return result def add_exception_breakpoint(plugin, pydb, type, exception): if type == 'jinja2': if not hasattr(pydb, 'jinja2_exception_break'): _init_plugin_breaks(pydb) pydb.jinja2_exception_break[exception] = True return True return False def _init_plugin_breaks(pydb): pydb.jinja2_exception_break = {} pydb.jinja2_breakpoints = {} def remove_exception_breakpoint(plugin, pydb, type, exception): if type == 'jinja2': try: del pydb.jinja2_exception_break[exception] return True except: pass return False def get_breakpoints(plugin, pydb, type): if type == 'jinja2-line': return pydb.jinja2_breakpoints return None def _is_jinja2_render_call(frame): try: name = frame.f_code.co_name if "__jinja_template__" in frame.f_globals and name in ("root", "loop", "macro") or name.startswith("block_"): return True return False except: traceback.print_exc() return False def _suspend_jinja2(pydb, thread, frame, cmd=CMD_SET_BREAK, message=None): frame = Jinja2TemplateFrame(frame) if frame.f_lineno is None: return None pydevd_vars.add_additional_frame_by_id(get_current_thread_id(thread), {id(frame): frame}) pydb.set_suspend(thread, cmd) thread.additional_info.suspend_type = JINJA2_SUSPEND if cmd == CMD_ADD_EXCEPTION_BREAK: # send exception name as message if message: message = "jinja2-%s" % str(message) thread.additional_info.pydev_message = message return frame def _is_jinja2_suspended(thread): return thread.additional_info.suspend_type == JINJA2_SUSPEND def _is_jinja2_context_call(frame): return "_Context__obj" in frame.f_locals def _is_jinja2_internal_function(frame): return 'self' in frame.f_locals and frame.f_locals['self'].__class__.__name__ in \ ('LoopContext', 'TemplateReference', 'Macro', 'BlockReference') def _find_jinja2_render_frame(frame): while frame is not None and not _is_jinja2_render_call(frame): frame = frame.f_back return frame #======================================================================================================================= # Jinja2 Frame #======================================================================================================================= class Jinja2TemplateFrame: def __init__(self, frame): file_name = _get_jinja2_template_filename(frame) self.back_context = None if 'context' in frame.f_locals: #sometimes we don't have 'context', e.g. in macros self.back_context = frame.f_locals['context'] self.f_code = FCode('template', file_name) self.f_lineno = _get_jinja2_template_line(frame) self.f_back = frame self.f_globals = {} self.f_locals = self.collect_context(frame) self.f_trace = None def _get_real_var_name(self, orig_name): # replace leading number for local variables parts = orig_name.split('_') if len(parts) > 1 and parts[0].isdigit(): return parts[1] return orig_name def collect_context(self, frame): res = {} for k, v in frame.f_locals.items(): if not k.startswith('l_'): res[k] = v elif v and not _is_missing(v): res[self._get_real_var_name(k[2:])] = v if self.back_context is not None: for k, v in self.back_context.items(): res[k] = v return res def _change_variable(self, frame, name, value): in_vars_or_parents = False if 'context' in frame.f_locals: if name in frame.f_locals['context'].parent: self.back_context.parent[name] = value in_vars_or_parents = True if name in frame.f_locals['context'].vars: self.back_context.vars[name] = value in_vars_or_parents = True l_name = 'l_' + name if l_name in frame.f_locals: if in_vars_or_parents: frame.f_locals[l_name] = self.back_context.resolve(name) else: frame.f_locals[l_name] = value def change_variable(plugin, frame, attr, expression): if isinstance(frame, Jinja2TemplateFrame): result = eval(expression, frame.f_globals, frame.f_locals) frame._change_variable(frame.f_back, attr, result) return result return False def _is_missing(item): if item.__class__.__name__ == 'MissingType': return True return False def _find_render_function_frame(frame): #in order to hide internal rendering functions old_frame = frame try: while not ('self' in frame.f_locals and frame.f_locals['self'].__class__.__name__ == 'Template' and \ frame.f_code.co_name == 'render'): frame = frame.f_back if frame is None: return old_frame return frame except: return old_frame def _get_jinja2_template_line(frame): debug_info = None if '__jinja_template__' in frame.f_globals: _debug_info = frame.f_globals['__jinja_template__']._debug_info if _debug_info != '': #sometimes template contains only plain text debug_info = frame.f_globals['__jinja_template__'].debug_info if debug_info is None: return None lineno = frame.f_lineno for pair in debug_info: if pair[1] == lineno: return pair[0] return None def _get_jinja2_template_filename(frame): if '__jinja_template__' in frame.f_globals: fname = frame.f_globals['__jinja_template__'].filename abs_path_real_path_and_base = get_abs_path_real_path_and_base_from_file(fname) return abs_path_real_path_and_base[1] return None #======================================================================================================================= # Jinja2 Step Commands #======================================================================================================================= def has_exception_breaks(plugin): if len(plugin.main_debugger.jinja2_exception_break) > 0: return True return False def has_line_breaks(plugin): for file, breakpoints in dict_iter_items(plugin.main_debugger.jinja2_breakpoints): if len(breakpoints) > 0: return True return False def can_not_skip(plugin, pydb, frame, info): if pydb.jinja2_breakpoints and _is_jinja2_render_call(frame): filename = _get_jinja2_template_filename(frame) jinja2_breakpoints_for_file = pydb.jinja2_breakpoints.get(filename) if jinja2_breakpoints_for_file: return True return False def cmd_step_into(plugin, pydb, frame, event, args, stop_info, stop): info = args[2] thread = args[3] plugin_stop = False stop_info['jinja2_stop'] = False if _is_jinja2_suspended(thread): stop_info['jinja2_stop'] = event in ('call', 'line') and _is_jinja2_render_call(frame) plugin_stop = stop_info['jinja2_stop'] stop = False if info.pydev_call_from_jinja2 is not None: if _is_jinja2_internal_function(frame): #if internal Jinja2 function was called, we sould continue debugging inside template info.pydev_call_from_jinja2 = None else: #we go into python code from Jinja2 rendering frame stop = True if event == 'call' and _is_jinja2_context_call(frame.f_back): #we called function from context, the next step will be in function info.pydev_call_from_jinja2 = 1 if event == 'return' and _is_jinja2_context_call(frame.f_back): #we return from python code to Jinja2 rendering frame info.pydev_step_stop = info.pydev_call_from_jinja2 info.pydev_call_from_jinja2 = None thread.additional_info.suspend_type = JINJA2_SUSPEND stop = False #print "info.pydev_call_from_jinja2", info.pydev_call_from_jinja2, "stop_info", stop_info, \ # "thread.additional_info.suspend_type", thread.additional_info.suspend_type #print "event", event, "farme.locals", frame.f_locals return stop, plugin_stop def cmd_step_over(plugin, pydb, frame, event, args, stop_info, stop): info = args[2] thread = args[3] plugin_stop = False stop_info['jinja2_stop'] = False if _is_jinja2_suspended(thread): stop = False if info.pydev_call_inside_jinja2 is None: if _is_jinja2_render_call(frame): if event == 'call': info.pydev_call_inside_jinja2 = frame.f_back if event in ('line', 'return'): info.pydev_call_inside_jinja2 = frame else: if event == 'line': if _is_jinja2_render_call(frame) and info.pydev_call_inside_jinja2 is frame: stop_info['jinja2_stop'] = True plugin_stop = stop_info['jinja2_stop'] if event == 'return': if frame is info.pydev_call_inside_jinja2 and 'event' not in frame.f_back.f_locals: info.pydev_call_inside_jinja2 = _find_jinja2_render_frame(frame.f_back) return stop, plugin_stop else: if event == 'return' and _is_jinja2_context_call(frame.f_back): #we return from python code to Jinja2 rendering frame info.pydev_call_from_jinja2 = None info.pydev_call_inside_jinja2 = _find_jinja2_render_frame(frame) thread.additional_info.suspend_type = JINJA2_SUSPEND stop = False return stop, plugin_stop #print "info.pydev_call_from_jinja2", info.pydev_call_from_jinja2, "stop", stop, "jinja_stop", jinja2_stop, \ # "thread.additional_info.suspend_type", thread.additional_info.suspend_type #print "event", event, "info.pydev_call_inside_jinja2", info.pydev_call_inside_jinja2 #print "frame", frame, "frame.f_back", frame.f_back, "step_stop", info.pydev_step_stop #print "is_context_call", _is_jinja2_context_call(frame) #print "render", _is_jinja2_render_call(frame) #print "-------------" return stop, plugin_stop def stop(plugin, pydb, frame, event, args, stop_info, arg, step_cmd): pydb = args[0] thread = args[3] if 'jinja2_stop' in stop_info and stop_info['jinja2_stop']: frame = _suspend_jinja2(pydb, thread, frame, step_cmd) if frame: pydb.do_wait_suspend(thread, frame, event, arg) return True return False def get_breakpoint(plugin, pydb, frame, event, args): pydb= args[0] filename = args[1] info = args[2] new_frame = None jinja2_breakpoint = None flag = False type = 'jinja2' if event == 'line' and info.pydev_state != STATE_SUSPEND and \ pydb.jinja2_breakpoints and _is_jinja2_render_call(frame): filename = _get_jinja2_template_filename(frame) jinja2_breakpoints_for_file = pydb.jinja2_breakpoints.get(filename) new_frame = Jinja2TemplateFrame(frame) if jinja2_breakpoints_for_file: lineno = frame.f_lineno template_lineno = _get_jinja2_template_line(frame) if template_lineno is not None and template_lineno in jinja2_breakpoints_for_file: jinja2_breakpoint = jinja2_breakpoints_for_file[template_lineno] flag = True new_frame = Jinja2TemplateFrame(frame) return flag, jinja2_breakpoint, new_frame, type def suspend(plugin, pydb, thread, frame, bp_type): if bp_type == 'jinja2': return _suspend_jinja2(pydb, thread, frame) return None def exception_break(plugin, pydb, frame, args, arg): pydb = args[0] thread = args[3] exception, value, trace = arg if pydb.jinja2_exception_break: exception_type = dict_keys(pydb.jinja2_exception_break)[0] if get_exception_name(exception) in ('UndefinedError', 'TemplateNotFound', 'TemplatesNotFound'): #errors in rendering render_frame = _find_jinja2_render_frame(frame) if render_frame: suspend_frame = _suspend_jinja2(pydb, thread, render_frame, CMD_ADD_EXCEPTION_BREAK, message=exception_type) if suspend_frame: add_exception_to_frame(suspend_frame, (exception, value, trace)) flag = True suspend_frame.f_back = frame frame = suspend_frame return flag, frame elif get_exception_name(exception) in ('TemplateSyntaxError', 'TemplateAssertionError'): #errors in compile time name = frame.f_code.co_name if name in ('template', 'top-level template code', '') or name.startswith('block '): #Jinja2 translates exception info and creates fake frame on his own pydb.set_suspend(thread, CMD_ADD_EXCEPTION_BREAK) add_exception_to_frame(frame, (exception, value, trace)) thread.additional_info.suspend_type = JINJA2_SUSPEND thread.additional_info.pydev_message = str(exception_type) flag = True return flag, frame return None