Skip to content

Commit

Permalink
Make it possible to edit variables in containers through the DAP. Fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
fabioz committed Apr 30, 2019
1 parent eb41c04 commit 87a7663
Show file tree
Hide file tree
Showing 11 changed files with 622 additions and 150 deletions.
59 changes: 34 additions & 25 deletions src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py
Original file line number Diff line number Diff line change
Expand Up @@ -747,37 +747,46 @@ def internal_change_variable_json(py_db, request):
if hasattr(fmt, 'to_dict'):
fmt = fmt.to_dict()

# : :type frame: _FrameVariable
frame_variable = py_db.suspended_frames_manager.get_variable(variables_reference)
if hasattr(frame_variable, 'frame'):
frame = frame_variable.frame

pydevd_vars.change_attr_expression(frame, arguments.name, arguments.value, py_db)

for child_var in frame_variable.get_children_variables(fmt=fmt):
if child_var.get_name() == arguments.name:
var_data = child_var.get_var_data(fmt=fmt)
body = SetVariableResponseBody(
value=var_data['value'],
type=var_data['type'],
variablesReference=var_data.get('variablesReference'),
namedVariables=var_data.get('namedVariables'),
indexedVariables=var_data.get('indexedVariables'),
)
variables_response = pydevd_base_schema.build_response(request, kwargs={'body':body})
py_db.writer.add_command(NetCommand(CMD_RETURN, 0, variables_response, is_json=True))
break
try:
variable = py_db.suspended_frames_manager.get_variable(variables_reference)
except KeyError:
variable = None

if variable is None:
_write_variable_response(
py_db, request, value='', success=False, message='Unable to find variable container to change: %s.' % (variables_reference,))
return

child_var = variable.change_variable(arguments.name, arguments.value, py_db, fmt=fmt)

if child_var is None:
_write_variable_response(
py_db, request, value='', success=False, message='Unable to change: %s.' % (arguments.name,))
return

var_data = child_var.get_var_data(fmt=fmt)
body = SetVariableResponseBody(
value=var_data['value'],
type=var_data['type'],
variablesReference=var_data.get('variablesReference'),
namedVariables=var_data.get('namedVariables'),
indexedVariables=var_data.get('indexedVariables'),
)
variables_response = pydevd_base_schema.build_response(request, kwargs={'body':body})
py_db.writer.add_command(NetCommand(CMD_RETURN, 0, variables_response, is_json=True))


# If it's gotten here we haven't been able to evaluate it properly. Let the client know.
def _write_variable_response(py_db, request, value, success, message):
body = SetVariableResponseBody('')
variables_response = pydevd_base_schema.build_response(
request,
kwargs={
'body':body,
'success': False,
'message': 'Unable to change: %s.' % (arguments.name,)
'message': message
})
return NetCommand(CMD_RETURN, 0, variables_response, is_json=True)
cmd = NetCommand(CMD_RETURN, 0, variables_response, is_json=True)
py_db.writer.add_command(cmd)


def internal_get_frame(dbg, seq, thread_id, frame_id):
Expand Down Expand Up @@ -896,7 +905,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)
variable = frame_tracker.obtain_as_variable(expression, result, frame=frame)
var_data = variable.get_var_data(fmt=fmt)

