Repository: omry/omegaconf

Issue:
More helpful stack trace
It would be a good feature to have a bit more helpful stack trace. When using the Structured configurations, and you try to access a non-existent configuration key you basically get a stack trace like this:

```
~/miniconda3/lib/python3.7/site-packages/omegaconf/base.py in _format_and_raise(self, key, value, cause, type_override)
     94             msg=str(cause),
     95             cause=cause,
---> 96             type_override=type_override,
     97         )
     98         assert False

~/miniconda3/lib/python3.7/site-packages/omegaconf/_utils.py in format_and_raise(node, key, value, msg, cause, type_override)
    494         ex.ref_type_str = ref_type_str
    495
--> 496     raise ex from cause
    497
    498

ConfigAttributeError: Key 'BLA' not in 'DefaultConfig'
        full_key: BLA
        reference_type=Optional[DefaultConfig]
        object_type=DefaultConfig

        full_key: BLA
        reference_type=Optional[DefaultConfig]
        object_type=DefaultConfig

```
which does not say which line we tried to access this key.

Discussion:

Patch:
diff --git a/omegaconf/_utils.py b/omegaconf/_utils.py
index bec1fe1..1a5b762 100644
--- a/omegaconf/_utils.py
+++ b/omegaconf/_utils.py
@@ -1,3 +1,4 @@
+import os
 import re
 import string
 import sys
@@ -476,13 +477,21 @@ def format_and_raise(
         KEY_TYPE=f"{type(key).__name__}",
     )
 
