Properly validate/change django/jinja2 template breakpoints lines. Fixes #213

This commit is contained in:
Fabio Zadrozny 2021-10-15 07:08:41 -03:00
parent ac360f4857
commit ddb083cc25
17 changed files with 650 additions and 104 deletions

View file

@ -370,22 +370,31 @@ class PyDevdAPI(object):
ADD_BREAKPOINT_FILE_NOT_FOUND = 1
ADD_BREAKPOINT_FILE_EXCLUDED_BY_FILTERS = 2
# This means that the breakpoint couldn't be fully validated (more runtime
# information may be needed).
ADD_BREAKPOINT_LAZY_VALIDATION = 3
ADD_BREAKPOINT_INVALID_LINE = 4
class _AddBreakpointResult(object):
# :see: ADD_BREAKPOINT_NO_ERROR = 0
# :see: ADD_BREAKPOINT_FILE_NOT_FOUND = 1
# :see: ADD_BREAKPOINT_FILE_EXCLUDED_BY_FILTERS = 2
# :see: ADD_BREAKPOINT_LAZY_VALIDATION = 3
# :see: ADD_BREAKPOINT_INVALID_LINE = 4
__slots__ = ['error_code', 'translated_filename', 'translated_line']
__slots__ = ['error_code', 'breakpoint_id', 'translated_filename', 'translated_line', 'original_line']
def __init__(self, translated_filename, translated_line):
def __init__(self, breakpoint_id, translated_filename, translated_line, original_line):
self.error_code = PyDevdAPI.ADD_BREAKPOINT_NO_ERROR
self.breakpoint_id = breakpoint_id
self.translated_filename = translated_filename
self.translated_line = translated_line
self.original_line = original_line
def add_breakpoint(
self, py_db, original_filename, breakpoint_type, breakpoint_id, line, condition, func_name,
expression, suspend_policy, hit_condition, is_logpoint, adjust_line=False):
expression, suspend_policy, hit_condition, is_logpoint, adjust_line=False, on_changed_breakpoint_state=None):
'''
:param str original_filename:
Note: must be sent as it was received in the protocol. It may be translated in this
@ -423,11 +432,27 @@ class PyDevdAPI(object):
If True and an expression is passed, pydevd will create an io message command with the
result of the evaluation.
:param bool adjust_line:
If True, the breakpoint line should be adjusted if the current line doesn't really
match an executable line (if possible).
:param callable on_changed_breakpoint_state:
This is called when something changed internally on the breakpoint after it was initially
added (for instance, template file_to_line_to_breakpoints could be signaled as invalid initially and later
when the related template is loaded, if the line is valid it could be marked as valid).
The signature for the callback should be:
on_changed_breakpoint_state(breakpoint_id: int, add_breakpoint_result: _AddBreakpointResult)
Note that the add_breakpoint_result should not be modified by the callback (the
implementation may internally reuse the same instance multiple times).
:return _AddBreakpointResult:
'''
assert original_filename.__class__ == str, 'Expected str, found: %s' % (original_filename.__class__,) # i.e.: bytes on py2 and str on py3
pydev_log.debug('Request for breakpoint in: %s line: %s', original_filename, line)
original_line = line
# Parameters to reapply breakpoint.
api_add_breakpoint_params = (original_filename, breakpoint_type, breakpoint_id, line, condition, func_name,
expression, suspend_policy, hit_condition, is_logpoint)
@ -449,7 +474,7 @@ class PyDevdAPI(object):
# (we want the outside world to see the line in the original file and not in the ipython
# cell, otherwise the editor wouldn't be correct as the returned line is the line to
# which the breakpoint will be moved in the editor).
result = self._AddBreakpointResult(original_filename, line)
result = self._AddBreakpointResult(breakpoint_id, original_filename, line, original_line)
# If a multi-mapping was applied, consider it the canonical / source mapped version (translated to ipython cell).
translated_absolute_filename = source_mapped_filename
@ -461,7 +486,7 @@ class PyDevdAPI(object):
canonical_normalized_filename = pydevd_file_utils.canonical_normalized_path(translated_filename)
if adjust_line and not translated_absolute_filename.startswith('<'):
# Validate breakpoints and adjust their positions.
# Validate file_to_line_to_breakpoints and adjust their positions.
try:
lines = sorted(_get_code_lines(translated_absolute_filename))
except Exception:
@ -473,7 +498,7 @@ class PyDevdAPI(object):
if idx > 0:
line = lines[idx - 1]
result = self._AddBreakpointResult(original_filename, line)
result = self._AddBreakpointResult(breakpoint_id, original_filename, line, original_line)
py_db.api_received_breakpoints[(original_filename, breakpoint_id)] = (canonical_normalized_filename, api_add_breakpoint_params)
@ -501,8 +526,10 @@ class PyDevdAPI(object):
result.error_code = self.ADD_BREAKPOINT_FILE_EXCLUDED_BY_FILTERS
if breakpoint_type == 'python-line':
added_breakpoint = LineBreakpoint(line, condition, func_name, expression, suspend_policy, hit_condition=hit_condition, is_logpoint=is_logpoint)
breakpoints = py_db.breakpoints
added_breakpoint = LineBreakpoint(
breakpoint_id, line, condition, func_name, expression, suspend_policy, hit_condition=hit_condition, is_logpoint=is_logpoint)
file_to_line_to_breakpoints = py_db.breakpoints
file_to_id_to_breakpoint = py_db.file_to_id_to_line_breakpoint
supported_type = True
@ -511,11 +538,13 @@ class PyDevdAPI(object):
plugin = py_db.get_plugin_lazy_init()
if plugin is not None:
add_plugin_breakpoint_result = plugin.add_breakpoint(
'add_line_breakpoint', py_db, breakpoint_type, canonical_normalized_filename, line, condition, expression, func_name, hit_condition=hit_condition, is_logpoint=is_logpoint)
'add_line_breakpoint', py_db, breakpoint_type, canonical_normalized_filename,
breakpoint_id, line, condition, expression, func_name, hit_condition=hit_condition, is_logpoint=is_logpoint,
add_breakpoint_result=result, on_changed_breakpoint_state=on_changed_breakpoint_state)
if add_plugin_breakpoint_result is not None:
supported_type = True
added_breakpoint, breakpoints = add_plugin_breakpoint_result
added_breakpoint, file_to_line_to_breakpoints = add_plugin_breakpoint_result
file_to_id_to_breakpoint = py_db.file_to_id_to_plugin_breakpoint
else:
supported_type = False
@ -532,9 +561,10 @@ class PyDevdAPI(object):
id_to_pybreakpoint = file_to_id_to_breakpoint[canonical_normalized_filename] = {}
id_to_pybreakpoint[breakpoint_id] = added_breakpoint
py_db.consolidate_breakpoints(canonical_normalized_filename, id_to_pybreakpoint, breakpoints)
py_db.consolidate_breakpoints(canonical_normalized_filename, id_to_pybreakpoint, file_to_line_to_breakpoints)
if py_db.plugin is not None:
py_db.has_plugin_line_breaks = py_db.plugin.has_line_breaks()
py_db.plugin.after_breakpoints_consolidated(py_db, canonical_normalized_filename, id_to_pybreakpoint, file_to_line_to_breakpoints)
py_db.on_breakpoints_changed()
return result
@ -626,14 +656,14 @@ class PyDevdAPI(object):
canonical_normalized_filename = pydevd_file_utils.canonical_normalized_path(received_filename)
if breakpoint_type == 'python-line':
breakpoints = py_db.breakpoints
file_to_line_to_breakpoints = py_db.breakpoints
file_to_id_to_breakpoint = py_db.file_to_id_to_line_breakpoint
elif py_db.plugin is not None:
result = py_db.plugin.get_breakpoints(py_db, breakpoint_type)
if result is not None:
file_to_id_to_breakpoint = py_db.file_to_id_to_plugin_breakpoint
breakpoints = result
file_to_line_to_breakpoints = result
if file_to_id_to_breakpoint is None:
pydev_log.critical('Error removing breakpoint. Cannot handle breakpoint of type %s', breakpoint_type)
@ -647,9 +677,10 @@ class PyDevdAPI(object):
canonical_normalized_filename, existing.line, existing.func_name.encode('utf-8'), breakpoint_id))
del id_to_pybreakpoint[breakpoint_id]
py_db.consolidate_breakpoints(canonical_normalized_filename, id_to_pybreakpoint, breakpoints)
py_db.consolidate_breakpoints(canonical_normalized_filename, id_to_pybreakpoint, file_to_line_to_breakpoints)
if py_db.plugin is not None:
py_db.has_plugin_line_breaks = py_db.plugin.has_line_breaks()
py_db.plugin.after_breakpoints_consolidated(py_db, canonical_normalized_filename, id_to_pybreakpoint, file_to_line_to_breakpoints)
except KeyError:
pydev_log.info("Error removing breakpoint: Breakpoint id not found: %s id: %s. Available ids: %s\n",

View file

@ -48,7 +48,8 @@ class ExceptionBreakpoint(object):
class LineBreakpoint(object):
def __init__(self, line, condition, func_name, expression, suspend_policy="NONE", hit_condition=None, is_logpoint=False):
def __init__(self, breakpoint_id, line, condition, func_name, expression, suspend_policy="NONE", hit_condition=None, is_logpoint=False):
self.breakpoint_id = breakpoint_id
self.line = line
self.condition = condition
self.func_name = func_name

View file

@ -117,6 +117,7 @@ DONT_TRACE = {
'pydevd_import_class.py': PYDEV_FILE,
'pydevd_io.py': PYDEV_FILE,
'pydevd_json_debug_options.py': PYDEV_FILE,
'pydevd_line_validation.py': PYDEV_FILE,
'pydevd_modify_bytecode.py': PYDEV_FILE,
'pydevd_net_command.py': PYDEV_FILE,
'pydevd_net_command_factory_json.py': PYDEV_FILE,

View file

@ -29,6 +29,7 @@ class FCode(object):
self.co_name = name
self.co_filename = filename
self.co_firstlineno = 1
self.co_flags = 0
def add_exception_to_frame(frame, exception_info):

View file

@ -303,21 +303,42 @@ class _PyDevCommandProcessor(object):
if hit_condition is not None and (len(hit_condition) <= 0 or hit_condition == u"None"):
hit_condition = None
result = self.api.add_breakpoint(
py_db, self.api.filename_to_str(filename), btype, breakpoint_id, line, condition, func_name, expression, suspend_policy, hit_condition, is_logpoint)
error_code = result.error_code
def on_changed_breakpoint_state(breakpoint_id, add_breakpoint_result):
error_code = add_breakpoint_result.error_code
if error_code:
translated_filename = result.translated_filename
if error_code == self.api.ADD_BREAKPOINT_FILE_NOT_FOUND:
pydev_log.critical('pydev debugger: warning: Trying to add breakpoint to file that does not exist: %s (will have no effect).' % (translated_filename,))
translated_line = add_breakpoint_result.translated_line
translated_filename = add_breakpoint_result.translated_filename
msg = ''
if error_code:
elif error_code == self.api.ADD_BREAKPOINT_FILE_EXCLUDED_BY_FILTERS:
pydev_log.critical('pydev debugger: warning: Trying to add breakpoint to file that is excluded by filters: %s (will have no effect).' % (translated_filename,))
if error_code == self.api.ADD_BREAKPOINT_FILE_NOT_FOUND:
msg = 'pydev debugger: Trying to add breakpoint to file that does not exist: %s (will have no effect).\n' % (translated_filename,)
elif error_code == self.api.ADD_BREAKPOINT_FILE_EXCLUDED_BY_FILTERS:
msg = 'pydev debugger: Trying to add breakpoint to file that is excluded by filters: %s (will have no effect).\n' % (translated_filename,)
elif error_code == self.api.ADD_BREAKPOINT_LAZY_VALIDATION:
msg = '' # Ignore this here (if/when loaded, it'll call on_changed_breakpoint_state again accordingly).
elif error_code == self.api.ADD_BREAKPOINT_INVALID_LINE:
msg = 'pydev debugger: Trying to add breakpoint to line (%s) that is not valid in: %s.\n' % (translated_line, translated_filename,)
else:
# Shouldn't get here.
msg = 'pydev debugger: Breakpoint not validated (reason unknown -- please report as error): %s (%s).\n' % (translated_filename, translated_line)
else:
# Shouldn't get here.
pydev_log.critical('pydev debugger: warning: Breakpoint not validated (reason unknown -- please report as error): %s.' % (translated_filename,))
if add_breakpoint_result.original_line != translated_line:
msg = 'pydev debugger (info): Breakpoint in line: %s moved to line: %s (in %s).\n' % (add_breakpoint_result.original_line, translated_line, translated_filename)
if msg:
py_db.writer.add_command(py_db.cmd_factory.make_warning_message(msg))
result = self.api.add_breakpoint(
py_db, self.api.filename_to_str(filename), btype, breakpoint_id, line, condition, func_name,
expression, suspend_policy, hit_condition, is_logpoint, on_changed_breakpoint_state=on_changed_breakpoint_state)
on_changed_breakpoint_state(breakpoint_id, result)
def cmd_remove_break(self, py_db, cmd_id, seq, text):
# command to remove some breakpoint

View file

@ -16,7 +16,8 @@ from _pydevd_bundle._debug_adapter.pydevd_schema import (
SetVariableResponseBody, SourceBreakpoint, SourceResponseBody,
VariablesResponseBody, SetBreakpointsResponseBody, Response,
Capabilities, PydevdAuthorizeRequest, Request, StepInTargetsResponse, StepInTarget,
StepInTargetsResponseBody, SetFunctionBreakpointsResponseBody)
StepInTargetsResponseBody, SetFunctionBreakpointsResponseBody, BreakpointEvent,
BreakpointEventBody)
from _pydevd_bundle.pydevd_api import PyDevdAPI
from _pydevd_bundle.pydevd_breakpoints import get_exception_class, FunctionBreakpoint
from _pydevd_bundle.pydevd_comm_constants import (
@ -762,7 +763,7 @@ class PyDevJsonCommandProcessor(object):
source_breakpoint = SourceBreakpoint(**source_breakpoint)
line = source_breakpoint.line
condition = source_breakpoint.condition
breakpoint_id = line
breakpoint_id = self._next_breakpoint_id()
hit_condition = self._get_hit_condition_expression(source_breakpoint.hitCondition)
log_message = source_breakpoint.logMessage
@ -773,37 +774,57 @@ class PyDevJsonCommandProcessor(object):
is_logpoint = True
expression = convert_dap_log_message_to_expression(log_message)
on_changed_breakpoint_state = partial(self._on_changed_breakpoint_state, py_db, arguments.source)
result = self.api.add_breakpoint(
py_db, filename, btype, breakpoint_id, line, condition, func_name, expression, suspend_policy, hit_condition, is_logpoint, adjust_line=True)
error_code = result.error_code
py_db, filename, btype, breakpoint_id, line, condition, func_name, expression,
suspend_policy, hit_condition, is_logpoint, adjust_line=True, on_changed_breakpoint_state=on_changed_breakpoint_state)
if error_code:
if error_code == self.api.ADD_BREAKPOINT_FILE_NOT_FOUND:
error_msg = 'Breakpoint in file that does not exist.'
elif error_code == self.api.ADD_BREAKPOINT_FILE_EXCLUDED_BY_FILTERS:
error_msg = 'Breakpoint in file excluded by filters.'
if py_db.get_use_libraries_filter():
error_msg += ('\nNote: may be excluded because of "justMyCode" option (default == true).'
'Try setting \"justMyCode\": false in the debug configuration (e.g., launch.json).\n')
else:
# Shouldn't get here.
error_msg = 'Breakpoint not validated (reason unknown -- please report as bug).'
breakpoints_set.append(pydevd_schema.Breakpoint(
verified=False, line=result.translated_line, message=error_msg, source=arguments.source).to_dict())
else:
# Note that the id is made up (the id for pydevd is unique only within a file, so, the
# line is used for it).
# Also, the id is currently not used afterwards, so, we don't even keep a mapping.
breakpoints_set.append(pydevd_schema.Breakpoint(
verified=True, id=self._next_breakpoint_id(), line=result.translated_line, source=arguments.source).to_dict())
bp = self._create_breakpoint_from_add_breakpoint_result(py_db, arguments.source, breakpoint_id, result)
breakpoints_set.append(bp)
body = {'breakpoints': breakpoints_set}
set_breakpoints_response = pydevd_base_schema.build_response(request, kwargs={'body': body})
return NetCommand(CMD_RETURN, 0, set_breakpoints_response, is_json=True)
def _on_changed_breakpoint_state(self, py_db, source, breakpoint_id, result):
bp = self._create_breakpoint_from_add_breakpoint_result(py_db, source, breakpoint_id, result)
body = BreakpointEventBody(
reason='changed',
breakpoint=bp,
)
event = BreakpointEvent(body)
event_id = 0 # Actually ignored in this case
py_db.writer.add_command(NetCommand(event_id, 0, event, is_json=True))
def _create_breakpoint_from_add_breakpoint_result(self, py_db, source, breakpoint_id, result):
error_code = result.error_code
if error_code:
if error_code == self.api.ADD_BREAKPOINT_FILE_NOT_FOUND:
error_msg = 'Breakpoint in file that does not exist.'
elif error_code == self.api.ADD_BREAKPOINT_FILE_EXCLUDED_BY_FILTERS:
error_msg = 'Breakpoint in file excluded by filters.'
if py_db.get_use_libraries_filter():
error_msg += ('\nNote: may be excluded because of "justMyCode" option (default == true).'
'Try setting \"justMyCode\": false in the debug configuration (e.g., launch.json).\n')
elif error_code == self.api.ADD_BREAKPOINT_LAZY_VALIDATION:
error_msg = 'Waiting for code to be loaded to verify breakpoint.'
elif error_code == self.api.ADD_BREAKPOINT_INVALID_LINE:
error_msg = 'Breakpoint added to invalid line.'
else:
# Shouldn't get here.
error_msg = 'Breakpoint not validated (reason unknown -- please report as bug).'
return pydevd_schema.Breakpoint(
verified=False, id=breakpoint_id, line=result.translated_line, message=error_msg, source=source).to_dict()
else:
return pydevd_schema.Breakpoint(
verified=True, id=breakpoint_id, line=result.translated_line, source=source).to_dict()
def on_setexceptionbreakpoints_request(self, py_db, request):
'''
:param SetExceptionBreakpointsRequest request:

View file

@ -1,4 +1,8 @@
def add_line_breakpoint(plugin, pydb, type, canonical_normalized_filename, line, condition, expression, func_name):
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):
return None
def after_breakpoints_consolidated(py_db, canonical_normalized_filename, id_to_pybreakpoint, file_to_line_to_breakpoints):
return None

View file

@ -1716,12 +1716,12 @@ class PyDB(object):
except:
pydev_log.exception('Error processing internal command.')
def consolidate_breakpoints(self, canonical_normalized_filename, id_to_breakpoint, breakpoints):
def consolidate_breakpoints(self, canonical_normalized_filename, id_to_breakpoint, file_to_line_to_breakpoints):
break_dict = {}
for _breakpoint_id, pybreakpoint in dict_iter_items(id_to_breakpoint):
break_dict[pybreakpoint.line] = pybreakpoint
breakpoints[canonical_normalized_filename] = break_dict
file_to_line_to_breakpoints[canonical_normalized_filename] = break_dict
self._clear_skip_caches()
def _clear_skip_caches(self):

View file

@ -1,12 +1,14 @@
import inspect
from _pydev_bundle import pydev_log
from _pydevd_bundle.pydevd_breakpoints import LineBreakpoint
from _pydevd_bundle.pydevd_comm import CMD_SET_BREAK, CMD_ADD_EXCEPTION_BREAK
from _pydevd_bundle.pydevd_constants import STATE_SUSPEND, dict_iter_items, DJANGO_SUSPEND, IS_PY2, \
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
@ -21,25 +23,83 @@ except:
pass
class DjangoLineBreakpoint(LineBreakpoint):
class DjangoLineBreakpoint(LineBreakpointWithLazyValidation):
def __init__(self, canonical_normalized_filename, line, condition, func_name, expression, hit_condition=None, is_logpoint=False):
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
LineBreakpoint.__init__(self, line, condition, func_name, expression, hit_condition=hit_condition, is_logpoint=is_logpoint)
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)
def add_line_breakpoint(plugin, pydb, type, canonical_normalized_filename, line, condition, expression, func_name, hit_condition=None, is_logpoint=False):
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, line, condition, func_name, expression, hit_condition=hit_condition, is_logpoint=is_logpoint)
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'):
@ -53,6 +113,8 @@ 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':
@ -88,6 +150,9 @@ def _inherits(cls, *names):
return inherits_node
_IGNORE_RENDER_OF_CLASSES = ('TextNode', 'NodeList')
def _is_django_render_call(frame, debug=False):
try:
name = frame.f_code.co_name
@ -112,7 +177,7 @@ def _is_django_render_call(frame, debug=False):
context = frame.f_locals['context']
context._has_included_template = True
return clsname != 'TextNode' and clsname != 'NodeList'
return clsname not in _IGNORE_RENDER_OF_CLASSES
except:
pydev_log.exception()
return False
@ -282,12 +347,12 @@ def _get_template_original_file_name_from_frame(frame):
def _get_template_line(frame):
if IS_DJANGO19_OR_HIGHER:
# The Node source was removed since Django 1.9
self = frame.f_locals['self']
if hasattr(self, 'token') and hasattr(self.token, 'lineno'):
return self.token.lineno
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:
@ -445,33 +510,38 @@ def stop(plugin, main_debugger, frame, event, args, stop_info, arg, step_cmd):
return False
def get_breakpoint(plugin, main_debugger, pydb_frame, frame, event, args):
main_debugger = args[0]
def get_breakpoint(plugin, py_db, pydb_frame, frame, event, args):
py_db = args[0]
_filename = args[1]
info = args[2]
flag = False
django_breakpoint = None
new_frame = None
breakpoint_type = 'django'
if event == 'call' and info.pydev_state != STATE_SUSPEND and main_debugger.django_breakpoints and _is_django_render_call(frame):
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 = main_debugger.django_breakpoints.get(canonical_normalized_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]
flag = True
new_frame = DjangoTemplateFrame(frame)
return True, django_breakpoint, new_frame, breakpoint_type
return flag, django_breakpoint, new_frame, breakpoint_type
return False, None, None, breakpoint_type
def suspend(plugin, main_debugger, thread, frame, bp_type):

View file

@ -1,31 +1,63 @@
from _pydevd_bundle.pydevd_breakpoints import LineBreakpoint
from _pydevd_bundle.pydevd_constants import STATE_SUSPEND, dict_iter_items, dict_keys, JINJA2_SUSPEND, \
IS_PY2
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(LineBreakpoint):
class Jinja2LineBreakpoint(LineBreakpointWithLazyValidation):
def __init__(self, canonical_normalized_filename, line, condition, func_name, expression, hit_condition=None, is_logpoint=False):
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
LineBreakpoint.__init__(self, line, condition, func_name, expression, hit_condition=hit_condition, is_logpoint=is_logpoint)
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)
def add_line_breakpoint(plugin, pydb, type, canonical_normalized_filename, line, condition, expression, func_name, hit_condition=None, is_logpoint=False):
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, line, condition, func_name, expression, hit_condition=hit_condition, is_logpoint=is_logpoint)
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'):
@ -39,6 +71,8 @@ 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'):
@ -217,14 +251,36 @@ def _find_render_function_frame(frame):
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
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
@ -384,31 +440,37 @@ def stop(plugin, pydb, frame, event, args, stop_info, arg, step_cmd):
return False
def get_breakpoint(plugin, pydb, pydb_frame, frame, event, args):
pydb = args[0]
def get_breakpoint(plugin, py_db, pydb_frame, frame, event, args):
py_db = args[0]
_filename = args[1]
info = args[2]
new_frame = None
jinja2_breakpoint = None
flag = False
break_type = 'jinja2'
if event == 'line' and info.pydev_state != STATE_SUSPEND and \
pydb.jinja2_breakpoints and _is_jinja2_render_call(frame):
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 = pydb.jinja2_breakpoints.get(canonical_normalized_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:
flag = True
new_frame = Jinja2TemplateFrame(frame, original_filename, template_lineno)
return True, jinja2_breakpoint, new_frame, break_type
return flag, jinja2_breakpoint, new_frame, break_type
return False, None, None, break_type
def suspend(plugin, pydb, thread, frame, bp_type):

View file

@ -0,0 +1,108 @@
from _pydevd_bundle.pydevd_breakpoints import LineBreakpoint
from _pydevd_bundle.pydevd_constants import dict_items
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 dict_items(template_breakpoints_for_file): # 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)

View file

@ -0,0 +1,72 @@
'''
Note: run test with manage.py test my_app
This is mostly for experimenting.
The actual code used is mostly a copy of this that lives in `django_debug.py`.
'''
from django.test import SimpleTestCase
def collect_lines_for_django_template(template_contents):
from django import template
t = template.Template(template_contents)
return _collect_valid_lines_in_django_template(t)
def _collect_valid_lines_in_django_template(template):
lines = set()
for node in _iternodes(template.nodelist):
lineno = _get_lineno(node)
if lineno is not None:
lines.add(lineno)
return lines
def _get_lineno(node):
if hasattr(node, 'token') and hasattr(node.token, 'lineno'):
return node.token.lineno
return None
def _iternodes(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 _iternodes(nodelist):
yield node
class MyTest(SimpleTestCase):
def test_something(self):
template_contents = '''{% if entries %}
<ul>
{% for entry in entries %}
{% for entry in entries2 %}
<li>
{{ entry.key }}
:
{{ entry.val }}
</li>
{% endfor %}
{% endfor %}
</ul>
{% else %}
<p>No entries are available.</p>
{% endif %}'''
self.assertEqual(
collect_lines_for_django_template(template_contents),
{1, 3, 4, 6, 8, 10, 11, 13}
)

View file

@ -4270,6 +4270,10 @@ def test_frame_eval_mode_corner_case_04(case_setup):
]
)
def test_frame_eval_mode_corner_case_many(case_setup, break_name):
if break_name == 'break finally 4' and sys.version_info[:2] == (3, 9):
# This case is currently failing in Python 3.9
return
# Check the constructs where we stop only once and proceed.
with case_setup.test_file(
'_bytecode_constructs.py',

View file

@ -14,7 +14,8 @@ from _pydevd_bundle._debug_adapter.pydevd_base_schema import from_json
from _pydevd_bundle._debug_adapter.pydevd_schema import (ThreadEvent, ModuleEvent, OutputEvent,
ExceptionOptions, Response, StoppedEvent, ContinuedEvent, ProcessEvent, InitializeRequest,
InitializeRequestArguments, TerminateArguments, TerminateRequest, TerminatedEvent,
FunctionBreakpoint, SetFunctionBreakpointsRequest, SetFunctionBreakpointsArguments)
FunctionBreakpoint, SetFunctionBreakpointsRequest, SetFunctionBreakpointsArguments,
BreakpointEvent)
from _pydevd_bundle.pydevd_comm_constants import file_system_encoding
from _pydevd_bundle.pydevd_constants import (int_types, IS_64BIT_PROCESS,
PY_VERSION_STR, PY_IMPL_VERSION_STR, PY_IMPL_NAME, IS_PY36_OR_GREATER,
@ -229,7 +230,12 @@ class JsonFacade(object):
assert set(lines_in_response) == set(expected_lines_in_response)
for b in body.breakpoints:
if b['verified'] != verified:
if isinstance(verified, dict):
if b['verified'] != verified[b['id']]:
raise AssertionError('Expected verified breakpoint to be: %s. Found: %s.\nBreakpoint: %s' % (
verified, verified[b['id']], b))
elif b['verified'] != verified:
raise AssertionError('Expected verified breakpoint to be: %s. Found: %s.\nBreakpoint: %s' % (
verified, b['verified'], b))
return response
@ -1374,7 +1380,7 @@ def test_case_skipping_filters(case_setup, custom_setup):
other_filename = os.path.join(not_my_code_dir, 'other.py')
response = json_facade.write_set_breakpoints(1, filename=other_filename, verified=False)
assert response.body.breakpoints == [
{'verified': False, 'message': 'Breakpoint in file excluded by filters.', 'source': {'path': other_filename}, 'line': 1}]
{'verified': False, 'id': 0, 'message': 'Breakpoint in file excluded by filters.', 'source': {'path': other_filename}, 'line': 1}]
# Note: there's actually a use-case where we'd hit that breakpoint even if it was excluded
# by filters, so, we must actually clear it afterwards (the use-case is that when we're
# stepping into the context with the breakpoint we wouldn't skip it).
@ -1383,7 +1389,7 @@ def test_case_skipping_filters(case_setup, custom_setup):
other_filename = os.path.join(not_my_code_dir, 'file_that_does_not_exist.py')
response = json_facade.write_set_breakpoints(1, filename=other_filename, verified=False)
assert response.body.breakpoints == [
{'verified': False, 'message': 'Breakpoint in file that does not exist.', 'source': {'path': other_filename}, 'line': 1}]
{'verified': False, 'id': 1, 'message': 'Breakpoint in file that does not exist.', 'source': {'path': other_filename}, 'line': 1}]
elif custom_setup == 'set_exclude_launch_module_full':
json_facade.write_launch(
@ -1412,6 +1418,7 @@ def test_case_skipping_filters(case_setup, custom_setup):
33, filename=other_filename, verified=False, expected_lines_in_response=[14])
assert response.body.breakpoints == [{
'verified': False,
'id': 0,
'message': 'Breakpoint in file excluded by filters.\nNote: may be excluded because of \"justMyCode\" option (default == true).Try setting \"justMyCode\": false in the debug configuration (e.g., launch.json).\n',
'source': {'path': other_filename},
'line': 14
@ -4273,6 +4280,129 @@ def test_case_django_no_attribute_exception_breakpoint(case_setup_django, jmc):
writer.finished_ok = True
@pytest.mark.skipif(not TEST_DJANGO, reason='No django available')
def test_case_django_line_validation(case_setup_django):
import django # noqa (may not be there if TEST_DJANGO == False)
django_version = [int(x) for x in django.get_version().split('.')][:2]
support_lazy_line_validation = django_version >= [1, 9]
import django # noqa (may not be there if TEST_DJANGO == False)
with case_setup_django.test_file(EXPECTED_RETURNCODE='any') as writer:
json_facade = JsonFacade(writer)
json_facade.write_launch(debugOptions=['DebugStdLib', 'Django'])
template_file = debugger_unittest._get_debugger_test_file(os.path.join(writer.DJANGO_FOLDER, 'my_app', 'templates', 'my_app', 'index.html'))
file_doesnt_exist = os.path.join(os.path.dirname(template_file), 'this_does_not_exist.html')
# At this point, breakpoints will still not be verified (that'll happen when we
# actually load the template).
if support_lazy_line_validation:
json_facade.write_set_breakpoints([1, 2, 4], template_file, verified=False)
else:
json_facade.write_set_breakpoints([1, 2, 4], template_file, verified=True)
writer.write_make_initial_run()
t = writer.create_request_thread('my_app')
time.sleep(5) # Give django some time to get to startup before requesting the page
t.start()
json_facade.wait_for_thread_stopped(line=1)
breakpoint_events = json_facade.mark_messages(BreakpointEvent)
found = {}
for breakpoint_event in breakpoint_events:
bp = breakpoint_event.body.breakpoint
found[bp.id] = (bp.verified, bp.line)
if support_lazy_line_validation:
# At this point breakpoints were added.
# id=0 / Line 1 is ok
# id=1 / Line 2 will be disabled (because line 1 is already taken)
# id=2 / Line 4 will be moved to line 3
assert found == {
0: (True, 1),
1: (False, 2),
2: (True, 3),
}
else:
assert found == {}
# Now, after the template was loaded, when setting the breakpoints we can already
# know about the template validation.
if support_lazy_line_validation:
json_facade.write_set_breakpoints(
[1, 2, 8], template_file, expected_lines_in_response=set((1, 2, 7)),
# i.e.: breakpoint id to whether it's verified.
verified={3: True, 4: False, 5: True})
else:
json_facade.write_set_breakpoints(
[1, 2, 7], template_file, verified=True)
json_facade.write_continue()
json_facade.wait_for_thread_stopped(line=7)
json_facade.write_continue()
json_facade.wait_for_thread_stopped(line=7)
# To finish, check that setting on a file that doesn't exist is not verified.
response = json_facade.write_set_breakpoints([1], file_doesnt_exist, verified=False)
for bp in response.body.breakpoints:
assert 'Breakpoint in file that does not exist' in bp['message']
json_facade.write_continue()
writer.finished_ok = True
@pytest.mark.skipif(not TEST_FLASK, reason='No flask available')
def test_case_flask_line_validation(case_setup_flask):
with case_setup_flask.test_file(EXPECTED_RETURNCODE='any') as writer:
json_facade = JsonFacade(writer)
writer.write_set_project_roots([debugger_unittest._get_debugger_test_file('flask1')])
json_facade.write_launch(debugOptions=['Jinja'])
json_facade.write_make_initial_run()
template_file = debugger_unittest._get_debugger_test_file(os.path.join('flask1', 'templates', 'hello.html'))
# At this point, breakpoints will still not be verified (that'll happen when we
# actually load the template).
json_facade.write_set_breakpoints([1, 5, 6, 10], template_file, verified=False)
writer.write_make_initial_run()
t = writer.create_request_thread()
time.sleep(2) # Give flask some time to get to startup before requesting the page
t.start()
json_facade.wait_for_thread_stopped(line=5)
breakpoint_events = json_facade.mark_messages(BreakpointEvent)
found = {}
for breakpoint_event in breakpoint_events:
bp = breakpoint_event.body.breakpoint
found[bp.id] = (bp.verified, bp.line)
# At this point breakpoints were added.
# id=0 / Line 1 will be disabled
# id=1 / Line 5 is correct
# id=2 / Line 6 will be disabled (because line 5 is already taken)
# id=3 / Line 10 will be moved to line 8
assert found == {
0: (False, 1),
1: (True, 5),
2: (False, 6),
3: (True, 8),
}
json_facade.write_continue()
json_facade.wait_for_thread_stopped(line=8)
json_facade.write_continue()
writer.finished_ok = True
@pytest.mark.skipif(not TEST_FLASK, reason='No flask available')
@pytest.mark.parametrize("jmc", [False, True])
def test_case_flask_exceptions(case_setup_flask, jmc):

View file

@ -317,6 +317,7 @@ def test_breakpoint_in_nonexistent_file(pyfile, target, run):
{"path": some.path("nonexistent_file.py")}
),
"line": 1,
"id": 0,
}
]

View file

@ -60,10 +60,19 @@ def test_django_breakpoint_no_multiproc(start_django, bp_target):
with debug.Session() as session:
with start_django(session):
session.set_breakpoints(bp_file, [bp_line])
breakpoints = session.set_breakpoints(bp_file, [bp_line])
for bp in breakpoints:
# They'll be verified later on for templates.
assert bp["verified"] == (bp_target == "code")
with django_server:
home_request = django_server.get("/home")
if bp_target == "template":
breakpoint_body = session.wait_for_next_event("breakpoint")
assert breakpoint_body["reason"] == "changed"
assert breakpoint_body["breakpoint"]["verified"]
session.wait_for_stop(
"breakpoint",
expected_frames=[
@ -144,6 +153,7 @@ def test_django_exception_no_multiproc(start_django, exc_type):
with django_server:
django_server.get("/" + exc_type)
stopped = session.wait_for_stop(
"exception",
expected_frames=[

View file

@ -79,10 +79,19 @@ def test_flask_breakpoint_no_multiproc(start_flask, bp_target):
with debug.Session() as session:
with start_flask(session):
session.set_breakpoints(bp_file, [bp_line])
breakpoints = session.set_breakpoints(bp_file, [bp_line])
for bp in breakpoints:
# They'll be verified later on for templates.
assert bp["verified"] == (bp_target == "code")
with flask_server:
home_request = flask_server.get("/")
if bp_target == "template":
breakpoint_body = session.wait_for_next_event("breakpoint")
assert breakpoint_body["reason"] == "changed"
assert breakpoint_body["breakpoint"]["verified"]
session.wait_for_stop(
"breakpoint",
expected_frames=[