body = pydevd_schema.EvaluateResponseBody(
Expand Down Expand Up @@ -982,7 +991,7 @@ def internal_set_expression_json(py_db, request, thread_id):

# Now that the exec is done, get the actual value changed to return.
result = pydevd_vars.evaluate_expression(py_db, frame, expression, is_exec=False)
variable = frame_tracker.obtain_as_variable(expression, result)
variable = frame_tracker.obtain_as_variable(expression, result, frame=frame)
var_data = variable.get_var_data(fmt=fmt)

body = pydevd_schema.SetExpressionResponseBody(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from _pydevd_bundle._debug_adapter import pydevd_base_schema
from _pydevd_bundle._debug_adapter.pydevd_schema import (SourceBreakpoint, ScopesResponseBody, Scope,
VariablesResponseBody, SetVariableResponseBody, ModulesResponseBody, SourceResponseBody,
GotoTargetsResponseBody, ExceptionOptions)
GotoTargetsResponseBody, ExceptionOptions, SetExpressionResponseBody)
from _pydevd_bundle._debug_adapter.pydevd_schema import CompletionsResponseBody
from _pydevd_bundle.pydevd_api import PyDevdAPI
from _pydevd_bundle.pydevd_comm_constants import (
Expand Down Expand Up @@ -637,8 +637,18 @@ def on_setexpression_request(self, py_db, request):
thread_id = py_db.suspended_frames_manager.get_thread_id_for_variable_reference(
arguments.frameId)

self.api.request_set_expression_json(
py_db, request, thread_id)
if thread_id is not None:
self.api.request_set_expression_json(py_db, request, thread_id)
else:
body = SetExpressionResponseBody('')
response = pydevd_base_schema.build_response(
request,
kwargs={
'body':body,
'success': False,
'message': 'Unable to find thread to set expression.'
})
return NetCommand(CMD_RETURN, 0, response, is_json=True)

def on_variables_request(self, py_db, request):
'''
Expand All @@ -665,7 +675,11 @@ def on_variables_request(self, py_db, request):
else:
variables = []
body = VariablesResponseBody(variables)
variables_response = pydevd_base_schema.build_response(request, kwargs={'body':body})
variables_response = pydevd_base_schema.build_response(request, kwargs={
'body':body,
'success': False,
'message': 'Unable to find thread to evaluate variable reference.'
})
return NetCommand(CMD_RETURN, 0, variables_response, is_json=True)

def on_setvariable_request(self, py_db, request):
Expand Down
39 changes: 36 additions & 3 deletions src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,23 @@ class SetResolver:
Resolves a set as dict id(object)->object
'''

def get_contents_debug_adapter_protocol(self, obj, fmt=None):
ret = []

for i, item in enumerate(obj):
ret.append((str(id(item)), item, None))

if i > MAX_ITEMS_TO_HANDLE:
ret.append((TOO_LARGE_ATTR, TOO_LARGE_MSG, None))
break

ret.append(('__len__', len(obj), partial(_apply_evaluate_name, evaluate_name='len(%s)')))
# Needed in case the class extends the built-in type and has some additional fields.
from_default_resolver = defaultResolver.get_contents_debug_adapter_protocol(obj, fmt=fmt)
if from_default_resolver:
ret = from_default_resolver + ret
return ret

def resolve(self, var, attribute):
if attribute in ('__len__', TOO_LARGE_ATTR):
return None
Expand All @@ -422,9 +439,7 @@ def resolve(self, var, attribute):

def get_dictionary(self, var):
d = {}
i = 0
for item in var:
i += 1
for i, item in enumerate(var):
d[str(id(item))] = item

if i > MAX_ITEMS_TO_HANDLE:
Expand All @@ -437,6 +452,24 @@ def get_dictionary(self, var):
d.update(additional_fields)
return d

def change_var_from_name(self, container, name, new_value):
# The name given in this case must be the id(item), so, we can actually
# iterate in the set and see which item matches the given id.

try:
# Check that the new value can actually be added to a set (i.e.: it's hashable/comparable).
set().add(new_value)
except:
return None

for item in container:
if str(id(item)) == name:
container.remove(item)
container.add(new_value)
return str(id(new_value))

return None


#=======================================================================================================================
# InstanceResolver
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
dict_iter_items
from _pydevd_bundle.pydevd_xml import get_variable_details, get_type
from _pydev_bundle.pydev_override import overrides
from _pydevd_bundle.pydevd_resolver import sorted_attributes_key
from _pydevd_bundle.pydevd_resolver import sorted_attributes_key, TOO_LARGE_ATTR
from _pydevd_bundle.pydevd_safe_repr import SafeRepr
from _pydev_bundle import pydev_log
from _pydevd_bundle import pydevd_vars
from _pydev_bundle.pydev_imports import Exec


class _AbstractVariable(object):
Expand Down Expand Up @@ -54,6 +56,9 @@ def get_var_data(self, fmt=None):
attributes.append('readOnly')
name = '(return) %s' % (name,)

elif name in (TOO_LARGE_ATTR, '__len__'):
attributes.append('readOnly')

var_data = {
'name': name,
'value': value,
Expand All @@ -65,6 +70,8 @@ def get_var_data(self, fmt=None):

if resolver is not None: # I.e.: it's a container
var_data['variablesReference'] = self.get_variable_reference()
else:
var_data['variablesReference'] = 0 # It's mandatory (although if == 0 it doesn't have children).

if len(attributes) > 0:
var_data['presentationHint'] = {'attributes': attributes}
Expand All @@ -74,11 +81,18 @@ def get_var_data(self, fmt=None):
def get_children_variables(self, fmt=None):
raise NotImplementedError()

def get_child_variable_named(self, name, fmt=None):
for child_var in self.get_children_variables(fmt=fmt):
if child_var.get_name() == name:
return child_var
return None


class _ObjectVariable(_AbstractVariable):

def __init__(self, name, value, register_variable, is_return_value=False, evaluate_name=None):
def __init__(self, name, value, register_variable, is_return_value=False, evaluate_name=None, frame=None):
_AbstractVariable.__init__(self)
self.frame = frame
self.name = name
self.value = value
self._register_variable = register_variable
Expand Down Expand Up @@ -112,16 +126,57 @@ def get_children_variables(self, fmt=None):
else:
evaluate_name = parent_evaluate_name + evaluate_name
variable = _ObjectVariable(
key, val, self._register_variable, evaluate_name=evaluate_name)
key, val, self._register_variable, evaluate_name=evaluate_name, frame=self.frame)
children_variables.append(variable)
else:
for key, val, evaluate_name in lst:
# No evaluate name
variable = _ObjectVariable(key, val, self._register_variable)
variable = _ObjectVariable(key, val, self._register_variable, frame=self.frame)
children_variables.append(variable)

return children_variables

def change_variable(self, name, value, py_db, fmt=None):

children_variable = self.get_child_variable_named(name)
if children_variable is None:
return None

var_data = children_variable.get_var_data()
evaluate_name = var_data.get('evaluateName')

if not evaluate_name:
# Note: right now we only pass control to the resolver in the cases where
# there's no evaluate name (the idea being that if we can evaluate it,
# we can use that evaluation to set the value too -- if in the future
# a case where this isn't true is found this logic may need to be changed).
_type, _type_name, container_resolver = get_type(self.value)
if hasattr(container_resolver, 'change_var_from_name'):
try:
new_value = eval(value)
except:
return None
new_key = container_resolver.change_var_from_name(self.value, name, new_value)
if new_key is not None:
return _ObjectVariable(
new_key, new_value, self._register_variable, evaluate_name=None, frame=self.frame)

return None
else:
return None

frame = self.frame
if frame is None:
return None

try:
# This handles the simple cases (such as dict, list, object)
Exec('%s=%s' % (evaluate_name, value), frame.f_globals, frame.f_locals)
except:
return None

return self.get_child_variable_named(name, fmt=fmt)


def sorted_variables_key(obj):
return sorted_attributes_key(obj.name)
Expand All @@ -139,6 +194,13 @@ def __init__(self, frame, register_variable):
self._register_variable = register_variable
self._register_variable(self)

def change_variable(self, name, value, py_db, fmt=None):
frame = self.frame

pydevd_vars.change_attr_expression(frame, name, value, py_db)

return self.get_child_variable_named(name, fmt=fmt)

@overrides(_AbstractVariable.get_children_variables)
def get_children_variables(self, fmt=None):
children_variables = []
Expand All @@ -147,10 +209,10 @@ def get_children_variables(self, fmt=None):
if is_return_value:
for return_key, return_value in dict_iter_items(val):
variable = _ObjectVariable(
return_key, return_value, self._register_variable, is_return_value, '%s[%r]' % (key, return_key))
return_key, return_value, self._register_variable, is_return_value, '%s[%r]' % (key, return_key), frame=self.frame)
children_variables.append(variable)
else:
variable = _ObjectVariable(key, val, self._register_variable, is_return_value, key)
variable = _ObjectVariable(key, val, self._register_variable, is_return_value, key, frame=self.frame)
children_variables.append(variable)

# Frame variables always sorted.
Expand Down Expand Up @@ -199,7 +261,7 @@ def _register_variable(self, variable):
variable_reference = variable.get_variable_reference()
self._variable_reference_to_variable[variable_reference] = variable

def obtain_as_variable(self, name, value, evaluate_name=None):
def obtain_as_variable(self, name, value, evaluate_name=None, frame=None):
if evaluate_name is None:
evaluate_name = name

Expand All @@ -210,7 +272,7 @@ def obtain_as_variable(self, name, value, evaluate_name=None):

# Still not created, let's do it now.
return _ObjectVariable(
name, value, self._register_variable, is_return_value=False, evaluate_name=evaluate_name)
name, value, self._register_variable, is_return_value=False, evaluate_name=evaluate_name, frame=frame)

def get_main_thread_id(self):
return self._main_thread_id
Expand Down
Loading

0 comments on commit 87a7663

Please sign in to comment.