From cbcfe221d7b5048cf101d7a3539a684c2c70e2f5 Mon Sep 17 00:00:00 2001 From: Fabio Zadrozny Date: Fri, 22 Jan 2021 15:24:55 -0300 Subject: [PATCH] Add object id if variable name would be duplicate. Fixes #148 --- .../pydevd/_pydevd_bundle/pydevd_resolver.py | 45 +++++++++- src/debugpy/_vendored/pydevd/setup_cython.py | 2 +- ..._debugger_case_variables_with_same_name.py | 15 ++++ .../pydevd/tests_python/test_debugger_json.py | 86 +++++++++++++++++++ 4 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_variables_with_same_name.py diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_resolver.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_resolver.py index 9279f262..5fa8c0dd 100644 --- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_resolver.py +++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_resolver.py @@ -215,6 +215,30 @@ class DAPGrouperResolver: 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 #======================================================================================================================= @@ -267,11 +291,28 @@ class DictResolver: 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 diff --git a/src/debugpy/_vendored/pydevd/setup_cython.py b/src/debugpy/_vendored/pydevd/setup_cython.py index b7e38e27..85a4aa80 100644 --- a/src/debugpy/_vendored/pydevd/setup_cython.py +++ b/src/debugpy/_vendored/pydevd/setup_cython.py @@ -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 diff --git a/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_variables_with_same_name.py b/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_variables_with_same_name.py new file mode 100644 index 00000000..196f15eb --- /dev/null +++ b/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_variables_with_same_name.py @@ -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 diff --git a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py index ce082dd6..6364561c 100644 --- a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py +++ b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py @@ -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)