Repository: microsoft/debugpy

Issue:
Set a variable from a different frame
Is there a way to copy a local variable (in a function) to the global scope (module), in order to inspect it after debugging ?

In my experience, sending a `setExpression` request with a global variable as expression does not create a global variable:

```
type: "request",
command: "setExpression",
arguments: {
   expression: "globals()['global_variable']", 
   value: "local_variable", 
   frameId: local_function_frame_id
},
seq: XXX,
```

The above request will create a variable within the frame of the function only. This variable will not be accessible from the frame of the module.

I may have misunderstood how frames and scopes work, I'd be happy to have some clarification if this is the case.

Comments:

Patch:
diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_save_locals.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_save_locals.py
index fa1a1252..c6bc3754 100644
--- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_save_locals.py
+++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_save_locals.py
@@ -70,6 +70,8 @@ def make_save_locals_impl():
 
 save_locals_impl = make_save_locals_impl()
 
+_SENTINEL = []  # Any mutable will do.
+
 
 def update_globals_and_locals(updated_globals, initial_globals, frame):
     # We don't have the locals and passed all in globals, so, we have to
@@ -83,8 +85,11 @@ def update_globals_and_locals(updated_globals, initial_globals, frame):
     # one that enabled creating and using variables during the same evaluation.
     assert updated_globals is not None
     f_locals = None
+
+    removed = set(initial_globals).difference(updated_globals)
+
     for key, val in updated_globals.items():
-        if initial_globals.get(key) is not val:
+        if val is not initial_globals.get(key, _SENTINEL):
             if f_locals is None:
                 # Note: we call f_locals only once because each time
                 # we call it the values may be reset.
@@ -92,5 +97,17 @@ def update_globals_and_locals(updated_globals, initial_globals, frame):
 
             f_locals[key] = val
 
+    if removed:
+        if f_locals is None:
+            # Note: we call f_locals only once because each time
+            # we call it the values may be reset.
+            f_locals = frame.f_locals
+
+        for key in removed:
+            try:
+                del f_locals[key]
+            except KeyError:
+                pass
+
     if f_locals is not None:
         save_locals(frame)
diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_vars.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_vars.py
index 2634714a..cdbe3085 100644
--- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_vars.py
+++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_vars.py
@@ -523,6 +523,10 @@ def evaluate_expression(py_db, frame, expression, is_exec):
     updated_globals = {}
     updated_globals.update(frame.f_globals)
     updated_globals.update(frame.f_locals)
+    if 'globals' not in updated_globals:
+        # If the user explicitly uses 'globals()' then we provide the
+        # frame globals (unless he has shadowed it already).
+        updated_globals['globals'] = lambda: frame.f_globals
 
     initial_globals = updated_globals.copy()
 


Test patch:
diff --git a/src/debugpy/_vendored/pydevd/tests_python/test_evaluate_expression.py b/src/debugpy/_vendored/pydevd/tests_python/test_evaluate_expression.py
index adc4ba26..123fb639 100644
--- a/src/debugpy/_vendored/pydevd/tests_python/test_evaluate_expression.py
+++ b/src/debugpy/_vendored/pydevd/tests_python/test_evaluate_expression.py
@@ -122,6 +122,47 @@ def test_evaluate_expression_4(disable_critical_log):
     assert 'email' not in sys._getframe().f_globals
 
 
+def test_evaluate_expression_access_globals(disable_critical_log):
+    from _pydevd_bundle.pydevd_vars import evaluate_expression
+
+    def check(frame):
+        eval_txt = '''globals()['global_variable'] = 22'''
+        evaluate_expression(None, frame, eval_txt, is_exec=True)
+        assert 'global_variable' not in frame.f_locals
+        assert 'global_variable' in frame.f_globals
+
+    check(next(iter(obtain_frame())))
+    assert 'global_variable' in sys._getframe().f_globals
+    assert 'global_variable' not in sys._getframe().f_locals
+
+
+def test_evaluate_expression_create_none(disable_critical_log):
+    from _pydevd_bundle.pydevd_vars import evaluate_expression
+
+    def check(frame):
+        eval_txt = 'x = None'
+        evaluate_expression(None, frame, eval_txt, is_exec=True)
+        assert 'x' in frame.f_locals
+        assert 'x' not in frame.f_globals
+
+    check(next(iter(obtain_frame())))
+
+
+def test_evaluate_expression_delete_var(disable_critical_log):
+    from _pydevd_bundle.pydevd_vars import evaluate_expression
+
+    def check(frame):
+        eval_txt = 'x = 22'
+        evaluate_expression(None, frame, eval_txt, is_exec=True)
+        assert 'x' in frame.f_locals
+
+        eval_txt = 'del x'
+        evaluate_expression(None, frame, eval_txt, is_exec=True)
+        assert 'x' not in frame.f_locals
+
+    check(next(iter(obtain_frame())))
+
+
 def test_evaluate_expression_5(disable_critical_log):
     from _pydevd_bundle.pydevd_vars import evaluate_expression