Skip to content

Commit

Permalink
Add object id if variable name would be duplicate. Fixes #148
Browse files Browse the repository at this point in the history
  • Loading branch information
fabioz committed Jan 23, 2021
1 parent c265d74 commit cbcfe22
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 3 deletions.
45 changes: 43 additions & 2 deletions src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,30 @@ def get_contents_debug_adapter_protocol(self, obj, fmt=None):
return obj.get_contents_debug_adapter_protocol()


_basic_immutable_types = (int, float, complex, str, bytes, type(None), bool, frozenset)
try:
_basic_immutable_types += (long, unicode) # Py2 types
except NameError:
pass


def _does_obj_repr_evaluate_to_obj(obj):
'''
If obj is an object where evaluating its representation leads to
the same object, return True, otherwise, return False.
'''
try:
if isinstance(obj, tuple):
for o in obj:
if not _does_obj_repr_evaluate_to_obj(o):
return False
return True
else:
return isinstance(obj, _basic_immutable_types)
except:
return False


#=======================================================================================================================
# DictResolver
#=======================================================================================================================
Expand Down Expand Up @@ -267,11 +291,28 @@ def get_contents_debug_adapter_protocol(self, dct, fmt=None):
ret = []

i = 0

found_representations = set()

for key, val in dict_iter_items(dct):
i += 1
key_as_str = self.key_to_str(key, fmt)
eval_key_str = self.key_to_str(key) # do not format the key
ret.append((key_as_str, val, '[%s]' % (eval_key_str,)))

if key_as_str not in found_representations:
found_representations.add(key_as_str)
else:
# If the key would be a duplicate, add the key id (otherwise
# VSCode won't show all keys correctly).
# See: https://github.com/microsoft/debugpy/issues/148
key_as_str = '%s (id: %s)' % (key_as_str, id(key))
found_representations.add(key_as_str)

if _does_obj_repr_evaluate_to_obj(key):
s = self.key_to_str(key) # do not format the key
eval_key_str = '[%s]' % (s,)
else:
eval_key_str = None
ret.append((key_as_str, val, eval_key_str))
if i > MAX_ITEMS_TO_HANDLE:
ret.append((TOO_LARGE_ATTR, TOO_LARGE_MSG, None))
break
Expand Down
2 changes: 1 addition & 1 deletion src/debugpy/_vendored/pydevd/setup_cython.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def build_extension(dir_name, extension_name, target_pydevd_name, force_cython,
Extension(
"%s%s.%s" % (dir_name, "_ext" if extended else "", target_pydevd_name,),
c_files,
**kwargs,
**kwargs
)]

# This is needed in CPython 3.8 to be able to include internal/pycore_pystate.h
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class T:

def __init__(self, name, value):
self.name = name
self.value = value

def __repr__(self):
return self.name


td = {T("foo", 24): "bar",
T("gad", 42): "zooks",
T("foo", 12): "bur"}

print('TEST SUCEEDED!') # Break here
86 changes: 86 additions & 0 deletions src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -1606,6 +1606,92 @@ def test_stack_and_variables_dict(case_setup):
writer.finished_ok = True


def test_variables_with_same_name(case_setup):
with case_setup.test_file('_debugger_case_variables_with_same_name.py') as writer:
json_facade = JsonFacade(writer)

writer.write_add_breakpoint(writer.get_line_index_with_content('Break here'))
json_facade.write_make_initial_run()

json_hit = json_facade.wait_for_thread_stopped()
json_hit = json_facade.get_stack_as_json_hit(json_hit.thread_id)

variables_response = json_facade.get_variables_response(json_hit.frame_id)

variables_references = json_facade.pop_variables_reference(variables_response.body.variables)
dict_variable_reference = variables_references[0]
assert isinstance(dict_variable_reference, int_types)
# : :type variables_response: VariablesResponse

assert variables_response.body.variables == [
{'name': 'td', 'value': "{foo: 'bar', gad: 'zooks', foo: 'bur'}", 'type': 'dict', 'evaluateName': 'td'}
]

dict_variables_response = json_facade.get_variables_response(dict_variable_reference)
# Note that we don't have the evaluateName because it's not possible to create a key
# from the user object to actually get its value from the dict in this case.
variables = dict_variables_response.body.variables[:]

found_foo = False
found_foo_with_id = False
for v in variables:
if v['name'].startswith('foo'):
if not found_foo:
assert v['name'] == 'foo'
found_foo = True
else:
assert v['name'].startswith('foo (id: ')
v['name'] = 'foo'
found_foo_with_id = True

assert found_foo
assert found_foo_with_id

def compute_key(entry):
return (entry['name'], entry['value'])

# Sort because the order may be different on Py2/Py3.
assert sorted(variables, key=compute_key) == sorted([
{
'name': 'foo',
'value': "'bar'",
'type': 'str',
'variablesReference': 0,
'presentationHint': {'attributes': ['rawString']}
},

{
# 'name': 'foo (id: 2699272929584)', In the code above we changed this
# to 'name': 'foo' for the comparisson.
'name': 'foo',
'value': "'bur'",
'type': 'str',
'variablesReference': 0,
'presentationHint': {'attributes': ['rawString']}
},

{
'name': 'gad',
'value': "'zooks'",
'type': 'str',
'variablesReference': 0,
'presentationHint': {'attributes': ['rawString']}
},

{
'name': 'len()',
'value': '3',
'type': 'int',
'evaluateName': 'len(td)',
'variablesReference': 0,
'presentationHint': {'attributes': ['readOnly']}
},
], key=compute_key)

json_facade.write_continue()
writer.finished_ok = True


def test_hasattr_failure(case_setup):
with case_setup.test_file('_debugger_case_hasattr_crash.py') as writer:
json_facade = JsonFacade(writer)
Expand Down

0 comments on commit cbcfe22

Please sign in to comment.