diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py index 219e6471f..629098092 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_api.py @@ -228,9 +228,31 @@ def request_step(self, py_db, thread_id, step_cmd_id): elif thread_id.startswith('__frame__:'): sys.stderr.write("Can't make tasklet step command: %s\n" % (thread_id,)) - def request_set_next(self, py_db, seq, thread_id, set_next_cmd_id, line, func_name): + def request_set_next(self, py_db, seq, thread_id, set_next_cmd_id, original_filename, line, func_name): + ''' + :param Optional[str] original_filename: + If available, the filename may be source translated, otherwise no translation will take + place (the set next just needs the line afterwards as it executes locally, but for + the Jupyter integration, the source mapping may change the actual lines and not only + the filename). + ''' t = pydevd_find_thread_by_id(thread_id) if t: + if original_filename is not None: + translated_filename = self.filename_to_server(original_filename) # Apply user path mapping. + pydev_log.debug('Set next (after path translation) in: %s line: %s', translated_filename, line) + func_name = self.to_str(func_name) + + assert translated_filename.__class__ == str # i.e.: bytes on py2 and str on py3 + assert func_name.__class__ == str # i.e.: bytes on py2 and str on py3 + + # Apply source mapping (i.e.: ipython). + _source_mapped_filename, new_line, multi_mapping_applied = py_db.source_mapping.map_to_server( + translated_filename, line) + if multi_mapping_applied: + pydev_log.debug('Set next (after source mapping) in: %s line: %s', translated_filename, line) + line = new_line + int_cmd = InternalSetNextStatementThread(thread_id, set_next_cmd_id, line, func_name, seq=seq) py_db.post_internal_command(int_cmd, thread_id) elif thread_id.startswith('__frame__:'): 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 2b335f2ca..98dd6b107 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 @@ -163,7 +163,7 @@ def _cmd_step(self, py_db, cmd_id, seq, text): def _cmd_set_next(self, py_db, cmd_id, seq, text): thread_id, line, func_name = text.split('\t', 2) - return self.api.request_set_next(py_db, seq, thread_id, cmd_id, line, func_name) + return self.api.request_set_next(py_db, seq, thread_id, cmd_id, None, line, func_name) cmd_run_to_line = _cmd_set_next cmd_set_next_statement = _cmd_set_next @@ -366,7 +366,6 @@ def cmd_set_path_mapping_json(self, py_db, cmd_id, seq, text): if debug or force: pydevd_file_utils.DEBUG_CLIENT_SERVER_TRANSLATION = debug - def cmd_set_py_exception_json(self, py_db, cmd_id, seq, text): # This API is optional and works 'in bulk' -- it's possible # to get finer-grained control with CMD_ADD_EXCEPTION_BREAK/CMD_REMOVE_EXCEPTION_BREAK 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 eefa77f16..22f8be008 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 @@ -994,7 +994,7 @@ def on_goto_request(self, py_db, request): target_id = int(request.arguments.targetId) thread_id = request.arguments.threadId try: - _, line = self._goto_targets_map.obtain_value(target_id) + path, line = self._goto_targets_map.obtain_value(target_id) except KeyError: response = pydevd_base_schema.build_response( request, @@ -1005,7 +1005,7 @@ def on_goto_request(self, py_db, request): }) return NetCommand(CMD_RETURN, 0, response, is_json=True) - self.api.request_set_next(py_db, request.seq, thread_id, CMD_SET_NEXT_STATEMENT, line, '*') + self.api.request_set_next(py_db, request.seq, thread_id, CMD_SET_NEXT_STATEMENT, path, line, '*') # See 'NetCommandFactoryJson.make_set_next_stmnt_status_message' for response return None diff --git a/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_source_map_goto_target.py b/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_source_map_goto_target.py new file mode 100644 index 000000000..11619b00e --- /dev/null +++ b/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_source_map_goto_target.py @@ -0,0 +1,24 @@ +# Some comment lines to move the function below +# Some comment lines to move the function below +# Some comment lines to move the function below +# Some comment lines to move the function below + + +def full_function(): + # Note that this function is not called, it's there just to make the mapping explicit. + # The test case should stop at `a = 1` and then skip the `print('Skip this print')`. + # map to Cell1, line 1 + a = 1 # map to Cell1, line 2 + print('Skip this print') # map to Cell1, line 3 + print('TEST SUCEEDED') # map to Cell1, line 4 + b = 2 # map to Cell1, line 5 + + +if __name__ == '__main__': + code = compile('''# line 1 +a = 1 # line 2 +print('Skip this print') # line 3 +print('TEST SUCEEDED') # line 4 +b = 2 # line 5 +''', '', 'exec') + exec(code) 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 fd61a2d5c..36adc064b 100644 --- a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py +++ b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py @@ -3293,6 +3293,60 @@ def test_source_mapping_just_my_code(case_setup): writer.finished_ok = True +def test_source_mapping_goto_target(case_setup): + from _pydevd_bundle._debug_adapter.pydevd_schema import Source + from _pydevd_bundle._debug_adapter.pydevd_schema import PydevdSourceMap + + with case_setup.test_file('_debugger_case_source_map_goto_target.py') as writer: + test_file = writer.TEST_FILE + if isinstance(test_file, bytes): + # file is in the filesystem encoding (needed for launch) but protocol needs it in utf-8 + test_file = test_file.decode(file_system_encoding) + test_file = test_file.encode('utf-8') + + json_facade = JsonFacade(writer) + json_facade.write_launch(justMyCode=False) + + map_to_cell_1_line1 = writer.get_line_index_with_content('map to Cell1, line 1') + map_to_cell_1_line2 = writer.get_line_index_with_content('map to Cell1, line 2') + map_to_cell_1_line4 = writer.get_line_index_with_content('map to Cell1, line 4') + map_to_cell_1_line5 = writer.get_line_index_with_content('map to Cell1, line 5') + + cell1_map = PydevdSourceMap(map_to_cell_1_line1, map_to_cell_1_line5, Source(path=''), 1) + pydevd_source_maps = [cell1_map] + json_facade.write_set_pydevd_source_map( + Source(path=test_file), + pydevd_source_maps=pydevd_source_maps, + ) + json_facade.write_set_breakpoints(map_to_cell_1_line2) + + json_facade.write_make_initial_run() + + json_hit = json_facade.wait_for_thread_stopped(line=map_to_cell_1_line2, file=os.path.basename(test_file)) + for stack_frame in json_hit.stack_trace_response.body.stackFrames: + assert stack_frame['source']['sourceReference'] == 0 + + goto_targets_request = json_facade.write_request( + pydevd_schema.GotoTargetsRequest(pydevd_schema.GotoTargetsArguments( + source=pydevd_schema.Source(path=writer.TEST_FILE, sourceReference=0), + line=map_to_cell_1_line4))) + goto_targets_response = json_facade.wait_for_response(goto_targets_request) + target_id = goto_targets_response.body.targets[0]['id'] + + goto_request = json_facade.write_request( + pydevd_schema.GotoRequest(pydevd_schema.GotoArguments( + threadId=json_hit.thread_id, + targetId=target_id))) + goto_response = json_facade.wait_for_response(goto_request) + assert goto_response.success + + json_hit = json_facade.wait_for_thread_stopped('goto') + + json_facade.write_continue() + + writer.finished_ok = True + + @pytest.mark.skipif(not TEST_CHERRYPY or IS_WINDOWS, reason='No CherryPy available / not ok in Windows.') def test_process_autoreload_cherrypy(case_setup_multiprocessing, tmpdir): '''