diff options
Diffstat (limited to 'python/helpers/pydev/pydevd_plugins/jinja2_debug.py')
-rw-r--r-- | python/helpers/pydev/pydevd_plugins/jinja2_debug.py | 341 |
1 files changed, 341 insertions, 0 deletions
diff --git a/python/helpers/pydev/pydevd_plugins/jinja2_debug.py b/python/helpers/pydev/pydevd_plugins/jinja2_debug.py new file mode 100644 index 000000000000..9968a81d0043 --- /dev/null +++ b/python/helpers/pydev/pydevd_plugins/jinja2_debug.py @@ -0,0 +1,341 @@ +import traceback +from pydevd_breakpoints import LineBreakpoint, get_exception_name +from pydevd_constants import GetThreadId, STATE_SUSPEND, DictContains +from pydevd_comm import CMD_SET_BREAK, CMD_STEP_OVER, CMD_ADD_EXCEPTION_BREAK +import pydevd_vars +from pydevd_file_utils import GetFileNameAndBaseFromFile +from pydevd_frame_utils import add_exception_to_frame, FCode, cached_call + +JINJA2_SUSPEND = 3 + +class Jinja2LineBreakpoint(LineBreakpoint): + + def __init__(self, file, line, condition, func_name, expression): + self.file = file + LineBreakpoint.__init__(self, line, condition, func_name, expression) + + 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 add_line_breakpoint(plugin, pydb, type, file, line, condition, func_name, expression): + result = None + if type == 'jinja2-line': + breakpoint = Jinja2LineBreakpoint(file, line, condition, func_name, expression) + if not hasattr(pydb, 'jinja2_breakpoints'): + pydb.jinja2_breakpoints = {} + 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'): + pydb.jinja2_exception_break = {} + pydb.jinja2_exception_break[exception] = True + pydb.setTracingForUntracedContexts() + return True + return False + +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 DictContains(frame.f_globals, "__jinja_template__") 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): + frame = Jinja2TemplateFrame(frame) + + if frame.f_lineno is None: + return None + + pydevd_vars.addAdditionalFrameById(GetThreadId(thread), {id(frame): frame}) + pydb.setSuspend(thread, cmd) + + thread.additionalInfo.suspend_type = JINJA2_SUSPEND + thread.additionalInfo.filename = frame.f_code.co_filename + thread.additionalInfo.line = frame.f_lineno + + return frame + +def is_jinja2_suspended(thread): + return thread.additionalInfo.suspend_type == JINJA2_SUSPEND + +def is_jinja2_context_call(frame): + return DictContains(frame.f_locals, "_Context__obj") + +def is_jinja2_internal_function(frame): + return DictContains(frame.f_locals, 'self') 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 + +def change_variable(plugin, pydb, frame, attr, expression): + if isinstance(frame, Jinja2TemplateFrame): + result = eval(expression, frame.f_globals, frame.f_locals) + frame.changeVariable(attr, result) + + +#======================================================================================================================= +# 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 = find_render_function_frame(frame) + self.f_globals = {} + self.f_locals = self.collect_context(frame) + self.f_trace = None + + def collect_context(self, frame): + res = {} + if self.back_context is not None: + for k, v in self.back_context.items(): + res[k] = v + for k, v in frame.f_locals.items(): + if not k.startswith('l_'): + if not k in res: + #local variables should shadow globals from context + res[k] = v + elif v and not is_missing(v): + res[k[2:]] = v + return res + + def changeVariable(self, name, value): + for k, v in self.back_context.items(): + if k == name: + self.back_context.vars[k] = value + +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 (DictContains(frame.f_locals, 'self') 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 DictContains(frame.f_globals,'__jinja_template__'): + _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 DictContains(frame.f_globals, '__jinja_template__'): + fname = frame.f_globals['__jinja_template__'].filename + filename, base = GetFileNameAndBaseFromFile(fname) + return filename + return None + + +#======================================================================================================================= +# Jinja2 Step Commands +#======================================================================================================================= + + +def has_exception_breaks(plugin, pydb): + return hasattr(pydb, 'jinja2_exception_break') and pydb.jinja2_exception_break + +def can_not_skip(plugin, pydb, pydb_frame, frame): + if hasattr(pydb, 'jinja2_breakpoints') and pydb.jinja2_breakpoints and cached_call(pydb_frame, 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): + pydb, filename, info, thread = args + if not hasattr(info, 'pydev_call_from_jinja2'): + info.pydev_call_from_jinja2 = None + if not hasattr(info, 'pydev_call_inside_jinja2'): + info.pydev_call_inside_jinja2 = None + if is_jinja2_suspended(thread): + stop_info['jinja2_stop'] = event in ('call', 'line') and is_jinja2_render_call(frame) + stop_info['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_info['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.additionalInfo.suspend_type = JINJA2_SUSPEND + stop_info['stop'] = False + + #print "info.pydev_call_from_jinja2", info.pydev_call_from_jinja2, "stop_info", stop_info, \ + # "thread.additionalInfo.suspend_type", thread.additionalInfo.suspend_type + #print "event", event, "farme.locals", frame.f_locals + + +def cmd_step_over(plugin, pydb, frame, event, args, stop_info): + pydb, filename, info, thread = args + if not hasattr(info, 'pydev_call_from_jinja2'): + info.pydev_call_from_jinja2 = None + if not hasattr(info, 'pydev_call_inside_jinja2'): + info.pydev_call_inside_jinja2 = None + if is_jinja2_suspended(thread): + stop_info['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 + if event == 'return': + if frame is info.pydev_call_inside_jinja2 and not DictContains(frame.f_back.f_locals,'event'): + info.pydev_call_inside_jinja2 = find_jinja2_render_frame(frame.f_back) + return True + 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.additionalInfo.suspend_type = JINJA2_SUSPEND + stop_info['stop'] = False + return True + #print "info.pydev_call_from_jinja2", info.pydev_call_from_jinja2, "stop", stop, "jinja_stop", jinja2_stop, \ + # "thread.additionalInfo.suspend_type", thread.additionalInfo.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 False + + +def stop(plugin, pydb, frame, event, args, stop_info, arg, step_cmd): + pydb, filename, info, thread = args + if DictContains(stop_info, 'jinja2_stop') and stop_info['jinja2_stop']: + frame = suspend_jinja2(pydb, thread, frame, step_cmd) + if frame: + pydb.doWaitSuspend(thread, frame, event, arg) + return True + return False + + +def get_breakpoint(plugin, pydb, pydb_frame, frame, event, args): + pydb, filename, info, thread = args + new_frame = None + jinja2_breakpoint = None + flag = False + if event in ('line', 'call') and info.pydev_state != STATE_SUSPEND and hasattr(pydb, 'jinja2_breakpoints') and \ + pydb.jinja2_breakpoints and cached_call(pydb_frame, 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 DictContains(jinja2_breakpoints_for_file, template_lineno): + jinja2_breakpoint = jinja2_breakpoints_for_file[template_lineno] + flag = True + new_frame = Jinja2TemplateFrame(frame) + + return flag, jinja2_breakpoint, new_frame + + +def suspend(plugin, pydb, thread, frame): + return suspend_jinja2(pydb, thread, frame) + + +def exception_break(plugin, pydb, pydb_frame, frame, args, arg): + pydb, filename, info, thread = args + exception, value, trace = arg + if hasattr(pydb, 'jinja2_exception_break') and pydb.jinja2_exception_break: + 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) + 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_frame.setSuspend(thread, CMD_ADD_EXCEPTION_BREAK) + add_exception_to_frame(frame, (exception, value, trace)) + thread.additionalInfo.suspend_type = JINJA2_SUSPEND + flag = True + return flag, frame + return None
\ No newline at end of file |