diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py index 95611b5d..69aa5fd0 100644 --- a/ptvsd/wrapper.py +++ b/ptvsd/wrapper.py @@ -704,6 +704,7 @@ DEBUG_OPTIONS_PARSER = { 'CLIENT_OS_TYPE': unquote, 'DEBUG_STDLIB': bool_parser, 'STOP_ON_ENTRY': bool_parser, + 'SHOW_RETURN_VALUE': bool_parser, } @@ -719,6 +720,7 @@ DEBUG_OPTIONS_BY_FLAG = { 'WindowsClient': 'CLIENT_OS_TYPE=WINDOWS', 'UnixClient': 'CLIENT_OS_TYPE=UNIX', 'StopOnEntry': 'STOP_ON_ENTRY=True', + 'ShowReturnValue': 'SHOW_RETURN_VALUE=True', } @@ -1362,6 +1364,9 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor): if opts.get('STOP_ON_ENTRY', False) and self.start_reason == 'launch': self.pydevd_request(pydevd_comm.CMD_STOP_ON_START, '1') + if opts.get('SHOW_RETURN_VALUE', False): + self.pydevd_request(pydevd_comm.CMD_SHOW_RETURN_VALUES, '1\t1') + self._apply_code_stepping_settings() def _is_just_my_code_stepping_enabled(self): @@ -1708,6 +1713,7 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor): variables = VariablesSorter() for xvar in xvars: + attributes = [] var_name = unquote(xvar['name']) var_type = unquote(xvar['type']) var_value = unquote(xvar['value']) @@ -1718,16 +1724,24 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor): } if self._is_raw_string(var_type): - var['presentationHint'] = {'attributes': ['rawString']} + attributes.append('rawString') - if bool(xvar['isContainer']): - pyd_child = pyd_var + (var_name,) - var['variablesReference'] = self.var_map.to_vscode( - pyd_child, autogen=True) + if bool(xvar['isRetVal']): + attributes.append('readOnly') + var['name'] = '(return) %s' % var_name + else: + if bool(xvar['isContainer']): + pyd_child = pyd_var + (var_name,) + var['variablesReference'] = self.var_map.to_vscode( + pyd_child, autogen=True) - eval_name = self._get_variable_evaluate_name(pyd_var, var_name) - if eval_name: - var['evaluateName'] = eval_name + eval_name = self._get_variable_evaluate_name( + pyd_var, var_name) + if eval_name: + var['evaluateName'] = eval_name + + if len(attributes) > 0: + var['presentationHint'] = {'attributes': attributes} variables.append(var) @@ -1779,18 +1793,23 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor): @async_handler def on_setVariable(self, request, args): """Handles DAP SetVariableRequest.""" - + var_name = args['name'] + var_value = args['value'] vsc_var = int(args['variablesReference']) + fmt = args.get('format', {}) + + if var_name.startswith('(return) '): + self.send_error_response( + request, + 'Cannot change return value.') + return + try: pyd_var = self.var_map.to_pydevd(vsc_var) except KeyError: self.send_error_response(request) return - var_name = args['name'] - var_value = args['value'] - fmt = args.get('format', {}) - lhs_expr = self._get_variable_evaluate_name(pyd_var, var_name) if not lhs_expr: lhs_expr = var_name diff --git a/tests/resources/system_tests/test_misc/returnvalues.py b/tests/resources/system_tests/test_misc/returnvalues.py new file mode 100644 index 00000000..fe8f27e2 --- /dev/null +++ b/tests/resources/system_tests/test_misc/returnvalues.py @@ -0,0 +1,12 @@ +class MyClass(object): + def do_something(self): + return 'did something' + + +def my_func(): + return 'did more things' + + +MyClass().do_something() +my_func() +print('done') diff --git a/tests/system_tests/test_misc.py b/tests/system_tests/test_misc.py index 4f5bd329..b7b8f0b0 100644 --- a/tests/system_tests/test_misc.py +++ b/tests/system_tests/test_misc.py @@ -198,3 +198,128 @@ class StopOnEntryLaunchPackageTests(StopOnEntryTests): modulename='mymod', cwd=cwd, env=env)) + + +class ShowReturnValueTests(LifecycleTestsBase): + def _debugger_step_next(self, session, thread_id): + stopped = session.get_awaiter_for_event('stopped') + req_next = session.send_request('next', threadId=thread_id) + req_next.wait(timeout=2.0) + stopped.wait(timeout=2.0) + + def _get_variables(self, session, thread_id): + req_stacktrace = session.send_request( + 'stackTrace', + threadId=thread_id, + ) + req_stacktrace.wait(timeout=2.0) + + frames = req_stacktrace.resp.body['stackFrames'] + frame_id = frames[0]['id'] + + req_scopes = session.send_request( + 'scopes', + frameId=frame_id, + ) + req_scopes.wait(timeout=2.0) + + scopes = req_scopes.resp.body['scopes'] + variables_reference = scopes[0]['variablesReference'] + + req_variables = session.send_request( + 'variables', + variablesReference=variables_reference, + ) + req_variables.wait(timeout=2.0) + + return req_variables.resp.body['variables'] + + def run_test_return_values(self, debug_info, is_enabled): + if is_enabled: + options = {'debugOptions': ['RedirectOutput', 'ShowReturnValue']} + else: + options = {'debugOptions': ['RedirectOutput']} + + breakpoints = [{ + 'source': { + 'path': debug_info.filename + }, + 'breakpoints': [{'line': 10}, {'line': 11}, {'line': 12}], + 'lines': [10, 11, 12] + }] + with self.start_debugging(debug_info) as dbg: + session = dbg.session + stopped = session.get_awaiter_for_event('stopped') + (_, req_launch_attach, _, _, _, _ + ) = lifecycle_handshake(session, debug_info.starttype, + options=options, + breakpoints=breakpoints) + req_launch_attach.wait(timeout=3.0) + + stopped.wait() + thread_id = stopped.event.body['threadId'] + + self._debugger_step_next(session, thread_id) + variables = self._get_variables(session, thread_id) + + return_values = list(v for v in variables + if v['name'].startswith('(return) ')) + + if is_enabled: + self.assertEqual(len(return_values), 1) + self.assertEqual(return_values[0]['name'], + '(return) MyClass.do_something') + self.assertEqual(return_values[0]['value'], + "'did something'") + self.assertEqual(return_values[0]['type'], + 'str') + attributes = return_values[0]['presentationHint']['attributes'] + self.assertTrue('readOnly' in attributes) + else: + self.assertEqual(len(return_values), 0) + + self._debugger_step_next(session, thread_id) + variables = self._get_variables(session, thread_id) + + return_values = list(v for v in variables + if v['name'].startswith('(return) ')) + + if is_enabled: + self.assertEqual(len(return_values), 2) + self.assertEqual(return_values[0]['name'], + '(return) MyClass.do_something') + self.assertEqual(return_values[0]['value'], + "'did something'") + self.assertEqual(return_values[0]['type'], + 'str') + attributes = return_values[0]['presentationHint']['attributes'] + self.assertTrue('readOnly' in attributes) + + self.assertEqual(return_values[1]['name'], + '(return) my_func') + self.assertEqual(return_values[1]['value'], + "'did more things'") + self.assertEqual(return_values[1]['type'], + 'str') + attributes = return_values[1]['presentationHint']['attributes'] + self.assertTrue('readOnly' in attributes) + else: + self.assertEqual(len(return_values), 0) + + exited = session.get_awaiter_for_event('exited') + session.send_request('continue').wait(timeout=2.0) + exited.wait(timeout=3.0) + + def test_return_values_enabled(self): + filename = TEST_FILES.resolve('returnvalues.py') + cwd = os.path.dirname(filename) + self.run_test_return_values( + DebugInfo(filename=filename, cwd=cwd), + is_enabled=True) + + def test_return_values_disabled(self): + filename = TEST_FILES.resolve('returnvalues.py') + cwd = os.path.dirname(filename) + self.run_test_return_values( + DebugInfo(filename=filename, cwd=cwd), + is_enabled=False)