-    s = string.Template(
-        """$MSG
+    full_backtrace = "OC_CAUSE" in os.environ and os.environ["OC_CAUSE"] == "1"
+    template = """$MSG
 \tfull_key: $FULL_KEY
 \treference_type=$REF_TYPE
 \tobject_type=$OBJECT_TYPE
 """
-    )
+
+    if not full_backtrace:
+        template += """
+
+Set env OC_CAUSE=1 to include causing exception
+    """
+
+    s = string.Template(template=template)
+
     message = s.substitute(
         REF_TYPE=ref_type_str, OBJECT_TYPE=object_type_str, MSG=msg, FULL_KEY=full_key,
     )
@@ -492,7 +501,7 @@ def format_and_raise(
     elif exception_type == IndexError:
         exception_type = ConfigIndexError
 
-    ex = exception_type(f"{message}").with_traceback(sys.exc_info()[2])
+    ex = exception_type(f"{message}")
     if issubclass(exception_type, OmegaConfBaseException):
         ex._initialized = True
         ex.parent_node = node
@@ -501,13 +510,16 @@ def format_and_raise(
         ex.full_key = full_key
         ex.value = value
         ex.msg = msg
-        ex.cause = cause
         ex.object_type = object_type
         ex.object_type_str = object_type_str
         ex.ref_type = ref_type
         ex.ref_type_str = ref_type_str
 
-    raise ex from cause
+    if full_backtrace:
+        ex.__cause__ = cause
+    else:
+        ex.__cause__ = None
+    raise ex
 
 
 def type_str(t: Any) -> str:
diff --git a/omegaconf/errors.py b/omegaconf/errors.py
index c43da1c..6efe646 100644
--- a/omegaconf/errors.py
+++ b/omegaconf/errors.py
@@ -24,7 +24,6 @@ class OmegaConfBaseException(Exception):
         self.full_key = None
         self.value = None
         self.msg = None
-        self.cause = None
         self.object_type = None
         self.ref_type = None
 
diff --git a/omegaconf/listconfig.py b/omegaconf/listconfig.py
index 34a59ec..cb21c94 100644
--- a/omegaconf/listconfig.py
+++ b/omegaconf/listconfig.py
@@ -24,6 +24,8 @@ from ._utils import (
 from .base import Container, ContainerMetadata, Node
 from .basecontainer import BaseContainer
 from .errors import (
+    ConfigAttributeError,
+    ConfigTypeError,
     ConfigValueError,
     KeyValidationError,
     MissingMandatoryValue,
@@ -58,14 +60,16 @@ class ListConfig(BaseContainer, MutableSequence[Any]):
                     key_type=int,
                 ),
             )
-            self._content = None
+            self.__dict__["_content"] = None
             self._set_value(value=content)
         except Exception as ex:
             format_and_raise(node=None, key=None, value=None, cause=ex, msg=str(ex))
 
     def _validate_get(self, key: Any, value: Any = None) -> None:
         if not isinstance(key, (int, slice)):
-            raise KeyValidationError("Invalid key type '$KEY_TYPE'")
+            raise KeyValidationError(
+                "ListConfig indices must be integers or slices, not $KEY_TYPE"
+            )
 
     def _validate_set(self, key: Any, value: Any) -> None:
 
@@ -98,8 +102,24 @@ class ListConfig(BaseContainer, MutableSequence[Any]):
             return 0
         if self._is_missing():
             return 0
-        assert isinstance(self._content, list)
-        return len(self._content)
+        assert isinstance(self.__dict__["_content"], list)
+        return len(self.__dict__["_content"])
+
+    def __setattr__(self, key: str, value: Any) -> None:
+        self._format_and_raise(
+            key=key,
+            value=value,
+            cause=ConfigAttributeError("ListConfig does not support attribute access"),
+        )
+        assert False
+
+    def __getattr__(self, key: str) -> None:
+        self._format_and_raise(
+            key=key,
+            value=None,
+            cause=ConfigAttributeError("ListConfig does not support attribute access"),
+        )
+        assert False
 
     def __getitem__(self, index: Union[int, slice]) -> Any:
         try:
@@ -111,19 +131,21 @@ class ListConfig(BaseContainer, MutableSequence[Any]):
                     "ListConfig object representing None is not subscriptable"
                 )
 
-            assert isinstance(self._content, list)
+            assert isinstance(self.__dict__["_content"], list)
             if isinstance(index, slice):
                 result = []
                 for slice_idx in itertools.islice(
                     range(0, len(self)), index.start, index.stop, index.step
                 ):
                     val = self._resolve_with_default(
-                        key=slice_idx, value=self._content[slice_idx]
+                        key=slice_idx, value=self.__dict__["_content"][slice_idx]
                     )
                     result.append(val)
                 return result
             else:
-                return self._resolve_with_default(key=index, value=self._content[index])
+                return self._resolve_with_default(
+                    key=index, value=self.__dict__["_content"][index]
+                )
         except Exception as e:
             self._format_and_raise(key=index, value=None, cause=e)
 
@@ -144,7 +166,7 @@ class ListConfig(BaseContainer, MutableSequence[Any]):
             self._validate_set(key=index, value=item)
 
             node = _maybe_wrap(
-                ref_type=self._metadata.element_type,
+                ref_type=self.__dict__["_metadata"].element_type,
                 key=index,
                 value=item,
                 is_optional=OmegaConf.is_optional(item),
@@ -175,11 +197,11 @@ class ListConfig(BaseContainer, MutableSequence[Any]):
                 raise MissingMandatoryValue("Cannot insert into missing ListConfig")
 
             try:
-                assert isinstance(self._content, list)
+                assert isinstance(self.__dict__["_content"], list)
                 # insert place holder
-                self._content.insert(index, None)
+                self.__dict__["_content"].insert(index, None)
                 node = _maybe_wrap(
-                    ref_type=self._metadata.element_type,
+                    ref_type=self.__dict__["_metadata"].element_type,
                     key=index,
                     value=item,
                     is_optional=OmegaConf.is_optional(item),
@@ -267,9 +289,11 @@ class ListConfig(BaseContainer, MutableSequence[Any]):
                 )
             if self._is_missing():
                 raise MissingMandatoryValue("Cannot get_node from a missing ListConfig")
-            assert isinstance(self._content, list)
-            return self._content[key]  # type: ignore
-        except (IndexError, TypeError, MissingMandatoryValue) as e:
+            assert isinstance(self.__dict__["_content"], list)
+            if validate_access:
+                self._validate_get(key)
+            return self.__dict__["_content"][key]  # type: ignore
+        except (IndexError, TypeError, MissingMandatoryValue, KeyValidationError) as e:
             if validate_access:
                 self._format_and_raise(key=key, value=None, cause=e)
                 assert False
@@ -283,9 +307,11 @@ class ListConfig(BaseContainer, MutableSequence[Any]):
             if self._is_missing():
                 raise MissingMandatoryValue("Cannot get from a missing ListConfig")
             self._validate_get(index, None)
-            assert isinstance(self._content, list)
+            assert isinstance(self.__dict__["_content"], list)
             return self._resolve_with_default(
-                key=index, value=self._content[index], default_value=default_value
+                key=index,
+                value=self.__dict__["_content"][index],
+                default_value=default_value,
             )
         except Exception as e:
             self._format_and_raise(key=index, value=None, cause=e)
@@ -300,13 +326,18 @@ class ListConfig(BaseContainer, MutableSequence[Any]):
             if self._is_missing():
                 raise MissingMandatoryValue("Cannot pop from a missing ListConfig")
 
-            assert isinstance(self._content, list)
+            assert isinstance(self.__dict__["_content"], list)
             ret = self._resolve_with_default(
                 key=index, value=self._get_node(index), default_value=None
             )
-            del self._content[index]
+            del self.__dict__["_content"][index]
             self._update_keys()
             return ret
+        except KeyValidationError as e:
+            self._format_and_raise(
+                key=index, value=None, cause=e, type_override=ConfigTypeError
+            )
+            assert False
         except Exception as e:
             self._format_and_raise(key=index, value=None, cause=e)
             assert False
@@ -332,8 +363,8 @@ class ListConfig(BaseContainer, MutableSequence[Any]):
                 def key1(x: Any) -> Any:
                     return key(x._value())  # type: ignore
 
-            assert isinstance(self._content, list)
-            self._content.sort(key=key1, reverse=reverse)
+            assert isinstance(self.__dict__["_content"], list)
+            self.__dict__["_content"].sort(key=key1, reverse=reverse)
 
         except Exception as e:
             self._format_and_raise(key=None, value=None, cause=e)
@@ -377,8 +408,8 @@ class ListConfig(BaseContainer, MutableSequence[Any]):
                         v = v._value()
                     return v
 
-            assert isinstance(self._content, list)
-            return MyItems(self._content)
+            assert isinstance(self.__dict__["_content"], list)
+            return MyItems(self.__dict__["_content"])
         except (ReadonlyConfigError, TypeError, MissingMandatoryValue) as e:
             self._format_and_raise(key=None, value=None, cause=e)
             assert False
@@ -420,13 +451,13 @@ class ListConfig(BaseContainer, MutableSequence[Any]):
             assert is_primitive_list(value) or isinstance(value, ListConfig)
             self.__dict__["_content"] = []
             if isinstance(value, ListConfig):
-                self._metadata = copy.deepcopy(value._metadata)
-                self._metadata.flags = {}
+                self.__dict__["_metadata"] = copy.deepcopy(value._metadata)
+                self.__dict__["_metadata"].flags = {}
             for item in value:
                 self.append(item)
 
             if isinstance(value, ListConfig):
-                self._metadata.flags = value._metadata.flags
+                self.__dict__["_metadata"].flags = value._metadata.flags
 
     @staticmethod
     def _list_eq(l1: Optional["ListConfig"], l2: Optional["ListConfig"]) -> bool:


Test patch:
diff --git a/tests/test_basic_ops_list.py b/tests/test_basic_ops_list.py
index f5a8a29..1566ce3 100644
--- a/tests/test_basic_ops_list.py
+++ b/tests/test_basic_ops_list.py
@@ -6,6 +6,7 @@ import pytest
 
 from omegaconf import AnyNode, ListConfig, OmegaConf
 from omegaconf.errors import (
+    ConfigTypeError,
     KeyValidationError,
     MissingMandatoryValue,
     UnsupportedValueType,
@@ -104,19 +105,12 @@ def test_list_pop() -> None:
     validate_list_keys(c)
 
 
-def test_list_pop_on_unexpected_exception_not_modifying(mocker: Any) -> None:
+def test_list_pop_on_unexpected_exception_not_modifying() -> None:
     src = [1, 2, 3, 4]
     c = OmegaConf.create(src)
 
-    def resolve_with_default_throws(
-        key: Any, value: Any, default_value: Any = None,
-    ) -> None:
-        raise Exception("mocked_exception")
-
-    mocker.patch.object(c, "_resolve_with_default", resolve_with_default_throws)
-
-    with pytest.raises(Exception, match="mocked_exception"):
-        c.pop(0)
+    with pytest.raises(ConfigTypeError):
+        c.pop("foo")  # type: ignore
     assert c == src
 
 
diff --git a/tests/test_errors.py b/tests/test_errors.py
index 2fca652..e51f15f 100644
--- a/tests/test_errors.py
+++ b/tests/test_errors.py
@@ -461,15 +461,39 @@ params = [
     ##############
     # ListConfig #
     ##############
+    # getattr
+    pytest.param(
+        Expected(
+            create=lambda: OmegaConf.create([1, 2, 3]),
+            op=lambda cfg: setattr(cfg, "foo", 10),
+            exception_type=ConfigAttributeError,
+            key="foo",
+            full_key="[foo]",
+            msg="ListConfig does not support attribute access",
+        ),
+        id="list:setattr",
+    ),
+    # # setattr
+    pytest.param(
+        Expected(
+            create=lambda: OmegaConf.create([1, 2, 3]),
+            op=lambda cfg: getattr(cfg, "foo"),
+            exception_type=ConfigAttributeError,
+            key="foo",
+            full_key="[foo]",
+            msg="ListConfig does not support attribute access",
+        ),
+        id="list:setattr",
+    ),
     # get node
     pytest.param(
         Expected(
             create=lambda: OmegaConf.create([1, 2, 3]),
             op=lambda cfg: cfg._get_node("foo"),
-            exception_type=TypeError,
+            exception_type=KeyValidationError,
             key="foo",
             full_key="[foo]",
-            msg="list indices must be integers or slices, not str",
+            msg="ListConfig indices must be integers or slices, not str",
         ),
         id="list:get_nox_ex:invalid_index_type",
     ),
@@ -542,18 +566,18 @@ params = [
             full_key="[0]",
             child_node=lambda cfg: cfg[0],
         ),
-        id="dict:readonly:pop",
+        id="list:readonly:pop",
     ),
     pytest.param(
         Expected(
-            create=lambda: create_readonly([1, 2, 3]),
+            create=lambda: OmegaConf.create([1, 2, 3]),
             op=lambda cfg: cfg.pop("Invalid_key_type"),
-            exception_type=ReadonlyConfigError,
-            msg="Cannot pop from read-only ListConfig",
+            exception_type=ConfigTypeError,
+            msg="ListConfig indices must be integers or slices, not str",
             key="Invalid_key_type",
             full_key="[Invalid_key_type]",
         ),
-        id="dict:readonly:pop",
+        id="list:pop_invalid_key",
     ),
     pytest.param(
         Expected(
@@ -618,7 +642,7 @@ params = [
             create=lambda: OmegaConf.create([1, 2, 3]),
             op=lambda cfg: cfg.__getitem__("foo"),
             exception_type=KeyValidationError,
-            msg="Invalid key type 'str'",
+            msg="ListConfig indices must be integers or slices, not str",
             key="foo",
             full_key="[foo]",
         ),
@@ -652,7 +676,7 @@ params = [
             create=lambda: OmegaConf.create([1, 2, 3]),
             op=lambda cfg: cfg.__setitem__("foo", 4),
             exception_type=KeyValidationError,
-            msg="Invalid key type 'str'",
+            msg="ListConfig indices must be integers or slices, not str",
             key="foo",
             full_key="[foo]",
         ),
@@ -873,7 +897,7 @@ def create_readonly(cfg: Any) -> Any:
 @pytest.mark.parametrize(  # type:ignore
     "expected", params,
 )
-def test_errors(expected: Expected) -> None:
+def test_errors(expected: Expected, monkeypatch: Any) -> None:
     cfg = expected.create()
     expected.finalize(cfg)
     msg = expected.msg
@@ -894,3 +918,17 @@ def test_errors(expected: Expected) -> None:
         assert isinstance(expected.full_key, str)
         assert ex.object_type_str == expected.object_type_str
         assert ex.ref_type_str == expected.ref_type_str
+
+        with monkeypatch.context() as m:
+            m.setenv("OC_CAUSE", "1")
+            try:
+                expected.op(cfg)
+            except Exception as e:
+                assert e.__cause__ is not None
+
+        with monkeypatch.context() as m:
+            m.delenv("OC_CAUSE", raising=False)
+            try:
+                expected.op(cfg)
+            except Exception as e:
+                assert e.__cause__ is None