Repository: microsoft/ptvsd

Issue:
AttributeError: 'tuple' object has no attribute 'startswith'
## Environment data

- PTVSD version: master
- OS and version: windows
- Python version (& distribution if applicable, e.g. Anaconda): 3.7
- Using VS Code or Visual Studio: VSC

## Actual behavior

```
Traceback (most recent call last):
  File "C:\GIT\ptvsd\src\ptvsd\_vendored\pydevd\_pydevd_bundle\pydevd_frame.py", line 271, in handle_exception
    self.do_wait_suspend(thread, frame, event, arg)
  File "C:\GIT\ptvsd\src\ptvsd\_vendored\pydevd\_pydevd_bundle\pydevd_frame.py", line 78, in do_wait_suspend
    self._args[0].do_wait_suspend(*args, **kwargs)
  File "C:\GIT\ptvsd\src\ptvsd\_vendored\pydevd\pydevd.py", line 1265, in do_wait_suspend
    self._do_wait_suspend(thread, frame, event, arg, suspend_type, from_this_thread, frames_tracker)
  File "C:\GIT\ptvsd\src\ptvsd\_vendored\pydevd\pydevd.py", line 1281, in _do_wait_suspend
    self.process_internal_commands()
  File "C:\GIT\ptvsd\src\ptvsd\_vendored\pydevd\pydevd.py", line 1004, in process_internal_commands
    int_cmd.do_it(self)
  File "C:\GIT\ptvsd\src\ptvsd\_vendored\pydevd\_pydevd_bundle\pydevd_comm.py", line 484, in do_it
    self.method(dbg, *self.args, **self.kwargs)
  File "C:\GIT\ptvsd\src\ptvsd\_vendored\pydevd\_pydevd_bundle\pydevd_comm.py", line 654, in internal_get_variable_json
    for child_var in variable.get_children_variables(fmt=fmt):
  File "C:\GIT\ptvsd\src\ptvsd\_vendored\pydevd\_pydevd_bundle\pydevd_suspended_frames.py", line 97, in get_children_variables
    lst = resolver.get_contents_debug_adapter_protocol(self.value)
  File "C:\GIT\ptvsd\src\ptvsd\_vendored\pydevd\_pydevd_bundle\pydevd_resolver.py", line 93, in get_contents_debug_adapter_protocol
    lst = sorted(dict_iter_items(dct), key=sorted_attributes_key)
  File "C:\GIT\ptvsd\src\ptvsd\_vendored\pydevd\_pydevd_bundle\pydevd_resolver.py", line 65, in sorted_attributes_key
    if attr_name.startswith('__'):
AttributeError: 'tuple' object has no attribute 'startswith'
```



Discussion:
Will take a look at this.

Resolving patch:
diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_resolver.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_resolver.py
--- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_resolver.py
+++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_resolver.py
@@ -89,17 +89,24 @@ def resolve(self, var, attribute):
         return getattr(var, attribute)
 
     def get_contents_debug_adapter_protocol(self, obj):
-        dct = self.get_dictionary(obj)
-        lst = sorted(dict_iter_items(dct), key=sorted_attributes_key)
-        return [(attr_name, attr_value, '.%s' % attr_name) for (attr_name, attr_value) in lst]
+        if MethodWrapperType:
+            dct, used___dict__ = self._get_py_dictionary(obj)
+        else:
+            dct = self._get_jy_dictionary(obj)[0]
 
-    def get_dictionary(self, var, names=None):
+        lst = sorted(dict_iter_items(dct), key=lambda tup: sorted_attributes_key(tup[0]))
+        if used___dict__:
+            return [(attr_name, attr_value, '.__dict__[%s]' % attr_name) for (attr_name, attr_value) in lst]
+        else:
+            return [(attr_name, attr_value, '.%s' % attr_name) for (attr_name, attr_value) in lst]
+
+    def get_dictionary(self, var, names=None, used___dict__=False):
         if MethodWrapperType:
-            return self._getPyDictionary(var, names)
+            return self._get_py_dictionary(var, names, used___dict__=used___dict__)[0]
         else:
-            return self._getJyDictionary(var)
+            return self._get_jy_dictionary(var)[0]
 
-    def _getJyDictionary(self, obj):
+    def _get_jy_dictionary(self, obj):
         ret = {}
         found = java.util.HashMap()
 
