| 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 '<Jinja2LineBreakpoint(%s, %s, %s, %s, %s)>' % (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, pydb_frame, 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, pydb_frame, 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, pydb_frame, 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', '<module>') or name.startswith('block '): | |
| #Jinja2 translates exception info and creates fake frame on his own | |
| pydb_frame.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 |