Repository: microsoft/ptvsd

Issue:
Support "evaluate" request without a "frameId"



Discussion:

Resolving patch:
diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py
--- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py
+++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py
@@ -897,10 +897,14 @@ def _evaluate_response(py_db, request, result, error_message=''):
         py_db.writer.add_command(NetCommand(CMD_RETURN, 0, variables_response, is_json=True))
 
 
+_global_frame = None
+
+
 def internal_evaluate_expression_json(py_db, request, thread_id):
     '''
     :param EvaluateRequest request:
     '''
+    global _global_frame
     # : :type arguments: EvaluateArguments
 
     arguments = request.arguments
@@ -918,28 +922,43 @@ def internal_evaluate_expression_json(py_db, request, thread_id):
             _evaluate_response(py_db, request, '', error_message='Expression is not valid utf-8.')
             raise
 
-    frame = py_db.find_frame(thread_id, frame_id)
-    result = pydevd_vars.evaluate_expression(py_db, frame, expression, is_exec=False)
-    is_error = isinstance(result, ExceptionOnEvaluate)
+    try_exec = False
+    if frame_id is None:
+        if _global_frame is None:
+            # Lazily create a frame to be used for evaluation with no frame id.
 
-    if is_error:
-        if context == 'hover':
-            _evaluate_response(py_db, request, result='')
-            return
+            def __create_frame():
+                yield sys._getframe()
 
-        elif context == 'repl':
-            try:
-                pydevd_vars.evaluate_expression(py_db, frame, expression, is_exec=True)
-            except Exception as ex:
-                err = ''.join(traceback.format_exception_only(type(ex), ex))
-                # Currently there is an issue in VSC where returning success=false for an
-                # eval request, in repl context, VSC does not show the error response in
-                # the debug console. So return the error message in result as well.
-                _evaluate_response(py_db, request, result=err, error_message=err)
+            _global_frame = next(__create_frame())
+
+        frame = _global_frame
+        try_exec = True  # Always exec in this case
+        eval_result = None
+    else:
+        frame = py_db.find_frame(thread_id, frame_id)
+        eval_result = pydevd_vars.evaluate_expression(py_db, frame, expression, is_exec=False)
+        is_error = isinstance(eval_result, ExceptionOnEvaluate)
+        if is_error:
+            if context == 'hover':  # In a hover it doesn't make sense to do an exec.
+                _evaluate_response(py_db, request, result='')
                 return
-            # No result on exec.
-            _evaluate_response(py_db, request, result='')
+            else:
+                try_exec = context == 'repl'
+
+    if try_exec:
+        try:
+            pydevd_vars.evaluate_expression(py_db, frame, expression, is_exec=True)
+        except Exception as ex:
+            err = ''.join(traceback.format_exception_only(type(ex), ex))
+            # Currently there is an issue in VSC where returning success=false for an
+            # eval request, in repl context, VSC does not show the error response in
+            # the debug console. So return the error message in result as well.
+            _evaluate_response(py_db, request, result=err, error_message=err)
             return
+        # No result on exec.
+        _evaluate_response(py_db, request, result='')
+        return
 
     # Ok, we have the result (could be an error), let's put it into the saved variables.
     frame_tracker = py_db.suspended_frames_manager.get_frame_tracker(thread_id)
@@ -948,7 +967,7 @@ def internal_evaluate_expression_json(py_db, request, thread_id):
         _evaluate_response(py_db, request, result='', error_message='Thread id: %s is not current thread id.' % (thread_id,))
         return
 