@@ -156,45 +163,63 @@ def _getJyDictionary(self, obj):
         return ret
 
     def get_names(self, var):
-        names = dir(var)
-        if not names and hasattr(var, '__members__'):
-            names = var.__members__
-        return names
+        used___dict__ = False
+        try:
+            names = dir(var)
+        except TypeError:
+            names = []
+        if not names:
+            if hasattr(var, '__dict__'):
+                names = dict_keys(var.__dict__)
+                used___dict__ = True
+        return names, used___dict__
+
+    def _get_py_dictionary(self, var, names=None, used___dict__=False):
+        '''
+        :return tuple(names, used___dict__), where used___dict__ means we have to access
+        using obj.__dict__[name] instead of getattr(obj, name)
+        '''
 
-    def _getPyDictionary(self, var, names=None):
-        filterPrivate = False
-        filterSpecial = True
-        filterFunction = True
-        filterBuiltIn = True
+        # TODO: Those should be options (would fix https://github.com/Microsoft/ptvsd/issues/66).
+        filter_private = False
+        filter_special = True
+        filter_function = True
+        filter_builtin = True
 
         if not names:
-            names = self.get_names(var)
+            names, used___dict__ = self.get_names(var)
         d = {}
 
         # Be aware that the order in which the filters are applied attempts to
         # optimize the operation by removing as many items as possible in the
         # first filters, leaving fewer items for later filters
 
-        if filterBuiltIn or filterFunction:
-            for n in names:
-                if filterSpecial:
-                    if n.startswith('__') and n.endswith('__'):
-                        continue
+        if filter_builtin or filter_function:
+            for name in names:
+                try:
+                    name_as_str = name
+                    if name_as_str.__class__ != str:
+                        name_as_str = '%r' % (name_as_str,)
 
-                if filterPrivate:
-                    if n.startswith('_') or n.endswith('__'):
-                        continue
+                    if filter_special:
+                        if name_as_str.startswith('__') and name_as_str.endswith('__'):
+                            continue
 
-                try:
-                    attr = getattr(var, n)
+                    if filter_private:
+                        if name_as_str.startswith('_') or name_as_str.endswith('__'):
+                            continue
+                    if not used___dict__:
+                        attr = getattr(var, name)
+                    else:
+                        attr = var.__dict__[name]
 
                     # filter builtins?
-                    if filterBuiltIn:
+                    if filter_builtin:
                         if inspect.isbuiltin(attr):
                             continue
 
                     # filter functions?
-                    if filterFunction:
+                    if filter_function:
                         if inspect.isroutine(attr) or isinstance(attr, MethodWrapperType):
                             continue
                 except:
@@ -203,9 +228,9 @@ def _getPyDictionary(self, var, names=None):
                     traceback.print_exc(file=strIO)
                     attr = strIO.getvalue()
 
-                d[ n ] = attr
+                d[name_as_str] = attr
 
-        return d
+        return d, used___dict__
 
 
 #=======================================================================================================================
@@ -469,22 +494,18 @@ def resolve(self, dict, key):
 # DjangoFormResolver
 #=======================================================================================================================
 class DjangoFormResolver(DefaultResolver):
-    has_errors_attr = False
 
-    def get_names(self, var):
-        names = dir(var)
-        if not names and hasattr(var, '__members__'):
-            names = var.__members__
+    def get_dictionary(self, var, names=None):
+        # Do not call self.errors because it is a property and has side effects.
+        names, used___dict__ = self.get_names(var)
 
+        has_errors_attr = False
         if "errors" in names:
-            self.has_errors_attr = True
+            has_errors_attr = True
             names.remove("errors")
-        return names
 
-    def get_dictionary(self, var, names=None):
-        # Do not call self.errors because it is property and has side effects
-        d = defaultResolver.get_dictionary(var, self.get_names(var))
-        if self.has_errors_attr:
+        d = defaultResolver.get_dictionary(var, names=names, used___dict__=used___dict__)
+        if has_errors_attr:
             try:
                 errors_attr = getattr(var, "_errors")
             except:


Test patch:
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/test_resolvers.py b/src/ptvsd/_vendored/pydevd/tests_python/test_resolvers.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/test_resolvers.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/test_resolvers.py
@@ -14,6 +14,111 @@ def test_dict_resolver():
             ("'22'", 22, "['22']"), ('(1, 2)', 2, '[(1, 2)]'), ('__len__', 2, '.__len__()')]
 
 
