Account for the scope when changing variables (#1869)

* Support using scope during setVariable request

* Fix other overloads to accept scope too

* Use 3.9 compatible unions

* Review feedback

* Pydevd test wasn't actually validating
This commit is contained in:
Rich Chiodo 2025-03-10 16:15:23 -07:00 committed by GitHub
parent e01e6dd8a9
commit f7d8963f99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 42 additions and 20 deletions

View file

@ -934,7 +934,7 @@ def internal_change_variable_json(py_db, request):
)
return
child_var = variable.change_variable(arguments.name, arguments.value, py_db, fmt=fmt)
child_var = variable.change_variable(arguments.name, arguments.value, py_db, fmt=fmt, scope=scope)
if child_var is None:
_write_variable_response(py_db, request, value="", success=False, message="Unable to change: %s." % (arguments.name,))

View file

@ -199,9 +199,9 @@ class PluginManager(object):
return None
def change_variable(self, frame, attr, expression):
def change_variable(self, frame, attr, expression, scope=None):
for plugin in self.active_plugins:
ret = plugin.change_variable(frame, attr, expression, self.EMPTY_SENTINEL)
ret = plugin.change_variable(frame, attr, expression, self.EMPTY_SENTINEL, scope)
if ret is not self.EMPTY_SENTINEL:
return ret

View file

@ -200,7 +200,7 @@ class _ObjectVariable(_AbstractVariable):
return children_variables
def change_variable(self, name, value, py_db, fmt=None):
def change_variable(self, name, value, py_db, fmt=None, scope: Optional[ScopeRequest]=None):
children_variable = self.get_child_variable_named(name)
if children_variable is None:
return None
@ -255,12 +255,10 @@ class _FrameVariable(_AbstractVariable):
self._register_variable = register_variable
self._register_variable(self)
def change_variable(self, name, value, py_db, fmt=None):
def change_variable(self, name, value, py_db, fmt=None, scope: Optional[ScopeRequest]=None):
frame = self.frame
pydevd_vars.change_attr_expression(frame, name, value, py_db)
return self.get_child_variable_named(name, fmt=fmt)
pydevd_vars.change_attr_expression(frame, name, value, py_db, scope=scope)
return self.get_child_variable_named(name, fmt=fmt, scope=scope)
@silence_warnings_decorator
@overrides(_AbstractVariable.get_children_variables)

View file

@ -15,11 +15,12 @@ import sys # @Reimport
from _pydev_bundle._pydev_saved_modules import threading
from _pydevd_bundle import pydevd_save_locals, pydevd_timeout, pydevd_constants
from _pydev_bundle.pydev_imports import Exec, execfile
from _pydevd_bundle.pydevd_utils import to_string
from _pydevd_bundle.pydevd_utils import to_string, ScopeRequest
import inspect
from _pydevd_bundle.pydevd_daemon_thread import PyDBDaemonThread
from _pydevd_bundle.pydevd_save_locals import update_globals_and_locals
from functools import lru_cache
from typing import Optional
SENTINEL_VALUE = []
@ -595,11 +596,15 @@ def evaluate_expression(py_db, frame, expression, is_exec):
del frame
def change_attr_expression(frame, attr, expression, dbg, value=SENTINEL_VALUE):
def change_attr_expression(frame, attr, expression, dbg, value=SENTINEL_VALUE, /, scope: Optional[ScopeRequest]=None):
"""Changes some attribute in a given frame."""
if frame is None:
return
if scope is not None:
assert isinstance(scope, ScopeRequest)
scope = scope.scope
try:
expression = expression.replace("@LINE@", "\n")
@ -608,13 +613,15 @@ def change_attr_expression(frame, attr, expression, dbg, value=SENTINEL_VALUE):
if result is not dbg.plugin.EMPTY_SENTINEL:
return result
if attr[:7] == "Globals":
attr = attr[8:]
if attr[:7] == "Globals" or scope == "globals":
attr = attr[8:] if attr.startswith("Globals") else attr
if attr in frame.f_globals:
if value is SENTINEL_VALUE:
value = eval(expression, frame.f_globals, frame.f_locals)
frame.f_globals[attr] = value
return frame.f_globals[attr]
else:
raise VariableError("Attribute %s not found in globals" % attr)
else:
if "." not in attr: # i.e.: if we have a '.', we're changing some attribute of a local var.
if pydevd_save_locals.is_save_locals_available():
@ -631,8 +638,9 @@ def change_attr_expression(frame, attr, expression, dbg, value=SENTINEL_VALUE):
Exec("%s=%s" % (attr, expression), frame.f_globals, frame.f_locals)
return result
except Exception:
pydev_log.exception()
except Exception as e:
pydev_log.exception(e)
MAXIMUM_ARRAY_SIZE = 100

View file

@ -427,10 +427,10 @@ class DjangoTemplateSyntaxErrorFrame(object):
self.f_trace = None
def change_variable(frame, attr, expression, default):
def change_variable(frame, attr, expression, default, scope=None):
if isinstance(frame, DjangoTemplateFrame):
result = eval(expression, frame.f_globals, frame.f_locals)
frame._change_variable(attr, result)
frame._change_variable(attr, result, scope=scope)
return result
return default

View file

@ -249,10 +249,10 @@ class Jinja2TemplateSyntaxErrorFrame(object):
self.f_trace = None
def change_variable(frame, attr, expression, default):
def change_variable(frame, attr, expression, default, scope=None):
if isinstance(frame, Jinja2TemplateFrame):
result = eval(expression, frame.f_globals, frame.f_locals)
frame._change_variable(frame.f_back, attr, result)
frame._change_variable(frame.f_back, attr, result, scope=scope)
return result
return default

View file

@ -9,4 +9,5 @@ class SomeClass(object):
if __name__ == '__main__':
SomeClass().method()
print('second breakpoint')
print('TEST SUCEEDED')

View file

@ -5931,13 +5931,28 @@ def test_send_json_message(case_setup_dap):
def test_global_scope(case_setup_dap):
with case_setup_dap.test_file("_debugger_case_globals.py") as writer:
json_facade = JsonFacade(writer)
json_facade.write_set_breakpoints(writer.get_line_index_with_content("breakpoint here"))
break1 = writer.get_line_index_with_content("breakpoint here")
break2 = writer.get_line_index_with_content("second breakpoint")
json_facade.write_set_breakpoints([break1, break2])
json_facade.write_make_initial_run()
json_hit = json_facade.wait_for_thread_stopped()
local_var = json_facade.get_global_var(json_hit.frame_id, "in_global_scope")
assert local_var.value == "'in_global_scope_value'"
scopes_request = json_facade.write_request(pydevd_schema.ScopesRequest(pydevd_schema.ScopesArguments(json_hit.frame_id)))
scopes_response = json_facade.wait_for_response(scopes_request)
assert len(scopes_response.body.scopes) == 2
assert scopes_response.body.scopes[0]["name"] == "Locals"
assert scopes_response.body.scopes[1]["name"] == "Globals"
globals_varreference = scopes_response.body.scopes[1]["variablesReference"]
json_facade.write_set_variable(globals_varreference, "in_global_scope", "'new_value'")
json_facade.write_continue()
json_hit2 = json_facade.wait_for_thread_stopped()
global_var = json_facade.get_global_var(json_hit2.frame_id, "in_global_scope")
assert global_var.value == "'new_value'"
json_facade.write_continue()
writer.finished_ok = True