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.

Discussion:
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=microsoft_debugpy&pullRequest=1147)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=1147&metric=coverage&view=list) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=1147&metric=duplicated_lines_density&view=list) No Duplication information


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=microsoft_debugpy&pullRequest=1147)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1147&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=1147&metric=coverage&view=list) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=1147&metric=duplicated_lines_density&view=list) No Duplication information



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