-    variable = frame_tracker.obtain_as_variable(expression, result, frame=frame)
+    variable = frame_tracker.obtain_as_variable(expression, eval_result, frame=frame)
     var_data = variable.get_var_data(fmt=fmt)
 
     body = pydevd_schema.EvaluateResponseBody(
diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py
--- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py
+++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py
@@ -701,22 +701,25 @@ def on_evaluate_request(self, py_db, request):
         # : :type arguments: EvaluateArguments
         arguments = request.arguments
 
-        thread_id = py_db.suspended_frames_manager.get_thread_id_for_variable_reference(
-            arguments.frameId)
-
-        if thread_id is not None:
-            self.api.request_exec_or_evaluate_json(
-                py_db, request, thread_id)
+        if arguments.frameId is None:
+            self.api.request_exec_or_evaluate_json(py_db, request, thread_id='*')
         else:
-            body = EvaluateResponseBody('', 0)
-            response = pydevd_base_schema.build_response(
-                request,
-                kwargs={
-                    'body': body,
-                    'success': False,
-                    'message': 'Unable to find thread for evaluation.'
-                })
-            return NetCommand(CMD_RETURN, 0, response, is_json=True)
+            thread_id = py_db.suspended_frames_manager.get_thread_id_for_variable_reference(
+                arguments.frameId)
+
+            if thread_id is not None:
+                self.api.request_exec_or_evaluate_json(
+                    py_db, request, thread_id)
+            else:
+                body = EvaluateResponseBody('', 0)
+                response = pydevd_base_schema.build_response(
+                    request,
+                    kwargs={
+                        'body': body,
+                        'success': False,
+                        'message': 'Unable to find thread for evaluation.'
+                    })
+                return NetCommand(CMD_RETURN, 0, response, is_json=True)
 
     def on_setexpression_request(self, py_db, request):
         # : :type arguments: SetExpressionArguments


Test patch:
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py
@@ -416,6 +416,14 @@ def write_initialize(self, access_token, success=True):
             assert isinstance(process_id, int)
         return response
 
+    def evaluate(self, expression, frameId=None, context=None, fmt=None, success=True):
+        eval_request = self.write_request(
+            pydevd_schema.EvaluateRequest(pydevd_schema.EvaluateArguments(
+                expression, frameId=frameId, context=context, format=fmt)))
+        eval_response = self.wait_for_response(eval_request)
+        assert eval_response.success == success
+        return eval_response
+
 
 def test_case_json_logpoints(case_setup):
     with case_setup.test_file('_debugger_case_change_breaks.py') as writer:
@@ -1803,25 +1811,36 @@ def test_evaluate(case_setup):
         stack_frame_id = stack_frame['id']
 
         # Test evaluate request that results in 'eval'
-        eval_request = json_facade.write_request(
-            pydevd_schema.EvaluateRequest(pydevd_schema.EvaluateArguments('var_1', frameId=stack_frame_id, context='repl')))
-        eval_response = json_facade.wait_for_response(eval_request)
+        eval_response = json_facade.evaluate('var_1', frameId=stack_frame_id, context='repl')
         assert eval_response.body.result == '5'
         assert eval_response.body.type == 'int'
 
         # Test evaluate request that results in 'exec'
-        exec_request = json_facade.write_request(
-            pydevd_schema.EvaluateRequest(pydevd_schema.EvaluateArguments('var_1 = 6', frameId=stack_frame_id, context='repl')))
-        exec_response = json_facade.wait_for_response(exec_request)
+        exec_response = json_facade.evaluate('var_1 = 6', frameId=stack_frame_id, context='repl')
         assert exec_response.body.result == ''
 
         # Test evaluate request that results in 'exec' but fails
-        exec_request = json_facade.write_request(
-            pydevd_schema.EvaluateRequest(pydevd_schema.EvaluateArguments('var_1 = "abc"/6', frameId=stack_frame_id, context='repl')))
-        exec_response = json_facade.wait_for_response(exec_request)
-        assert exec_response.success == False
-        assert exec_response.body.result.find('TypeError') > -1
-        assert exec_response.message.find('TypeError') > -1
+        exec_response = json_facade.evaluate(
+            'var_1 = "abc"/6', frameId=stack_frame_id, context='repl', success=False)
+        assert 'TypeError' in exec_response.body.result
+        assert 'TypeError' in exec_response.message
+
+        # Evaluate without a frameId.
+
+        # Error because 'foo_value' is not set in 'sys'.
+        exec_response = json_facade.evaluate('import email;email.foo_value', success=False)
+        assert 'AttributeError' in exec_response.body.result
+        assert 'AttributeError' in exec_response.message
+
+        # Reading foo_value didn't work, but 'email' should be in the namespace now.
+        json_facade.evaluate('email.foo_value=True')
+
+        # Ok, 'foo_value' is now set in 'email' module.
+        exec_response = json_facade.evaluate('email.foo_value')
+
+        # We don't actually get variables without a frameId, we can just evaluate and observe side effects
+        # (so, the result is always empty -- or an error).
+        assert exec_response.body.result == ''
 
         json_facade.write_continue(wait_for_response=False)