Vendor debugpy for VS Code attach debugging
Bundles debugpy 1.7.0 directly in python/ so "Maya: Attach (debugpy)" works with no per-machine pip install step. Installed via Maya 2022's own pip so it resolved a version actually compatible with Python 3.7 (Maya 2022's interpreter), then verified import + listen() succeeds under all three target Maya Python versions (3.7/3.9/3.10). Also fixes a latent __file__-under-exec() bug in start_debug_server.py (same pitfall as Maya's own plugin loader, never hit until this exercised it) and corrects the README's Maya Python version claim -- 2022 ships Python 3.7, not 3.9 as previously stated, which was never independently verified until now. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,613 @@
|
||||
import inspect
|
||||
|
||||
from _pydev_bundle import pydev_log
|
||||
from _pydevd_bundle.pydevd_comm import CMD_SET_BREAK, CMD_ADD_EXCEPTION_BREAK
|
||||
from _pydevd_bundle.pydevd_constants import STATE_SUSPEND, DJANGO_SUSPEND, \
|
||||
DebugInfoHolder
|
||||
from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, FCode, just_raised, ignore_exception_trace
|
||||
from pydevd_file_utils import canonical_normalized_path, absolute_path
|
||||
from _pydevd_bundle.pydevd_api import PyDevdAPI
|
||||
from pydevd_plugins.pydevd_line_validation import LineBreakpointWithLazyValidation, ValidationInfo
|
||||
from _pydev_bundle.pydev_override import overrides
|
||||
|
||||
IS_DJANGO18 = False
|
||||
IS_DJANGO19 = False
|
||||
IS_DJANGO19_OR_HIGHER = False
|
||||
try:
|
||||
import django
|
||||
version = django.VERSION
|
||||
IS_DJANGO18 = version[0] == 1 and version[1] == 8
|
||||
IS_DJANGO19 = version[0] == 1 and version[1] == 9
|
||||
IS_DJANGO19_OR_HIGHER = ((version[0] == 1 and version[1] >= 9) or version[0] > 1)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class DjangoLineBreakpoint(LineBreakpointWithLazyValidation):
|
||||
|
||||
def __init__(self, canonical_normalized_filename, breakpoint_id, line, condition, func_name, expression, hit_condition=None, is_logpoint=False):
|
||||
self.canonical_normalized_filename = canonical_normalized_filename
|
||||
LineBreakpointWithLazyValidation.__init__(self, breakpoint_id, line, condition, func_name, expression, hit_condition=hit_condition, is_logpoint=is_logpoint)
|
||||
|
||||
def __str__(self):
|
||||
return "DjangoLineBreakpoint: %s-%d" % (self.canonical_normalized_filename, self.line)
|
||||
|
||||
|
||||
class _DjangoValidationInfo(ValidationInfo):
|
||||
|
||||
@overrides(ValidationInfo._collect_valid_lines_in_template_uncached)
|
||||
def _collect_valid_lines_in_template_uncached(self, template):
|
||||
lines = set()
|
||||
for node in self._iternodes(template.nodelist):
|
||||
if node.__class__.__name__ in _IGNORE_RENDER_OF_CLASSES:
|
||||
continue
|
||||
lineno = self._get_lineno(node)
|
||||
if lineno is not None:
|
||||
lines.add(lineno)
|
||||
return lines
|
||||
|
||||
def _get_lineno(self, node):
|
||||
if hasattr(node, 'token') and hasattr(node.token, 'lineno'):
|
||||
return node.token.lineno
|
||||
return None
|
||||
|
||||
def _iternodes(self, nodelist):
|
||||
for node in nodelist:
|
||||
yield node
|
||||
|
||||
try:
|
||||
children = node.child_nodelists
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
for attr in children:
|
||||
nodelist = getattr(node, attr, None)
|
||||
if nodelist:
|
||||
# i.e.: yield from _iternodes(nodelist)
|
||||
for node in self._iternodes(nodelist):
|
||||
yield node
|
||||
|
||||
|
||||
def add_line_breakpoint(plugin, pydb, type, canonical_normalized_filename, breakpoint_id, line, condition, expression, func_name, hit_condition=None, is_logpoint=False, add_breakpoint_result=None, on_changed_breakpoint_state=None):
|
||||
if type == 'django-line':
|
||||
django_line_breakpoint = DjangoLineBreakpoint(canonical_normalized_filename, breakpoint_id, line, condition, func_name, expression, hit_condition=hit_condition, is_logpoint=is_logpoint)
|
||||
if not hasattr(pydb, 'django_breakpoints'):
|
||||
_init_plugin_breaks(pydb)
|
||||
|
||||
if IS_DJANGO19_OR_HIGHER:
|
||||
add_breakpoint_result.error_code = PyDevdAPI.ADD_BREAKPOINT_LAZY_VALIDATION
|
||||
django_line_breakpoint.add_breakpoint_result = add_breakpoint_result
|
||||
django_line_breakpoint.on_changed_breakpoint_state = on_changed_breakpoint_state
|
||||
else:
|
||||
add_breakpoint_result.error_code = PyDevdAPI.ADD_BREAKPOINT_NO_ERROR
|
||||
|
||||
return django_line_breakpoint, pydb.django_breakpoints
|
||||
return None
|
||||
|
||||
|
||||
def after_breakpoints_consolidated(plugin, py_db, canonical_normalized_filename, id_to_pybreakpoint, file_to_line_to_breakpoints):
|
||||
if IS_DJANGO19_OR_HIGHER:
|
||||
django_breakpoints_for_file = file_to_line_to_breakpoints.get(canonical_normalized_filename)
|
||||
if not django_breakpoints_for_file:
|
||||
return
|
||||
|
||||
if not hasattr(py_db, 'django_validation_info'):
|
||||
_init_plugin_breaks(py_db)
|
||||
|
||||
# In general we validate the breakpoints only when the template is loaded, but if the template
|
||||
# was already loaded, we can validate the breakpoints based on the last loaded value.
|
||||
py_db.django_validation_info.verify_breakpoints_from_template_cached_lines(
|
||||
py_db, canonical_normalized_filename, django_breakpoints_for_file)
|
||||
|
||||
|
||||
def add_exception_breakpoint(plugin, pydb, type, exception):
|
||||
if type == 'django':
|
||||
if not hasattr(pydb, 'django_exception_break'):
|
||||
_init_plugin_breaks(pydb)
|
||||
pydb.django_exception_break[exception] = True
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _init_plugin_breaks(pydb):
|
||||
pydb.django_exception_break = {}
|
||||
pydb.django_breakpoints = {}
|
||||
|
||||
pydb.django_validation_info = _DjangoValidationInfo()
|
||||
|
||||
|
||||
def remove_exception_breakpoint(plugin, pydb, type, exception):
|
||||
if type == 'django':
|
||||
try:
|
||||
del pydb.django_exception_break[exception]
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def remove_all_exception_breakpoints(plugin, pydb):
|
||||
if hasattr(pydb, 'django_exception_break'):
|
||||
pydb.django_exception_break = {}
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_breakpoints(plugin, pydb, type):
|
||||
if type == 'django-line':
|
||||
return pydb.django_breakpoints
|
||||
return None
|
||||
|
||||
|
||||
def _inherits(cls, *names):
|
||||
if cls.__name__ in names:
|
||||
return True
|
||||
inherits_node = False
|
||||
for base in inspect.getmro(cls):
|
||||
if base.__name__ in names:
|
||||
inherits_node = True
|
||||
break
|
||||
return inherits_node
|
||||
|
||||
|
||||
_IGNORE_RENDER_OF_CLASSES = ('TextNode', 'NodeList')
|
||||
|
||||
|
||||
def _is_django_render_call(frame, debug=False):
|
||||
try:
|
||||
name = frame.f_code.co_name
|
||||
if name != 'render':
|
||||
return False
|
||||
|
||||
if 'self' not in frame.f_locals:
|
||||
return False
|
||||
|
||||
cls = frame.f_locals['self'].__class__
|
||||
|
||||
inherits_node = _inherits(cls, 'Node')
|
||||
|
||||
if not inherits_node:
|
||||
return False
|
||||
|
||||
clsname = cls.__name__
|
||||
if IS_DJANGO19:
|
||||
# in Django 1.9 we need to save the flag that there is included template
|
||||
if clsname == 'IncludeNode':
|
||||
if 'context' in frame.f_locals:
|
||||
context = frame.f_locals['context']
|
||||
context._has_included_template = True
|
||||
|
||||
return clsname not in _IGNORE_RENDER_OF_CLASSES
|
||||
except:
|
||||
pydev_log.exception()
|
||||
return False
|
||||
|
||||
|
||||
def _is_django_context_get_call(frame):
|
||||
try:
|
||||
if 'self' not in frame.f_locals:
|
||||
return False
|
||||
|
||||
cls = frame.f_locals['self'].__class__
|
||||
|
||||
return _inherits(cls, 'BaseContext')
|
||||
except:
|
||||
pydev_log.exception()
|
||||
return False
|
||||
|
||||
|
||||
def _is_django_resolve_call(frame):
|
||||
try:
|
||||
name = frame.f_code.co_name
|
||||
if name != '_resolve_lookup':
|
||||
return False
|
||||
|
||||
if 'self' not in frame.f_locals:
|
||||
return False
|
||||
|
||||
cls = frame.f_locals['self'].__class__
|
||||
|
||||
clsname = cls.__name__
|
||||
return clsname == 'Variable'
|
||||
except:
|
||||
pydev_log.exception()
|
||||
return False
|
||||
|
||||
|
||||
def _is_django_suspended(thread):
|
||||
return thread.additional_info.suspend_type == DJANGO_SUSPEND
|
||||
|
||||
|
||||
def suspend_django(main_debugger, thread, frame, cmd=CMD_SET_BREAK):
|
||||
if frame.f_lineno is None:
|
||||
return None
|
||||
|
||||
main_debugger.set_suspend(thread, cmd)
|
||||
thread.additional_info.suspend_type = DJANGO_SUSPEND
|
||||
|
||||
return frame
|
||||
|
||||
|
||||
def _find_django_render_frame(frame):
|
||||
while frame is not None and not _is_django_render_call(frame):
|
||||
frame = frame.f_back
|
||||
|
||||
return frame
|
||||
|
||||
#=======================================================================================================================
|
||||
# Django Frame
|
||||
#=======================================================================================================================
|
||||
|
||||
|
||||
def _read_file(filename):
|
||||
# type: (str) -> str
|
||||
f = open(filename, 'r', encoding='utf-8', errors='replace')
|
||||
s = f.read()
|
||||
f.close()
|
||||
return s
|
||||
|
||||
|
||||
def _offset_to_line_number(text, offset):
|
||||
curLine = 1
|
||||
curOffset = 0
|
||||
while curOffset < offset:
|
||||
if curOffset == len(text):
|
||||
return -1
|
||||
c = text[curOffset]
|
||||
if c == '\n':
|
||||
curLine += 1
|
||||
elif c == '\r':
|
||||
curLine += 1
|
||||
if curOffset < len(text) and text[curOffset + 1] == '\n':
|
||||
curOffset += 1
|
||||
|
||||
curOffset += 1
|
||||
|
||||
return curLine
|
||||
|
||||
|
||||
def _get_source_django_18_or_lower(frame):
|
||||
# This method is usable only for the Django <= 1.8
|
||||
try:
|
||||
node = frame.f_locals['self']
|
||||
if hasattr(node, 'source'):
|
||||
return node.source
|
||||
else:
|
||||
if IS_DJANGO18:
|
||||
# The debug setting was changed since Django 1.8
|
||||
pydev_log.error_once("WARNING: Template path is not available. Set the 'debug' option in the OPTIONS of a DjangoTemplates "
|
||||
"backend.")
|
||||
else:
|
||||
# The debug setting for Django < 1.8
|
||||
pydev_log.error_once("WARNING: Template path is not available. Please set TEMPLATE_DEBUG=True in your settings.py to make "
|
||||
"django template breakpoints working")
|
||||
return None
|
||||
|
||||
except:
|
||||
pydev_log.exception()
|
||||
return None
|
||||
|
||||
|
||||
def _convert_to_str(s):
|
||||
return s
|
||||
|
||||
|
||||
def _get_template_original_file_name_from_frame(frame):
|
||||
try:
|
||||
if IS_DJANGO19:
|
||||
# The Node source was removed since Django 1.9
|
||||
if 'context' in frame.f_locals:
|
||||
context = frame.f_locals['context']
|
||||
if hasattr(context, '_has_included_template'):
|
||||
# if there was included template we need to inspect the previous frames and find its name
|
||||
back = frame.f_back
|
||||
while back is not None and frame.f_code.co_name in ('render', '_render'):
|
||||
locals = back.f_locals
|
||||
if 'self' in locals:
|
||||
self = locals['self']
|
||||
if self.__class__.__name__ == 'Template' and hasattr(self, 'origin') and \
|
||||
hasattr(self.origin, 'name'):
|
||||
return _convert_to_str(self.origin.name)
|
||||
back = back.f_back
|
||||
else:
|
||||
if hasattr(context, 'template') and hasattr(context.template, 'origin') and \
|
||||
hasattr(context.template.origin, 'name'):
|
||||
return _convert_to_str(context.template.origin.name)
|
||||
return None
|
||||
elif IS_DJANGO19_OR_HIGHER:
|
||||
# For Django 1.10 and later there is much simpler way to get template name
|
||||
if 'self' in frame.f_locals:
|
||||
self = frame.f_locals['self']
|
||||
if hasattr(self, 'origin') and hasattr(self.origin, 'name'):
|
||||
return _convert_to_str(self.origin.name)
|
||||
return None
|
||||
|
||||
source = _get_source_django_18_or_lower(frame)
|
||||
if source is None:
|
||||
pydev_log.debug("Source is None\n")
|
||||
return None
|
||||
fname = _convert_to_str(source[0].name)
|
||||
|
||||
if fname == '<unknown source>':
|
||||
pydev_log.debug("Source name is %s\n" % fname)
|
||||
return None
|
||||
else:
|
||||
return fname
|
||||
except:
|
||||
if DebugInfoHolder.DEBUG_TRACE_LEVEL >= 2:
|
||||
pydev_log.exception('Error getting django template filename.')
|
||||
return None
|
||||
|
||||
|
||||
def _get_template_line(frame):
|
||||
if IS_DJANGO19_OR_HIGHER:
|
||||
node = frame.f_locals['self']
|
||||
if hasattr(node, 'token') and hasattr(node.token, 'lineno'):
|
||||
return node.token.lineno
|
||||
else:
|
||||
return None
|
||||
|
||||
source = _get_source_django_18_or_lower(frame)
|
||||
original_filename = _get_template_original_file_name_from_frame(frame)
|
||||
if original_filename is not None:
|
||||
try:
|
||||
absolute_filename = absolute_path(original_filename)
|
||||
return _offset_to_line_number(_read_file(absolute_filename), source[1][0])
|
||||
except:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
class DjangoTemplateFrame(object):
|
||||
|
||||
IS_PLUGIN_FRAME = True
|
||||
|
||||
def __init__(self, frame):
|
||||
original_filename = _get_template_original_file_name_from_frame(frame)
|
||||
self._back_context = frame.f_locals['context']
|
||||
self.f_code = FCode('Django Template', original_filename)
|
||||
self.f_lineno = _get_template_line(frame)
|
||||
self.f_back = frame
|
||||
self.f_globals = {}
|
||||
self.f_locals = self._collect_context(self._back_context)
|
||||
self.f_trace = None
|
||||
|
||||
def _collect_context(self, context):
|
||||
res = {}
|
||||
try:
|
||||
for d in context.dicts:
|
||||
for k, v in d.items():
|
||||
res[k] = v
|
||||
except AttributeError:
|
||||
pass
|
||||
return res
|
||||
|
||||
def _change_variable(self, name, value):
|
||||
for d in self._back_context.dicts:
|
||||
for k, v in d.items():
|
||||
if k == name:
|
||||
d[k] = value
|
||||
|
||||
|
||||
class DjangoTemplateSyntaxErrorFrame(object):
|
||||
|
||||
IS_PLUGIN_FRAME = True
|
||||
|
||||
def __init__(self, frame, original_filename, lineno, f_locals):
|
||||
self.f_code = FCode('Django TemplateSyntaxError', original_filename)
|
||||
self.f_lineno = lineno
|
||||
self.f_back = frame
|
||||
self.f_globals = {}
|
||||
self.f_locals = f_locals
|
||||
self.f_trace = None
|
||||
|
||||
|
||||
def change_variable(plugin, frame, attr, expression):
|
||||
if isinstance(frame, DjangoTemplateFrame):
|
||||
result = eval(expression, frame.f_globals, frame.f_locals)
|
||||
frame._change_variable(attr, result)
|
||||
return result
|
||||
return False
|
||||
|
||||
|
||||
def _is_django_variable_does_not_exist_exception_break_context(frame):
|
||||
try:
|
||||
name = frame.f_code.co_name
|
||||
except:
|
||||
name = None
|
||||
return name in ('_resolve_lookup', 'find_template')
|
||||
|
||||
|
||||
def _is_ignoring_failures(frame):
|
||||
while frame is not None:
|
||||
if frame.f_code.co_name == 'resolve':
|
||||
ignore_failures = frame.f_locals.get('ignore_failures')
|
||||
if ignore_failures:
|
||||
return True
|
||||
frame = frame.f_back
|
||||
|
||||
return False
|
||||
|
||||
#=======================================================================================================================
|
||||
# Django Step Commands
|
||||
#=======================================================================================================================
|
||||
|
||||
|
||||
def can_skip(plugin, main_debugger, frame):
|
||||
if main_debugger.django_breakpoints:
|
||||
if _is_django_render_call(frame):
|
||||
return False
|
||||
|
||||
if main_debugger.django_exception_break:
|
||||
module_name = frame.f_globals.get('__name__', '')
|
||||
|
||||
if module_name == 'django.template.base':
|
||||
# Exceptions raised at django.template.base must be checked.
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def has_exception_breaks(plugin):
|
||||
if len(plugin.main_debugger.django_exception_break) > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def has_line_breaks(plugin):
|
||||
for _canonical_normalized_filename, breakpoints in plugin.main_debugger.django_breakpoints.items():
|
||||
if len(breakpoints) > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def cmd_step_into(plugin, main_debugger, frame, event, args, stop_info, stop):
|
||||
info = args[2]
|
||||
thread = args[3]
|
||||
plugin_stop = False
|
||||
if _is_django_suspended(thread):
|
||||
stop_info['django_stop'] = event == 'call' and _is_django_render_call(frame)
|
||||
plugin_stop = stop_info['django_stop']
|
||||
stop = stop and _is_django_resolve_call(frame.f_back) and not _is_django_context_get_call(frame)
|
||||
if stop:
|
||||
info.pydev_django_resolve_frame = True # we remember that we've go into python code from django rendering frame
|
||||
return stop, plugin_stop
|
||||
|
||||
|
||||
def cmd_step_over(plugin, main_debugger, frame, event, args, stop_info, stop):
|
||||
info = args[2]
|
||||
thread = args[3]
|
||||
plugin_stop = False
|
||||
if _is_django_suspended(thread):
|
||||
stop_info['django_stop'] = event == 'call' and _is_django_render_call(frame)
|
||||
plugin_stop = stop_info['django_stop']
|
||||
stop = False
|
||||
return stop, plugin_stop
|
||||
else:
|
||||
if event == 'return' and info.pydev_django_resolve_frame and _is_django_resolve_call(frame.f_back):
|
||||
# we return to Django suspend mode and should not stop before django rendering frame
|
||||
info.pydev_step_stop = frame.f_back
|
||||
info.pydev_django_resolve_frame = False
|
||||
thread.additional_info.suspend_type = DJANGO_SUSPEND
|
||||
stop = info.pydev_step_stop is frame and event in ('line', 'return')
|
||||
return stop, plugin_stop
|
||||
|
||||
|
||||
def stop(plugin, main_debugger, frame, event, args, stop_info, arg, step_cmd):
|
||||
main_debugger = args[0]
|
||||
thread = args[3]
|
||||
if 'django_stop' in stop_info and stop_info['django_stop']:
|
||||
frame = suspend_django(main_debugger, thread, DjangoTemplateFrame(frame), step_cmd)
|
||||
if frame:
|
||||
main_debugger.do_wait_suspend(thread, frame, event, arg)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_breakpoint(plugin, py_db, pydb_frame, frame, event, args):
|
||||
py_db = args[0]
|
||||
_filename = args[1]
|
||||
info = args[2]
|
||||
breakpoint_type = 'django'
|
||||
|
||||
if event == 'call' and info.pydev_state != STATE_SUSPEND and py_db.django_breakpoints and _is_django_render_call(frame):
|
||||
original_filename = _get_template_original_file_name_from_frame(frame)
|
||||
pydev_log.debug("Django is rendering a template: %s", original_filename)
|
||||
|
||||
canonical_normalized_filename = canonical_normalized_path(original_filename)
|
||||
django_breakpoints_for_file = py_db.django_breakpoints.get(canonical_normalized_filename)
|
||||
|
||||
if django_breakpoints_for_file:
|
||||
|
||||
# At this point, let's validate whether template lines are correct.
|
||||
if IS_DJANGO19_OR_HIGHER:
|
||||
django_validation_info = py_db.django_validation_info
|
||||
context = frame.f_locals['context']
|
||||
django_template = context.template
|
||||
django_validation_info.verify_breakpoints(py_db, canonical_normalized_filename, django_breakpoints_for_file, django_template)
|
||||
|
||||
pydev_log.debug("Breakpoints for that file: %s", django_breakpoints_for_file)
|
||||
template_line = _get_template_line(frame)
|
||||
pydev_log.debug("Tracing template line: %s", template_line)
|
||||
|
||||
if template_line in django_breakpoints_for_file:
|
||||
django_breakpoint = django_breakpoints_for_file[template_line]
|
||||
new_frame = DjangoTemplateFrame(frame)
|
||||
return True, django_breakpoint, new_frame, breakpoint_type
|
||||
|
||||
return False, None, None, breakpoint_type
|
||||
|
||||
|
||||
def suspend(plugin, main_debugger, thread, frame, bp_type):
|
||||
if bp_type == 'django':
|
||||
return suspend_django(main_debugger, thread, DjangoTemplateFrame(frame))
|
||||
return None
|
||||
|
||||
|
||||
def _get_original_filename_from_origin_in_parent_frame_locals(frame, parent_frame_name):
|
||||
filename = None
|
||||
parent_frame = frame
|
||||
while parent_frame.f_code.co_name != parent_frame_name:
|
||||
parent_frame = parent_frame.f_back
|
||||
|
||||
origin = None
|
||||
if parent_frame is not None:
|
||||
origin = parent_frame.f_locals.get('origin')
|
||||
|
||||
if hasattr(origin, 'name') and origin.name is not None:
|
||||
filename = _convert_to_str(origin.name)
|
||||
return filename
|
||||
|
||||
|
||||
def exception_break(plugin, main_debugger, pydb_frame, frame, args, arg):
|
||||
main_debugger = args[0]
|
||||
thread = args[3]
|
||||
exception, value, trace = arg
|
||||
|
||||
if main_debugger.django_exception_break and exception is not None:
|
||||
if exception.__name__ in ['VariableDoesNotExist', 'TemplateDoesNotExist', 'TemplateSyntaxError'] and \
|
||||
just_raised(trace) and not ignore_exception_trace(trace):
|
||||
|
||||
if exception.__name__ == 'TemplateSyntaxError':
|
||||
# In this case we don't actually have a regular render frame with the context
|
||||
# (we didn't really get to that point).
|
||||
token = getattr(value, 'token', None)
|
||||
|
||||
if token is None:
|
||||
# Django 1.7 does not have token in exception. Try to get it from locals.
|
||||
token = frame.f_locals.get('token')
|
||||
|
||||
lineno = getattr(token, 'lineno', None)
|
||||
|
||||
original_filename = None
|
||||
if lineno is not None:
|
||||
original_filename = _get_original_filename_from_origin_in_parent_frame_locals(frame, 'get_template')
|
||||
|
||||
if original_filename is None:
|
||||
# Django 1.7 does not have origin in get_template. Try to get it from
|
||||
# load_template.
|
||||
original_filename = _get_original_filename_from_origin_in_parent_frame_locals(frame, 'load_template')
|
||||
|
||||
if original_filename is not None and lineno is not None:
|
||||
syntax_error_frame = DjangoTemplateSyntaxErrorFrame(
|
||||
frame, original_filename, lineno, {'token': token, 'exception': exception})
|
||||
|
||||
suspend_frame = suspend_django(
|
||||
main_debugger, thread, syntax_error_frame, CMD_ADD_EXCEPTION_BREAK)
|
||||
return True, suspend_frame
|
||||
|
||||
elif exception.__name__ == 'VariableDoesNotExist':
|
||||
if _is_django_variable_does_not_exist_exception_break_context(frame):
|
||||
if not getattr(exception, 'silent_variable_failure', False) and not _is_ignoring_failures(frame):
|
||||
render_frame = _find_django_render_frame(frame)
|
||||
if render_frame:
|
||||
suspend_frame = suspend_django(
|
||||
main_debugger, thread, DjangoTemplateFrame(render_frame), CMD_ADD_EXCEPTION_BREAK)
|
||||
if suspend_frame:
|
||||
add_exception_to_frame(suspend_frame, (exception, value, trace))
|
||||
thread.additional_info.pydev_message = 'VariableDoesNotExist'
|
||||
suspend_frame.f_back = frame
|
||||
frame = suspend_frame
|
||||
return True, frame
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,30 @@
|
||||
Extensions allow extending the debugger without modifying the debugger code. This is implemented with explicit namespace
|
||||
packages.
|
||||
|
||||
To implement your own extension:
|
||||
|
||||
1. Ensure that the root folder of your extension is in sys.path (add it to PYTHONPATH)
|
||||
2. Ensure that your module follows the directory structure below
|
||||
3. The ``__init__.py`` files inside the pydevd_plugin and extension folder must contain the preamble below,
|
||||
and nothing else.
|
||||
Preamble:
|
||||
```python
|
||||
try:
|
||||
__import__('pkg_resources').declare_namespace(__name__)
|
||||
except ImportError:
|
||||
import pkgutil
|
||||
__path__ = pkgutil.extend_path(__path__, __name__)
|
||||
```
|
||||
4. Your plugin name inside the extensions folder must start with `"pydevd_plugin"`
|
||||
5. Implement one or more of the abstract base classes defined in `_pydevd_bundle.pydevd_extension_api`. This can be done
|
||||
by either inheriting from them or registering with the abstract base class.
|
||||
|
||||
* Directory structure:
|
||||
```
|
||||
|-- root_directory-> must be on python path
|
||||
| |-- pydevd_plugins
|
||||
| | |-- __init__.py -> must contain preamble
|
||||
| | |-- extensions
|
||||
| | | |-- __init__.py -> must contain preamble
|
||||
| | | |-- pydevd_plugin_plugin_name.py
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
import sys
|
||||
|
||||
|
||||
def find_cached_module(mod_name):
|
||||
return sys.modules.get(mod_name, None)
|
||||
|
||||
def find_mod_attr(mod_name, attr):
|
||||
mod = find_cached_module(mod_name)
|
||||
if mod is None:
|
||||
return None
|
||||
return getattr(mod, attr, None)
|
||||
|
||||
|
||||
def find_class_name(val):
|
||||
class_name = str(val.__class__)
|
||||
if class_name.find('.') != -1:
|
||||
class_name = class_name.split('.')[-1]
|
||||
|
||||
elif class_name.find("'") != -1: #does not have '.' (could be something like <type 'int'>)
|
||||
class_name = class_name[class_name.index("'") + 1:]
|
||||
|
||||
if class_name.endswith("'>"):
|
||||
class_name = class_name[:-2]
|
||||
|
||||
return class_name
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
from _pydevd_bundle.pydevd_extension_api import TypeResolveProvider
|
||||
from _pydevd_bundle.pydevd_resolver import defaultResolver
|
||||
from .pydevd_helpers import find_mod_attr
|
||||
from _pydevd_bundle import pydevd_constants
|
||||
|
||||
TOO_LARGE_MSG = 'Maximum number of items (%s) reached. To show more items customize the value of the PYDEVD_CONTAINER_NUMPY_MAX_ITEMS environment variable.'
|
||||
TOO_LARGE_ATTR = 'Unable to handle:'
|
||||
|
||||
|
||||
class NdArrayItemsContainer(object):
|
||||
pass
|
||||
|
||||
|
||||
class NDArrayTypeResolveProvider(object):
|
||||
'''
|
||||
This resolves a numpy ndarray returning some metadata about the NDArray
|
||||
'''
|
||||
|
||||
def can_provide(self, type_object, type_name):
|
||||
nd_array = find_mod_attr('numpy', 'ndarray')
|
||||
return nd_array is not None and issubclass(type_object, nd_array)
|
||||
|
||||
def is_numeric(self, obj):
|
||||
if not hasattr(obj, 'dtype'):
|
||||
return False
|
||||
return obj.dtype.kind in 'biufc'
|
||||
|
||||
def resolve(self, obj, attribute):
|
||||
if attribute == '__internals__':
|
||||
return defaultResolver.get_dictionary(obj)
|
||||
if attribute == 'min':
|
||||
if self.is_numeric(obj) and obj.size > 0:
|
||||
return obj.min()
|
||||
else:
|
||||
return None
|
||||
if attribute == 'max':
|
||||
if self.is_numeric(obj) and obj.size > 0:
|
||||
return obj.max()
|
||||
else:
|
||||
return None
|
||||
if attribute == 'shape':
|
||||
return obj.shape
|
||||
if attribute == 'dtype':
|
||||
return obj.dtype
|
||||
if attribute == 'size':
|
||||
return obj.size
|
||||
if attribute.startswith('['):
|
||||
container = NdArrayItemsContainer()
|
||||
i = 0
|
||||
format_str = '%0' + str(int(len(str(len(obj))))) + 'd'
|
||||
for item in obj:
|
||||
setattr(container, format_str % i, item)
|
||||
i += 1
|
||||
if i >= pydevd_constants.PYDEVD_CONTAINER_NUMPY_MAX_ITEMS:
|
||||
setattr(container, TOO_LARGE_ATTR, TOO_LARGE_MSG % (pydevd_constants.PYDEVD_CONTAINER_NUMPY_MAX_ITEMS,))
|
||||
break
|
||||
return container
|
||||
return None
|
||||
|
||||
def get_dictionary(self, obj):
|
||||
ret = dict()
|
||||
ret['__internals__'] = defaultResolver.get_dictionary(obj)
|
||||
if obj.size > 1024 * 1024:
|
||||
ret['min'] = 'ndarray too big, calculating min would slow down debugging'
|
||||
ret['max'] = 'ndarray too big, calculating max would slow down debugging'
|
||||
elif obj.size == 0:
|
||||
ret['min'] = 'array is empty'
|
||||
ret['max'] = 'array is empty'
|
||||
else:
|
||||
if self.is_numeric(obj):
|
||||
ret['min'] = obj.min()
|
||||
ret['max'] = obj.max()
|
||||
else:
|
||||
ret['min'] = 'not a numeric object'
|
||||
ret['max'] = 'not a numeric object'
|
||||
ret['shape'] = obj.shape
|
||||
ret['dtype'] = obj.dtype
|
||||
ret['size'] = obj.size
|
||||
try:
|
||||
ret['[0:%s] ' % (len(obj))] = list(obj[0:pydevd_constants.PYDEVD_CONTAINER_NUMPY_MAX_ITEMS])
|
||||
except:
|
||||
# This may not work depending on the array shape.
|
||||
pass
|
||||
return ret
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
if not sys.platform.startswith("java"):
|
||||
TypeResolveProvider.register(NDArrayTypeResolveProvider)
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
import sys
|
||||
|
||||
from _pydevd_bundle.pydevd_constants import PANDAS_MAX_ROWS, PANDAS_MAX_COLS, PANDAS_MAX_COLWIDTH
|
||||
from _pydevd_bundle.pydevd_extension_api import TypeResolveProvider, StrPresentationProvider
|
||||
from _pydevd_bundle.pydevd_resolver import inspect, MethodWrapperType
|
||||
from _pydevd_bundle.pydevd_utils import Timer
|
||||
|
||||
from .pydevd_helpers import find_mod_attr
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
def _get_dictionary(obj, replacements):
|
||||
ret = dict()
|
||||
cls = obj.__class__
|
||||
for attr_name in dir(obj):
|
||||
|
||||
# This is interesting but it actually hides too much info from the dataframe.
|
||||
# attr_type_in_cls = type(getattr(cls, attr_name, None))
|
||||
# if attr_type_in_cls == property:
|
||||
# ret[attr_name] = '<property (not computed)>'
|
||||
# continue
|
||||
|
||||
timer = Timer()
|
||||
try:
|
||||
replacement = replacements.get(attr_name)
|
||||
if replacement is not None:
|
||||
ret[attr_name] = replacement
|
||||
continue
|
||||
|
||||
attr_value = getattr(obj, attr_name, '<unable to get>')
|
||||
if inspect.isroutine(attr_value) or isinstance(attr_value, MethodWrapperType):
|
||||
continue
|
||||
ret[attr_name] = attr_value
|
||||
except Exception as e:
|
||||
ret[attr_name] = '<error getting: %s>' % (e,)
|
||||
finally:
|
||||
timer.report_if_getting_attr_slow(cls, attr_name)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@contextmanager
|
||||
def customize_pandas_options():
|
||||
# The default repr depends on the settings of:
|
||||
#
|
||||
# pandas.set_option('display.max_columns', None)
|
||||
# pandas.set_option('display.max_rows', None)
|
||||
#
|
||||
# which can make the repr **very** slow on some cases, so, we customize pandas to have
|
||||
# smaller values if the current values are too big.
|
||||
custom_options = []
|
||||
|
||||
from pandas import get_option
|
||||
|
||||
max_rows = get_option("display.max_rows")
|
||||
max_cols = get_option("display.max_columns")
|
||||
max_colwidth = get_option("display.max_colwidth")
|
||||
|
||||
if max_rows is None or max_rows > PANDAS_MAX_ROWS:
|
||||
custom_options.append("display.max_rows")
|
||||
custom_options.append(PANDAS_MAX_ROWS)
|
||||
|
||||
if max_cols is None or max_cols > PANDAS_MAX_COLS:
|
||||
custom_options.append("display.max_columns")
|
||||
custom_options.append(PANDAS_MAX_COLS)
|
||||
|
||||
if max_colwidth is None or max_colwidth > PANDAS_MAX_COLWIDTH:
|
||||
custom_options.append("display.max_colwidth")
|
||||
custom_options.append(PANDAS_MAX_COLWIDTH)
|
||||
|
||||
if custom_options:
|
||||
from pandas import option_context
|
||||
with option_context(*custom_options):
|
||||
yield
|
||||
else:
|
||||
yield
|
||||
|
||||
|
||||
class PandasDataFrameTypeResolveProvider(object):
|
||||
|
||||
def can_provide(self, type_object, type_name):
|
||||
data_frame_class = find_mod_attr('pandas.core.frame', 'DataFrame')
|
||||
return data_frame_class is not None and issubclass(type_object, data_frame_class)
|
||||
|
||||
def resolve(self, obj, attribute):
|
||||
return getattr(obj, attribute)
|
||||
|
||||
def get_dictionary(self, obj):
|
||||
replacements = {
|
||||
# This actually calls: DataFrame.transpose(), which can be expensive, so,
|
||||
# let's just add some string representation for it.
|
||||
'T': '<transposed dataframe -- debugger:skipped eval>',
|
||||
|
||||
# This creates a whole new dict{index: Series) for each column. Doing a
|
||||
# subsequent repr() from this dict can be very slow, so, don't return it.
|
||||
'_series': '<dict[index:Series] -- debugger:skipped eval>',
|
||||
|
||||
'style': '<pandas.io.formats.style.Styler -- debugger: skipped eval>',
|
||||
}
|
||||
return _get_dictionary(obj, replacements)
|
||||
|
||||
def get_str_in_context(self, df, context:str):
|
||||
'''
|
||||
:param context:
|
||||
This is the context in which the variable is being requested. Valid values:
|
||||
"watch",
|
||||
"repl",
|
||||
"hover",
|
||||
"clipboard"
|
||||
'''
|
||||
if context in ('repl', 'clipboard'):
|
||||
return repr(df)
|
||||
return self.get_str(df)
|
||||
|
||||
def get_str(self, df):
|
||||
with customize_pandas_options():
|
||||
return repr(df)
|
||||
|
||||
|
||||
class PandasSeriesTypeResolveProvider(object):
|
||||
|
||||
def can_provide(self, type_object, type_name):
|
||||
series_class = find_mod_attr('pandas.core.series', 'Series')
|
||||
return series_class is not None and issubclass(type_object, series_class)
|
||||
|
||||
def resolve(self, obj, attribute):
|
||||
return getattr(obj, attribute)
|
||||
|
||||
def get_dictionary(self, obj):
|
||||
replacements = {
|
||||
# This actually calls: DataFrame.transpose(), which can be expensive, so,
|
||||
# let's just add some string representation for it.
|
||||
'T': '<transposed dataframe -- debugger:skipped eval>',
|
||||
|
||||
# This creates a whole new dict{index: Series) for each column. Doing a
|
||||
# subsequent repr() from this dict can be very slow, so, don't return it.
|
||||
'_series': '<dict[index:Series] -- debugger:skipped eval>',
|
||||
|
||||
'style': '<pandas.io.formats.style.Styler -- debugger: skipped eval>',
|
||||
}
|
||||
return _get_dictionary(obj, replacements)
|
||||
|
||||
def get_str_in_context(self, df, context:str):
|
||||
'''
|
||||
:param context:
|
||||
This is the context in which the variable is being requested. Valid values:
|
||||
"watch",
|
||||
"repl",
|
||||
"hover",
|
||||
"clipboard"
|
||||
'''
|
||||
if context in ('repl', 'clipboard'):
|
||||
return repr(df)
|
||||
return self.get_str(df)
|
||||
|
||||
def get_str(self, series):
|
||||
with customize_pandas_options():
|
||||
return repr(series)
|
||||
|
||||
|
||||
class PandasStylerTypeResolveProvider(object):
|
||||
|
||||
def can_provide(self, type_object, type_name):
|
||||
series_class = find_mod_attr('pandas.io.formats.style', 'Styler')
|
||||
return series_class is not None and issubclass(type_object, series_class)
|
||||
|
||||
def resolve(self, obj, attribute):
|
||||
return getattr(obj, attribute)
|
||||
|
||||
def get_dictionary(self, obj):
|
||||
replacements = {
|
||||
'data': '<Styler data -- debugger:skipped eval>',
|
||||
|
||||
'__dict__': '<dict -- debugger: skipped eval>',
|
||||
}
|
||||
return _get_dictionary(obj, replacements)
|
||||
|
||||
|
||||
if not sys.platform.startswith("java"):
|
||||
TypeResolveProvider.register(PandasDataFrameTypeResolveProvider)
|
||||
StrPresentationProvider.register(PandasDataFrameTypeResolveProvider)
|
||||
|
||||
TypeResolveProvider.register(PandasSeriesTypeResolveProvider)
|
||||
StrPresentationProvider.register(PandasSeriesTypeResolveProvider)
|
||||
|
||||
TypeResolveProvider.register(PandasStylerTypeResolveProvider)
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
from _pydevd_bundle.pydevd_extension_api import StrPresentationProvider
|
||||
from .pydevd_helpers import find_mod_attr, find_class_name
|
||||
|
||||
|
||||
class DjangoFormStr(object):
|
||||
def can_provide(self, type_object, type_name):
|
||||
form_class = find_mod_attr('django.forms', 'Form')
|
||||
return form_class is not None and issubclass(type_object, form_class)
|
||||
|
||||
def get_str(self, val):
|
||||
return '%s: %r' % (find_class_name(val), val)
|
||||
|
||||
import sys
|
||||
|
||||
if not sys.platform.startswith("java"):
|
||||
StrPresentationProvider.register(DjangoFormStr)
|
||||
@@ -0,0 +1,506 @@
|
||||
from _pydevd_bundle.pydevd_constants import STATE_SUSPEND, JINJA2_SUSPEND
|
||||
from _pydevd_bundle.pydevd_comm import CMD_SET_BREAK, CMD_ADD_EXCEPTION_BREAK
|
||||
from pydevd_file_utils import canonical_normalized_path
|
||||
from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, FCode
|
||||
from _pydev_bundle import pydev_log
|
||||
from pydevd_plugins.pydevd_line_validation import LineBreakpointWithLazyValidation, ValidationInfo
|
||||
from _pydev_bundle.pydev_override import overrides
|
||||
from _pydevd_bundle.pydevd_api import PyDevdAPI
|
||||
|
||||
|
||||
class Jinja2LineBreakpoint(LineBreakpointWithLazyValidation):
|
||||
|
||||
def __init__(self, canonical_normalized_filename, breakpoint_id, line, condition, func_name, expression, hit_condition=None, is_logpoint=False):
|
||||
self.canonical_normalized_filename = canonical_normalized_filename
|
||||
LineBreakpointWithLazyValidation.__init__(self, breakpoint_id, line, condition, func_name, expression, hit_condition=hit_condition, is_logpoint=is_logpoint)
|
||||
|
||||
def __str__(self):
|
||||
return "Jinja2LineBreakpoint: %s-%d" % (self.canonical_normalized_filename, self.line)
|
||||
|
||||
|
||||
class _Jinja2ValidationInfo(ValidationInfo):
|
||||
|
||||
@overrides(ValidationInfo._collect_valid_lines_in_template_uncached)
|
||||
def _collect_valid_lines_in_template_uncached(self, template):
|
||||
lineno_mapping = _get_frame_lineno_mapping(template)
|
||||
if not lineno_mapping:
|
||||
return set()
|
||||
|
||||
return set(x[0] for x in lineno_mapping)
|
||||
|
||||
|
||||
def add_line_breakpoint(plugin, pydb, type, canonical_normalized_filename, breakpoint_id, line, condition, expression, func_name, hit_condition=None, is_logpoint=False, add_breakpoint_result=None, on_changed_breakpoint_state=None):
|
||||
if type == 'jinja2-line':
|
||||
jinja2_line_breakpoint = Jinja2LineBreakpoint(canonical_normalized_filename, breakpoint_id, line, condition, func_name, expression, hit_condition=hit_condition, is_logpoint=is_logpoint)
|
||||
if not hasattr(pydb, 'jinja2_breakpoints'):
|
||||
_init_plugin_breaks(pydb)
|
||||
|
||||
add_breakpoint_result.error_code = PyDevdAPI.ADD_BREAKPOINT_LAZY_VALIDATION
|
||||
jinja2_line_breakpoint.add_breakpoint_result = add_breakpoint_result
|
||||
jinja2_line_breakpoint.on_changed_breakpoint_state = on_changed_breakpoint_state
|
||||
|
||||
return jinja2_line_breakpoint, pydb.jinja2_breakpoints
|
||||
return None
|
||||
|
||||
|
||||
def after_breakpoints_consolidated(plugin, py_db, canonical_normalized_filename, id_to_pybreakpoint, file_to_line_to_breakpoints):
|
||||
jinja2_breakpoints_for_file = file_to_line_to_breakpoints.get(canonical_normalized_filename)
|
||||
if not jinja2_breakpoints_for_file:
|
||||
return
|
||||
|
||||
if not hasattr(py_db, 'jinja2_validation_info'):
|
||||
_init_plugin_breaks(py_db)
|
||||
|
||||
# In general we validate the breakpoints only when the template is loaded, but if the template
|
||||
# was already loaded, we can validate the breakpoints based on the last loaded value.
|
||||
py_db.jinja2_validation_info.verify_breakpoints_from_template_cached_lines(
|
||||
py_db, canonical_normalized_filename, jinja2_breakpoints_for_file)
|
||||
|
||||
|
||||
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 = {}
|
||||
|
||||
pydb.jinja2_validation_info = _Jinja2ValidationInfo()
|
||||
|
||||
|
||||
def remove_all_exception_breakpoints(plugin, pydb):
|
||||
if hasattr(pydb, 'jinja2_exception_break'):
|
||||
pydb.jinja2_exception_break = {}
|
||||
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 "__jinja_template__" in frame.f_globals and name in ("root", "loop", "macro") or name.startswith("block_"):
|
||||
return True
|
||||
return False
|
||||
except:
|
||||
pydev_log.exception()
|
||||
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
|
||||
|
||||
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 = 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(object):
|
||||
|
||||
IS_PLUGIN_FRAME = True
|
||||
|
||||
def __init__(self, frame, original_filename=None, template_lineno=None):
|
||||
|
||||
if original_filename is None:
|
||||
original_filename = _get_jinja2_template_original_filename(frame)
|
||||
|
||||
if template_lineno is None:
|
||||
template_lineno = _get_jinja2_template_line(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', original_filename)
|
||||
self.f_lineno = template_lineno
|
||||
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
|
||||
|
||||
|
||||
class Jinja2TemplateSyntaxErrorFrame(object):
|
||||
|
||||
IS_PLUGIN_FRAME = True
|
||||
|
||||
def __init__(self, frame, exception_cls_name, filename, lineno, f_locals):
|
||||
self.f_code = FCode('Jinja2 %s' % (exception_cls_name,), filename)
|
||||
self.f_lineno = lineno
|
||||
self.f_back = frame
|
||||
self.f_globals = {}
|
||||
self.f_locals = f_locals
|
||||
self.f_trace = None
|
||||
|
||||
|
||||
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_debug_info(frame):
|
||||
frame_globals = frame.f_globals
|
||||
|
||||
jinja_template = frame_globals.get('__jinja_template__')
|
||||
|
||||
if jinja_template is None:
|
||||
return None
|
||||
|
||||
return _get_frame_lineno_mapping(jinja_template)
|
||||
|
||||
|
||||
def _get_frame_lineno_mapping(jinja_template):
|
||||
'''
|
||||
:rtype: list(tuple(int,int))
|
||||
:return: list((original_line, line_in_frame))
|
||||
'''
|
||||
# _debug_info is a string with the mapping from frame line to actual line
|
||||
# i.e.: "5=13&8=14"
|
||||
_debug_info = jinja_template._debug_info
|
||||
if not _debug_info:
|
||||
# Sometimes template contains only plain text.
|
||||
return None
|
||||
|
||||
# debug_info is a list with the mapping from frame line to actual line
|
||||
# i.e.: [(5, 13), (8, 14)]
|
||||
return jinja_template.debug_info
|
||||
|
||||
|
||||
def _get_jinja2_template_line(frame):
|
||||
debug_info = _get_jinja2_template_debug_info(frame)
|
||||
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 _convert_to_str(s):
|
||||
return s
|
||||
|
||||
|
||||
def _get_jinja2_template_original_filename(frame):
|
||||
if '__jinja_template__' in frame.f_globals:
|
||||
return _convert_to_str(frame.f_globals['__jinja_template__'].filename)
|
||||
|
||||
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 _canonical_normalized_filename, breakpoints in plugin.main_debugger.jinja2_breakpoints.items():
|
||||
if len(breakpoints) > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def can_skip(plugin, pydb, frame):
|
||||
if pydb.jinja2_breakpoints and _is_jinja2_render_call(frame):
|
||||
filename = _get_jinja2_template_original_filename(frame)
|
||||
if filename is not None:
|
||||
canonical_normalized_filename = canonical_normalized_path(filename)
|
||||
jinja2_breakpoints_for_file = pydb.jinja2_breakpoints.get(canonical_normalized_filename)
|
||||
if jinja2_breakpoints_for_file:
|
||||
return False
|
||||
|
||||
if pydb.jinja2_exception_break:
|
||||
name = frame.f_code.co_name
|
||||
|
||||
# errors in compile time
|
||||
if name in ('template', 'top-level template code', '<module>') or name.startswith('block '):
|
||||
f_back = frame.f_back
|
||||
module_name = ''
|
||||
if f_back is not None:
|
||||
module_name = f_back.f_globals.get('__name__', '')
|
||||
if module_name.startswith('jinja2.'):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
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, py_db, pydb_frame, frame, event, args):
|
||||
py_db = args[0]
|
||||
_filename = args[1]
|
||||
info = args[2]
|
||||
break_type = 'jinja2'
|
||||
|
||||
if event == 'line' and info.pydev_state != STATE_SUSPEND and py_db.jinja2_breakpoints and _is_jinja2_render_call(frame):
|
||||
|
||||
jinja_template = frame.f_globals.get('__jinja_template__')
|
||||
if jinja_template is None:
|
||||
return False, None, None, break_type
|
||||
|
||||
original_filename = _get_jinja2_template_original_filename(frame)
|
||||
if original_filename is not None:
|
||||
pydev_log.debug("Jinja2 is rendering a template: %s", original_filename)
|
||||
canonical_normalized_filename = canonical_normalized_path(original_filename)
|
||||
jinja2_breakpoints_for_file = py_db.jinja2_breakpoints.get(canonical_normalized_filename)
|
||||
|
||||
if jinja2_breakpoints_for_file:
|
||||
|
||||
jinja2_validation_info = py_db.jinja2_validation_info
|
||||
jinja2_validation_info.verify_breakpoints(py_db, canonical_normalized_filename, jinja2_breakpoints_for_file, jinja_template)
|
||||
|
||||
template_lineno = _get_jinja2_template_line(frame)
|
||||
if template_lineno is not None:
|
||||
jinja2_breakpoint = jinja2_breakpoints_for_file.get(template_lineno)
|
||||
if jinja2_breakpoint is not None:
|
||||
new_frame = Jinja2TemplateFrame(frame, original_filename, template_lineno)
|
||||
return True, jinja2_breakpoint, new_frame, break_type
|
||||
|
||||
return False, None, None, break_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 and exception is not None:
|
||||
exception_type = list(pydb.jinja2_exception_break.keys())[0]
|
||||
if exception.__name__ 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))
|
||||
suspend_frame.f_back = frame
|
||||
frame = suspend_frame
|
||||
return True, frame
|
||||
|
||||
elif exception.__name__ in ('TemplateSyntaxError', 'TemplateAssertionError'):
|
||||
name = frame.f_code.co_name
|
||||
|
||||
# errors in compile time
|
||||
if name in ('template', 'top-level template code', '<module>') or name.startswith('block '):
|
||||
|
||||
f_back = frame.f_back
|
||||
if f_back is not None:
|
||||
module_name = f_back.f_globals.get('__name__', '')
|
||||
|
||||
if module_name.startswith('jinja2.'):
|
||||
# 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)
|
||||
return True, frame
|
||||
return None
|
||||
@@ -0,0 +1,107 @@
|
||||
from _pydevd_bundle.pydevd_breakpoints import LineBreakpoint
|
||||
from _pydevd_bundle.pydevd_api import PyDevdAPI
|
||||
import bisect
|
||||
from _pydev_bundle import pydev_log
|
||||
|
||||
|
||||
class LineBreakpointWithLazyValidation(LineBreakpoint):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
LineBreakpoint.__init__(self, *args, **kwargs)
|
||||
# This is the _AddBreakpointResult that'll be modified (and then re-sent on the
|
||||
# on_changed_breakpoint_state).
|
||||
self.add_breakpoint_result = None
|
||||
|
||||
# The signature for the callback should be:
|
||||
# on_changed_breakpoint_state(breakpoint_id: int, add_breakpoint_result: _AddBreakpointResult)
|
||||
self.on_changed_breakpoint_state = None
|
||||
|
||||
# When its state is checked (in which case it'd call on_changed_breakpoint_state if the
|
||||
# state changed), we store a cache key in 'verified_cache_key' -- in case it changes
|
||||
# we'd need to re-verify it (for instance, the template could have changed on disk).
|
||||
self.verified_cache_key = None
|
||||
|
||||
|
||||
class ValidationInfo(object):
|
||||
|
||||
def __init__(self):
|
||||
self._canonical_normalized_filename_to_last_template_lines = {}
|
||||
|
||||
def _collect_valid_lines_in_template(self, template):
|
||||
# We cache the lines in the template itself. Note that among requests the
|
||||
# template may be a different instance (because the template contents could be
|
||||
# changed on disk), but this may still be called multiple times during the
|
||||
# same render session, so, caching is interesting.
|
||||
lines_cache = getattr(template, '__pydevd_lines_cache__', None)
|
||||
if lines_cache is not None:
|
||||
lines, sorted_lines = lines_cache
|
||||
return lines, sorted_lines
|
||||
|
||||
lines = self._collect_valid_lines_in_template_uncached(template)
|
||||
|
||||
lines = frozenset(lines)
|
||||
sorted_lines = tuple(sorted(lines))
|
||||
template.__pydevd_lines_cache__ = lines, sorted_lines
|
||||
return lines, sorted_lines
|
||||
|
||||
def _collect_valid_lines_in_template_uncached(self, template):
|
||||
raise NotImplementedError()
|
||||
|
||||
def verify_breakpoints(self, py_db, canonical_normalized_filename, template_breakpoints_for_file, template):
|
||||
'''
|
||||
This function should be called whenever a rendering is detected.
|
||||
|
||||
:param str canonical_normalized_filename:
|
||||
:param dict[int:LineBreakpointWithLazyValidation] template_breakpoints_for_file:
|
||||
'''
|
||||
valid_lines_frozenset, sorted_lines = self._collect_valid_lines_in_template(template)
|
||||
|
||||
self._canonical_normalized_filename_to_last_template_lines[canonical_normalized_filename] = valid_lines_frozenset, sorted_lines
|
||||
self._verify_breakpoints_with_lines_collected(py_db, canonical_normalized_filename, template_breakpoints_for_file, valid_lines_frozenset, sorted_lines)
|
||||
|
||||
def verify_breakpoints_from_template_cached_lines(self, py_db, canonical_normalized_filename, template_breakpoints_for_file):
|
||||
'''
|
||||
This is used when the lines are already available (if just the template is available,
|
||||
`verify_breakpoints` should be used instead).
|
||||
'''
|
||||
cached = self._canonical_normalized_filename_to_last_template_lines.get(canonical_normalized_filename)
|
||||
if cached is not None:
|
||||
valid_lines_frozenset, sorted_lines = cached
|
||||
self._verify_breakpoints_with_lines_collected(py_db, canonical_normalized_filename, template_breakpoints_for_file, valid_lines_frozenset, sorted_lines)
|
||||
|
||||
def _verify_breakpoints_with_lines_collected(self, py_db, canonical_normalized_filename, template_breakpoints_for_file, valid_lines_frozenset, sorted_lines):
|
||||
for line, template_bp in list(template_breakpoints_for_file.items()): # Note: iterate in a copy (we may mutate it).
|
||||
if template_bp.verified_cache_key != valid_lines_frozenset:
|
||||
template_bp.verified_cache_key = valid_lines_frozenset
|
||||
valid = line in valid_lines_frozenset
|
||||
|
||||
if not valid:
|
||||
new_line = -1
|
||||
if sorted_lines:
|
||||
# Adjust to the first preceding valid line.
|
||||
idx = bisect.bisect_left(sorted_lines, line)
|
||||
if idx > 0:
|
||||
new_line = sorted_lines[idx - 1]
|
||||
|
||||
if new_line >= 0 and new_line not in template_breakpoints_for_file:
|
||||
# We just add it if found and if there's no existing breakpoint at that
|
||||
# location.
|
||||
if template_bp.add_breakpoint_result.error_code != PyDevdAPI.ADD_BREAKPOINT_NO_ERROR and template_bp.add_breakpoint_result.translated_line != new_line:
|
||||
pydev_log.debug('Template breakpoint in %s in line: %s moved to line: %s', canonical_normalized_filename, line, new_line)
|
||||
template_bp.add_breakpoint_result.error_code = PyDevdAPI.ADD_BREAKPOINT_NO_ERROR
|
||||
template_bp.add_breakpoint_result.translated_line = new_line
|
||||
|
||||
# Add it to a new line.
|
||||
template_breakpoints_for_file.pop(line, None)
|
||||
template_breakpoints_for_file[new_line] = template_bp
|
||||
template_bp.on_changed_breakpoint_state(template_bp.breakpoint_id, template_bp.add_breakpoint_result)
|
||||
else:
|
||||
if template_bp.add_breakpoint_result.error_code != PyDevdAPI.ADD_BREAKPOINT_INVALID_LINE:
|
||||
pydev_log.debug('Template breakpoint in %s in line: %s invalid (valid lines: %s)', canonical_normalized_filename, line, valid_lines_frozenset)
|
||||
template_bp.add_breakpoint_result.error_code = PyDevdAPI.ADD_BREAKPOINT_INVALID_LINE
|
||||
template_bp.on_changed_breakpoint_state(template_bp.breakpoint_id, template_bp.add_breakpoint_result)
|
||||
else:
|
||||
if template_bp.add_breakpoint_result.error_code != PyDevdAPI.ADD_BREAKPOINT_NO_ERROR:
|
||||
template_bp.add_breakpoint_result.error_code = PyDevdAPI.ADD_BREAKPOINT_NO_ERROR
|
||||
template_bp.on_changed_breakpoint_state(template_bp.breakpoint_id, template_bp.add_breakpoint_result)
|
||||
|
||||
Reference in New Issue
Block a user