+def test_object_resolver_simple():
+    from _pydevd_bundle.pydevd_resolver import DefaultResolver
+    default_resolver = DefaultResolver()
+
+    class MyObject(object):
+
+        def __init__(self):
+            self.a = 10
+            self.b = 20
+
+    obj = MyObject()
+    dictionary = default_resolver.get_dictionary(obj)
+    assert dictionary == {'a': 10, 'b': 20}
+
+    contents_debug_adapter_protocol = default_resolver.get_contents_debug_adapter_protocol(obj)
+    assert contents_debug_adapter_protocol == [('a', 10, '.a'), ('b', 20, '.b')]
+
+
+def test_object_resolver_error():
+    from _pydevd_bundle.pydevd_resolver import DefaultResolver
+    default_resolver = DefaultResolver()
+
+    class MyObject(object):
+
+        def __init__(self):
+            self.a = 10
+
+        def __dir__(self):
+            return ['a', 'b']
+
+        def __getattribute__(self, attr_name):
+            if attr_name == 'b':
+                raise RuntimeError('unavailable')
+            return object.__getattribute__(self, attr_name)
+
+    obj = MyObject()
+    dictionary = default_resolver.get_dictionary(obj)
+    b_value = dictionary.pop('b')
+    assert dictionary == {'a': 10}
+    assert "raise RuntimeError('unavailable')" in b_value
+
+    contents_debug_adapter_protocol = default_resolver.get_contents_debug_adapter_protocol(obj)
+    b_value = contents_debug_adapter_protocol.pop(-1)
+    assert contents_debug_adapter_protocol == [('a', 10, '.a')]
+    assert b_value[0] == 'b'
+    assert "raise RuntimeError('unavailable')" in b_value[1]
+    assert b_value[2] == '.b'
+
+
+def test_object_resolver__dict__non_strings():
+    from _pydevd_bundle.pydevd_resolver import DefaultResolver
+    default_resolver = DefaultResolver()
+
+    class MyObject(object):
+
+        def __init__(self):
+            self.__dict__[(1, 2)] = (3, 4)
+
+    obj = MyObject()
+    dictionary = default_resolver.get_dictionary(obj)
+    if IS_PY2:
+        assert 'attribute name must be string' in dictionary.pop('(1, 2)')
+        assert dictionary == {}
+    else:
+        assert dictionary == {'(1, 2)': (3, 4)}
+
+    contents_debug_adapter_protocol = default_resolver.get_contents_debug_adapter_protocol(obj)
+    if IS_PY2:
+        assert len(contents_debug_adapter_protocol) == 1
+        entry = contents_debug_adapter_protocol[0]
+        assert entry[0] == '(1, 2)'
+        assert 'attribute name must be string' in entry[1]
+        assert entry[2] == '.(1, 2)'
+    else:
+        assert contents_debug_adapter_protocol == [('(1, 2)', (3, 4), '.__dict__[(1, 2)]')]
+
+
+def test_django_forms_resolver():
+    from _pydevd_bundle.pydevd_resolver import DjangoFormResolver
+    django_form_resolver = DjangoFormResolver()
+
+    class MyObject(object):
+
+        def __init__(self):
+            self.__dict__[(1, 2)] = (3, 4)
+            self.__dict__['errors'] = 'foo'
+
+    obj = MyObject()
+
+    dictionary = django_form_resolver.get_dictionary(obj)
+    if IS_PY2:
+        assert 'attribute name must be string' in dictionary.pop('(1, 2)')
+        assert dictionary == {'errors': None}
+    else:
+        assert dictionary == {'(1, 2)': (3, 4), 'errors': None}
+
+    obj._errors = 'bar'
+    dictionary = django_form_resolver.get_dictionary(obj)
+    if IS_PY2:
+        assert 'attribute name must be string' in dictionary.pop('(1, 2)')
+        assert dictionary == {'errors': 'bar', '_errors': 'bar'}
+    else:
+        assert dictionary == {'(1, 2)': (3, 4), 'errors': 'bar', '_errors': 'bar'}
+
+
 def test_tuple_resolver():
     from _pydevd_bundle.pydevd_resolver import TupleResolver
     tuple_resolver = TupleResolver()