diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py index 843b9e459..8b738c3a4 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py @@ -370,22 +370,31 @@ def __init__(self, filename): 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 @@ def add_breakpoint( 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 @@ def add_breakpoint( # (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 @@ def add_breakpoint( 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 @@ def add_breakpoint( 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 @@ def add_breakpoint( 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 @@ def add_breakpoint( 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 @@ def add_breakpoint( 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 @@ def remove_breakpoint(self, py_db, received_filename, breakpoint_type, breakpoin 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 @@ def remove_breakpoint(self, py_db, received_filename, breakpoint_type, breakpoin 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", diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_breakpoints.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_breakpoints.py index 51944460b..29cb7ff47 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_breakpoints.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_breakpoints.py @@ -48,7 +48,8 @@ def handle_hit_condition(self, frame): 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 diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_dont_trace_files.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_dont_trace_files.py index 77d464eea..0efb4b4b6 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_dont_trace_files.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_dont_trace_files.py @@ -117,6 +117,7 @@ '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, diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_frame_utils.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_frame_utils.py index f01fcdf02..c34ed3047 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_frame_utils.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_frame_utils.py @@ -29,6 +29,7 @@ def __init__(self, name, filename): self.co_name = name self.co_filename = filename self.co_firstlineno = 1 + self.co_flags = 0 def add_exception_to_frame(frame, exception_info): diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command.py index 6d2fb577b..75a52431d 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command.py @@ -303,21 +303,42 @@ def cmd_set_break(self, py_db, cmd_id, seq, text): 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 + + translated_line = add_breakpoint_result.translated_line + translated_filename = add_breakpoint_result.translated_filename + msg = '' + if error_code: + + 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). - 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,)) + 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,) - 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,)) + 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 diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py index 9639e5ded..18c54ec52 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py @@ -16,7 +16,8 @@ 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 @@ def on_setbreakpoints_request(self, py_db, request): 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,36 +774,56 @@ def on_setbreakpoints_request(self, py_db, request): 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.' + bp = self._create_breakpoint_from_add_breakpoint_result(py_db, arguments.source, breakpoint_id, result) + breakpoints_set.append(bp) - 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') + 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) - else: - # Shouldn't get here. - error_msg = 'Breakpoint not validated (reason unknown -- please report as bug).' + 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.' - 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()) + # Shouldn't get here. + error_msg = 'Breakpoint not validated (reason unknown -- please report as bug).' - 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) + 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): ''' diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_trace_api.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_trace_api.py index 79417c5cd..77e8b3fad 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_trace_api.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_trace_api.py @@ -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 diff --git a/src/debugpy/_vendored/pydevd/pydevd.py b/src/debugpy/_vendored/pydevd/pydevd.py index a4f110fbd..ad465fdaa 100644 --- a/src/debugpy/_vendored/pydevd/pydevd.py +++ b/src/debugpy/_vendored/pydevd/pydevd.py @@ -1716,12 +1716,12 @@ def process_internal_commands(self): 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): diff --git a/src/debugpy/_vendored/pydevd/pydevd_plugins/django_debug.py b/src/debugpy/_vendored/pydevd/pydevd_plugins/django_debug.py index 7d72c7498..20e10de50 100644 --- a/src/debugpy/_vendored/pydevd/pydevd_plugins/django_debug.py +++ b/src/debugpy/_vendored/pydevd/pydevd_plugins/django_debug.py @@ -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 @@ 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): diff --git a/src/debugpy/_vendored/pydevd/pydevd_plugins/jinja2_debug.py b/src/debugpy/_vendored/pydevd/pydevd_plugins/jinja2_debug.py index ad4e8d4e7..a660f4922 100644 --- a/src/debugpy/_vendored/pydevd/pydevd_plugins/jinja2_debug.py +++ b/src/debugpy/_vendored/pydevd/pydevd_plugins/jinja2_debug.py @@ -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): diff --git a/src/debugpy/_vendored/pydevd/pydevd_plugins/pydevd_line_validation.py b/src/debugpy/_vendored/pydevd/pydevd_plugins/pydevd_line_validation.py new file mode 100644 index 000000000..418e518fe --- /dev/null +++ b/src/debugpy/_vendored/pydevd/pydevd_plugins/pydevd_line_validation.py @@ -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) + diff --git a/src/debugpy/_vendored/pydevd/tests_python/my_django_proj_21/my_app/tests.py b/src/debugpy/_vendored/pydevd/tests_python/my_django_proj_21/my_app/tests.py new file mode 100644 index 000000000..759ab0e5b --- /dev/null +++ b/src/debugpy/_vendored/pydevd/tests_python/my_django_proj_21/my_app/tests.py @@ -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 %} + +{% else %} +

No entries are available.

+{% endif %}''' + + self.assertEqual( + collect_lines_for_django_template(template_contents), + {1, 3, 4, 6, 8, 10, 11, 13} + ) diff --git a/src/debugpy/_vendored/pydevd/tests_python/test_debugger.py b/src/debugpy/_vendored/pydevd/tests_python/test_debugger.py index ae9377a29..cba32237b 100644 --- a/src/debugpy/_vendored/pydevd/tests_python/test_debugger.py +++ b/src/debugpy/_vendored/pydevd/tests_python/test_debugger.py @@ -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', diff --git a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py index 556652d4b..5d20991b1 100644 --- a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py +++ b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py @@ -14,7 +14,8 @@ 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 @@ def write_set_breakpoints( 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): diff --git a/tests/debugpy/test_breakpoints.py b/tests/debugpy/test_breakpoints.py index 0b5b681d0..6e3c35936 100644 --- a/tests/debugpy/test_breakpoints.py +++ b/tests/debugpy/test_breakpoints.py @@ -317,6 +317,7 @@ def code_to_debug(): {"path": some.path("nonexistent_file.py")} ), "line": 1, + "id": 0, } ] diff --git a/tests/debugpy/test_django.py b/tests/debugpy/test_django.py index 35cd8bb52..2e73bae6c 100644 --- a/tests/debugpy/test_django.py +++ b/tests/debugpy/test_django.py @@ -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=[ diff --git a/tests/debugpy/test_flask.py b/tests/debugpy/test_flask.py index 434eb3136..b188031ee 100644 --- a/tests/debugpy/test_flask.py +++ b/tests/debugpy/test_flask.py @@ -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=[