blob: 9968a81d0043ca7c12e03357c518b0dbb5a97f9e [file] [log] [blame]
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