mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
Properly validate/change django/jinja2 template breakpoints lines. Fixes #213
This commit is contained in:
parent
ac360f4857
commit
ddb083cc25
17 changed files with 650 additions and 104 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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}
|
||||
)
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -317,6 +317,7 @@ def test_breakpoint_in_nonexistent_file(pyfile, target, run):
|
|||
{"path": some.path("nonexistent_file.py")}
|
||||
),
|
||||
"line": 1,
|
||||
"id": 0,
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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=[
|
||||
|
|
|
|||
|
|
@ -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=[
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue