Repository: omry/omegaconf

Issue:
improve exception message to contain full key when creatning a config with OmegaConf.create() that has an illegal value
```
# TODO: improve exception message to contain full key a.b
def test_create_nested_dict_with_illegal_value():
```

Discussion:
deepcode-ci-bot[bot]: ## DeepCode's analysis on [#3cb92b](https://www.deepcode.ai/app/gh/omry/omegaconf/cffc6940307be4da09d1683a676b30dc35abb5b7/omry/omegaconf/3cb92bec237445f824af70ecd7138a4130f51c53/_/%2F/code/?utm_source=gh_review) found:

- :x: **0** critical issues.  :warning: **0** warnings  and **2** minor issues.  :heavy_check_mark: **2** issues were fixed.

:speech_balloon: This comment has been generated by the [**DeepCode bot**](https://github.com/apps/deepcode-ci-bot/), installed by the owner of the repository. The DeepCode bot **protects** your repository by detecting and commenting on security vulnerabilities or other critical issues.

---
:relaxed: If you want to provide feedback on our bot, here is how to [contact us](https://www.deepcode.ai/feedback?select=4&utm_source=gh_review).


Patch:
diff --git a/news/14.bugfix b/news/14.bugfix
new file mode 100644
index 0000000..75b9786
--- /dev/null
+++ b/news/14.bugfix
@@ -0,0 +1,1 @@
+Improved exception value when creating an config with an illegal value to contain full key path
diff --git a/omegaconf/base.py b/omegaconf/base.py
index 1ad6ea3..abb1485 100644
--- a/omegaconf/base.py
+++ b/omegaconf/base.py
@@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
 from collections import defaultdict
 from dataclasses import dataclass, field
 from enum import Enum
-from typing import Any, Dict, Iterator, Optional, Type, Union
+from typing import Any, Dict, Iterator, Optional, Tuple, Type, Union
 
 from ._utils import ValueKind, _get_value, get_value_kind
 from .errors import MissingMandatoryValue, UnsupportedInterpolationType
@@ -79,48 +79,56 @@ class Node(ABC):
             # noinspection PyProtectedMember
             return parent._get_flag(flag)
 
-    # TODO: simplify now that each node has it's own key
     def _get_full_key(self, key: Union[str, Enum, int]) -> str:
-        from .dictconfig import DictConfig
         from .listconfig import ListConfig
+        from .omegaconf import _select_one
 
-        full_key = ""
-        child = None
-        cur: Optional[Node] = self
-        while cur is not None:
-            if isinstance(cur, DictConfig):
-                if child is None:
-                    full_key = "{}".format(key)
-                else:
-                    # find which the key for child in the parent
-                    for parent_key in cur.keys():
-                        if id(cur.get_node(parent_key)) == id(child):
-                            if isinstance(child, ListConfig):
-                                full_key = "{}{}".format(parent_key, full_key)
-                            else:
-                                if full_key == "":
-                                    full_key = parent_key
-                                else:
-                                    full_key = "{}.{}".format(parent_key, full_key)
-                            break
-            elif isinstance(cur, ListConfig):
-                if child is None:
-                    if key == "":
-                        full_key = f"{key}"
+        def prepand(full_key: str, parent_type: Any, cur_type: Any, key: Any) -> str:
+            if issubclass(parent_type, ListConfig):
+                if full_key != "":
+                    if issubclass(cur_type, ListConfig):
+                        full_key = f"[{key}]{full_key}"
                     else:
-                        full_key = f"[{key}]"
+                        full_key = f"[{key}].{full_key}"
+                else:
+                    full_key = f"[{key}]"
+            else:
+                if full_key == "":
+                    full_key = key
                 else:
-                    for idx, v in enumerate(cur):
-                        if id(v) == id(child):
-                            if isinstance(child, ListConfig):
-                                full_key = "[{}]{}".format(idx, full_key)
-                            else:
-                                full_key = "[{}].{}".format(idx, full_key)
-                            break
-            child = cur
-            parent = child._get_parent()
-            assert parent is None or isinstance(parent, Container)
-            cur = parent
+                    if issubclass(cur_type, ListConfig):
+                        full_key = f"{key}{full_key}"
+                    else:
+                        full_key = f"{key}.{full_key}"
+            return full_key
+
+        if key is not None and key != "":
+            assert isinstance(self, Container)
+            cur, _ = _select_one(c=self, key=str(key), throw_on_missing=False)
+            if cur is None:
+                cur = self
+                full_key = prepand("", type(cur), None, key)
+                if cur._key() is not None:
+                    full_key = prepand(
+                        full_key, type(cur._get_parent()), type(cur), cur._key()
+                    )
+            else:
+                full_key = prepand("", type(cur._get_parent()), type(cur), cur._key())
+        else:
+            cur = self
+            if cur._key() is None:
+                return ""
+            full_key = self._key()
+
+        assert cur is not None
+        while cur._get_parent() is not None:
+            cur = cur._get_parent()
+            assert cur is not None
+            key = cur._key()
+            if key is not None:
+                full_key = prepand(
+                    full_key, type(cur._get_parent()), type(cur), cur._key()
+                )
 
         return full_key
 
@@ -157,11 +165,7 @@ class Node(ABC):
         else:
             # not interpolation, compare directly
             if throw_on_missing:
-
-                if isinstance(self, Container):
-                    value = self.__dict__["_content"]  # TODO normalize
-                else:
-                    value = self._value()
+                value = self._value()
                 if value == "???":
                     raise MissingMandatoryValue(self._get_full_key(""))
             return self
@@ -225,7 +229,7 @@ class Container(Node):
         ...  # pragma: no cover
 
     @abstractmethod
-    def select(self, key: str) -> Any:
+    def select(self, key: str, throw_on_missing: bool = False) -> Any:
         ...  # pragma: no cover
 
     def get_node(self, key: Any) -> Node:
@@ -257,6 +261,41 @@ class Container(Node):
             assert root is not None and isinstance(root, Container)
         return root
 
+    def _select_impl(
+        self, key: str, throw_on_missing: bool
+    ) -> Tuple[Optional["Container"], Optional[str], Optional[Node]]:
+        """
+        Select a value using dot separated key sequence
+        :param key:
+        :return:
+        """
+        from .omegaconf import _select_one
+
+        if key == "":
+            return self, "", self
+
+        split = key.split(".")
+        root: Optional[Container] = self
+        for i in range(len(split) - 1):
+            if root is None:
+                break
+            k = split[i]
+            ret, _ = _select_one(c=root, key=k, throw_on_missing=throw_on_missing)
+            assert ret is None or isinstance(ret, Container)
+            root = ret
+
+        if root is None:
+            return None, None, None
+
+        last_key = split[-1]
+        value, _ = _select_one(c=root, key=last_key, throw_on_missing=throw_on_missing)
+        if value is None:
+            return root, last_key, value
+        value = root._resolve_str_interpolation(
+            key=last_key, value=value, throw_on_missing=False
+        )
+        return root, last_key, value
+
     def _resolve_interpolation(
         self, key: Any, inter_type: str, inter_key: str, throw_on_missing: bool,
     ) -> "Node":
@@ -268,20 +307,21 @@ class Container(Node):
 
         inter_type = ("str:" if inter_type is None else inter_type)[0:-1]
         if inter_type == "str":
-            parent, last_key, value = root_node._select_impl(inter_key)  # type: ignore
-            if parent is None or (value is None and last_key not in parent):
+            parent, last_key, value = root_node._select_impl(
+                inter_key, throw_on_missing=throw_on_missing
+            )
+
+            if parent is None or (value is None and last_key not in parent):  # type: ignore
                 raise KeyError(
                     "{} interpolation key '{}' not found".format(inter_type, inter_key)
                 )
-            if throw_on_missing and isinstance(value, Node) and value._is_missing():
-                raise MissingMandatoryValue(parent._get_full_key(str(key)))
             assert isinstance(value, Node)
             return value
         else:
             resolver = OmegaConf.get_resolver(inter_type)
             if resolver is not None:
                 value = resolver(root_node, inter_key)
-                assert isinstance(self, Container)
+
                 node = self.get_node(key)
                 return ValueNode(
                     value=value,
diff --git a/omegaconf/basecontainer.py b/omegaconf/basecontainer.py
index 583481f..83b41ef 100644
--- a/omegaconf/basecontainer.py
+++ b/omegaconf/basecontainer.py
@@ -129,7 +129,7 @@ class BaseContainer(Container, ABC):
         for i in range(len(split) - 1):
             k = split[i]
             # if next_root is a primitive (string, int etc) replace it with an empty map
-            next_root, key_ = _select_one(root, k)
+            next_root, key_ = _select_one(root, k, throw_on_missing=False)
             if not isinstance(next_root, Container):
                 root[key_] = {}
             root = root[key_]
@@ -146,44 +146,15 @@ class BaseContainer(Container, ABC):
             idx = int(last)
             root[idx] = value
 
-    def select(self, key: str) -> Any:
-        _root, _last_key, value = self._select_impl(key)
-        return _get_value(value)
-
-    def _select_impl(
-        self, key: str
-    ) -> Tuple[Optional[Container], Optional[str], Optional[Node]]:
-        """
-        Select a value using dot separated key sequence
-        :param key:
-        :return:
-        """
-        from .omegaconf import _select_one
-
-        if key == "":
-            return self, "", self
-
-        split = key.split(".")
-        root: Optional[Container] = self
-        for i in range(len(split) - 1):
-            if root is None:
-                break
-            k = split[i]
-            ret, _ = _select_one(root, k)
-            assert ret is None or isinstance(ret, Container)
-            root = ret
-
-        if root is None:
-            return None, None, None
+    def select(self, key: str, throw_on_missing: bool = False) -> Any:
+        _root, _last_key, value = self._select_impl(key, throw_on_missing=False)
+        if value is not None and value._is_missing():
+            if throw_on_missing:
+                raise MissingMandatoryValue(value._get_full_key(""))
+            else:
+                return None
 
-        last_key = split[-1]
-        value, _ = _select_one(root, last_key)
-        if value is None:
-            return root, last_key, value
-        value = self._resolve_str_interpolation(
-            key=last_key, value=value, throw_on_missing=False
-        )
-        return root, last_key, value
+        return _get_value(value)
 
     def is_empty(self) -> bool:
         """return true if config is empty"""
diff --git a/omegaconf/dictconfig.py b/omegaconf/dictconfig.py
index 2b7f47c..2d64bc9 100644
--- a/omegaconf/dictconfig.py
+++ b/omegaconf/dictconfig.py
@@ -377,7 +377,7 @@ class DictConfig(BaseContainer, MutableMapping[str, Any]):
     def __hash__(self) -> int:
         return hash(str(self))
 
-    def _promote(self, type_or_prototype: Type[Any]) -> None:
+    def _promote(self, type_or_prototype: Optional[Type[Any]]) -> None:
         """
         Retypes a node.
         This should only be used in rare circumstances, where you want to dynamically change
@@ -427,9 +427,12 @@ class DictConfig(BaseContainer, MutableMapping[str, Any]):
 
             self._metadata.object_type = None
             self.__dict__["_content"] = {}
-
-            for k, v in value.items():
-                self.__setitem__(k, v)
+            if isinstance(value, DictConfig):
+                for k, v in value.items_ex(resolve=False):
+                    self.__setitem__(k, v)
+            else:
+                for k, v in value.items():
+                    self.__setitem__(k, v)
 
             if is_structured:
                 self._metadata.object_type = _type
diff --git a/omegaconf/omegaconf.py b/omegaconf/omegaconf.py
index a405be1..ee31bbf 100644
--- a/omegaconf/omegaconf.py
+++ b/omegaconf/omegaconf.py
@@ -42,7 +42,7 @@ from ._utils import (
 )
 from .base import Container, Node
 from .basecontainer import BaseContainer
-from .errors import UnsupportedInterpolationType, ValidationError
+from .errors import MissingMandatoryValue, UnsupportedInterpolationType, ValidationError
 from .nodes import (
     AnyNode,
     BooleanNode,
@@ -613,7 +613,9 @@ def _maybe_wrap(
     return ret
 
 
-def _select_one(c: Container, key: str) -> Tuple[Optional[Node], Union[str, int]]:
+def _select_one(
+    c: Container, key: str, throw_on_missing: bool
+) -> Tuple[Optional[Node], Union[str, int]]:
     from .dictconfig import DictConfig
     from .listconfig import ListConfig
 
@@ -624,6 +626,11 @@ def _select_one(c: Container, key: str) -> Tuple[Optional[Node], Union[str, int]
         val: Optional[Node]
         if c.get_node_ex(ret_key, validate_access=False) is not None:
             val = c.get_node(ret_key)
+            if val._is_missing():
+                if throw_on_missing:
+                    raise MissingMandatoryValue(c._get_full_key(ret_key))
+                else:
+                    return val, ret_key
         else:
             val = None
     elif isinstance(c, ListConfig):


Test patch:
diff --git a/tests/examples/test_dataclass_example.py b/tests/examples/test_dataclass_example.py
index 900f8b4..14c09c2 100644
--- a/tests/examples/test_dataclass_example.py
+++ b/tests/examples/test_dataclass_example.py
@@ -60,15 +60,18 @@ def test_simple_types_obj() -> None:
 
 
 def test_conversions() -> None:
-    conf = OmegaConf.structured(SimpleTypes)
+    conf: SimpleTypes = OmegaConf.structured(SimpleTypes)
 
     # OmegaConf can convert types at runtime
     conf.num = 20  # ok, type matches
-    conf.num = "20"  # ok, the String "20" is converted to the int 20
-    # mypy does not understand ValueNode type conversion
-    assert conf.num == 20  # type: ignore
+
+    # ok, the String "20" is converted to the int 20
+    conf.num = "20"  # type: ignore
+
+    assert conf.num == 20
     with pytest.raises(ValidationError):
-        conf.num = "one"  # ValidationError: "one" cannot be converted to an integer
+        # ValidationError: "one" cannot be converted to an integer
+        conf.num = "one"  # type: ignore
 
     # booleans can take many forms
     for expected, values in {
@@ -76,7 +79,7 @@ def test_conversions() -> None:
         False: ["off", "no", "false", False, "0"],
     }.items():
         for b in values:
-            conf.is_awesome = b
+            conf.is_awesome = b  # type: ignore
             assert conf.is_awesome == expected
 
     # Enums too
@@ -85,7 +88,7 @@ def test_conversions() -> None:
         Height.TALL: [Height.TALL, "Height.TALL", "TALL", 1],
     }.items():
         for b in values1:
-            conf.height = b
+            conf.height = b  # type: ignore
             assert conf.height == expected1
 
 
diff --git a/tests/test_basic_ops_dict.py b/tests/test_basic_ops_dict.py
index 4275df4..f8c7220 100644
--- a/tests/test_basic_ops_dict.py
+++ b/tests/test_basic_ops_dict.py
@@ -315,13 +315,13 @@ def test_dict_len(d: Any, expected: Any) -> None:
 
 
 def test_dict_assign_illegal_value() -> None:
-    c = OmegaConf.create(dict())
+    c = OmegaConf.create()
     with pytest.raises(UnsupportedValueType, match=re.escape("key: a")):
         c.a = IllegalType()
 
 
 def test_dict_assign_illegal_value_nested() -> None:
-    c = OmegaConf.create(dict(a=dict()))
+    c = OmegaConf.create({"a": {}})
     with pytest.raises(UnsupportedValueType, match=re.escape("key: a.b")):
         c.a.b = IllegalType()
 
diff --git a/tests/test_basic_ops_list.py b/tests/test_basic_ops_list.py
index d615edd..5e4038b 100644
--- a/tests/test_basic_ops_list.py
+++ b/tests/test_basic_ops_list.py
@@ -50,11 +50,32 @@ def test_list_get_with_default() -> None:
     assert c.get(2, "default_value") == "found"
 
 
-def test_iterate_list() -> None:
-    c = OmegaConf.create([1, 2])
-    items = [x for x in c]
-    assert items[0] == 1
-    assert items[1] == 2
+@pytest.mark.parametrize(  # type: ignore
+    "input_, list_key",
+    [
+        ([1, 2], None),
+        (["${1}", 2], None),
+        (
+            {
+                "defaults": [
+                    {"optimizer": "adam"},
+                    {"dataset": "imagenet"},
+                    {"foo": "${defaults.0.optimizer}_${defaults.1.dataset}"},
+                ]
+            },
+            "defaults",
+        ),
+        ([1, 2, "${missing}"], None),
+    ],
+)
+def test_iterate_list(input_: Any, list_key: str) -> None:
+    c = OmegaConf.create(input_)
+    if list_key is not None:
+        lst = c.get(list_key)
+    else:
+        lst = c
+    items = [x for x in lst]
+    assert items == lst
 
 
 def test_iterate_interpolated_list() -> None:
diff --git a/tests/test_create.py b/tests/test_create.py
index 1c412d7..61a4dc9 100644
--- a/tests/test_create.py
+++ b/tests/test_create.py
@@ -38,6 +38,8 @@ from . import IllegalType
             {"a": 2, "b": {"c": {"f": 1}, "d": {}}},
             {"a": 2, "b": {"c": {"f": 1}, "d": {}}},
         ),
+        ({"foo": "${missing}"}, {"foo": "${missing}"}),
+        (OmegaConf.create({"foo": "${missing}"}), {"foo": "${missing}"}),
         (OmegaConf.create(), {}),
         (OmegaConf.create({}), {}),
         (OmegaConf.create([]), []),
@@ -46,7 +48,7 @@ from . import IllegalType
     ],
 )
 def test_create_value(input_: Any, expected: Any) -> None:
-    assert expected == OmegaConf.create(input_)
+    assert OmegaConf.create(input_) == expected
 
 
 def test_create_from_cli() -> None:
@@ -89,14 +91,12 @@ def test_create_list_with_illegal_value_idx1() -> None:
 
 def test_create_dict_with_illegal_value() -> None:
     with pytest.raises(UnsupportedValueType, match=re.escape("key: a")):
-        OmegaConf.create(dict(a=IllegalType()))
+        OmegaConf.create({"a": IllegalType()})
 
 
-# TODO: improve exception message to contain full key a.b
-# https://github.com/omry/omegaconf/issues/14
 def test_create_nested_dict_with_illegal_value() -> None:
-    with pytest.raises(ValueError):
-        OmegaConf.create(dict(a=dict(b=IllegalType())))
+    with pytest.raises(ValueError, match=re.escape("key: a.b")):
+        OmegaConf.create({"a": {"b": IllegalType()}})
 
 
 def test_create_from_oc() -> None:
diff --git a/tests/test_get_full_key.py b/tests/test_get_full_key.py
index 66f6b6e..6c0acf7 100644
--- a/tests/test_get_full_key.py
+++ b/tests/test_get_full_key.py
@@ -1,99 +1,76 @@
-from omegaconf import DictConfig, ListConfig, OmegaConf
-
-
-class TestGetFullKey:
-    # 1
-    def test_dict(self) -> None:
-        c = OmegaConf.create(dict(a=1))
-        assert isinstance(c, DictConfig)
-        assert c._get_full_key("a") == "a"
-
-    def test_dict_empty_key(self) -> None:
-        c = OmegaConf.create(dict(a=1))
-        assert isinstance(c, DictConfig)
-        assert c._get_full_key("") == ""
-
-    def test_list(self) -> None:
-        c = OmegaConf.create([1, 2, 3])
-        assert isinstance(c, ListConfig)
-        assert c._get_full_key("2") == "[2]"
-
-    def test_list_empty_key(self) -> None:
-        c = OmegaConf.create([1, 2, 3])
-        assert isinstance(c, ListConfig)
-        assert c._get_full_key("") == ""
-
-    # 2
-    def test_dd(self) -> None:
-        c = OmegaConf.create(dict(a=1, b=dict(c=1)))
-        assert isinstance(c, DictConfig)
-        assert c.b._get_full_key("c") == "b.c"
-
-    def test_dl(self) -> None:
-        c = OmegaConf.create(dict(a=[1, 2, 3]))
-        assert isinstance(c, DictConfig)
-        assert c.a._get_full_key(1) == "a[1]"
-
-    def test_ll(self) -> None:
-        c = OmegaConf.create([[1, 2, 3]])
-        assert isinstance(c, ListConfig)
-        assert c[0]._get_full_key("2") == "[0][2]"
-
-    def test_ld(self) -> None:
-        c = OmegaConf.create([1, 2, dict(a=1)])
-        assert isinstance(c, ListConfig)
-        assert c[2]._get_full_key("a") == "[2].a"
-
-    def test_dd_empty_key(self) -> None:
-        c = OmegaConf.create(dict(a=1, b=dict(c=1)))
-        assert isinstance(c, DictConfig)
-        assert c.b._get_full_key(key="") == "b"
-
-    # 3
-    def test_ddd(self) -> None:
-        c = OmegaConf.create(dict(a=dict(b=dict(c=1))))
-        assert isinstance(c, DictConfig)
-        assert c.a.b._get_full_key("c") == "a.b.c"
-
-    def test_ddl(self) -> None:
-        c = OmegaConf.create(dict(a=dict(b=[0, 1])))
-        assert c.a.b._get_full_key(0) == "a.b[0]"
-
-    def test_dll(self) -> None:
-        c = OmegaConf.create(dict(a=[1, [2]]))
-        assert c.a[1]._get_full_key(0) == "a[1][0]"
-
-    def test_dld(self) -> None:
-        c = OmegaConf.create(dict(a=[dict(b=2)]))
-        assert c.a[0]._get_full_key("b") == "a[0].b"
-
-    def test_ldd(self) -> None:
-        c = OmegaConf.create([dict(a=dict(b=1))])
-        assert isinstance(c, ListConfig)
-        assert c[0].a._get_full_key("b") == "[0].a.b"
-
-    def test_ldl(self) -> None:
-        c = OmegaConf.create([dict(a=[0])])
-        assert isinstance(c, ListConfig)
-        assert c[0].a._get_full_key(0) == "[0].a[0]"
-
-    def test_lll(self) -> None:
-        c = OmegaConf.create([[[0]]])
-        assert isinstance(c, ListConfig)
-        assert c[0][0]._get_full_key(0) == "[0][0][0]"
-
-    def test_lld(self) -> None:
-        c = OmegaConf.create([[dict(a=1)]])
-        assert isinstance(c, ListConfig)
-        assert c[0][0]._get_full_key("a") == "[0][0].a"
-
-    def test_lldddl(self) -> None:
-        c = OmegaConf.create([[dict(a=dict(a=[0]))]])
-        assert isinstance(c, ListConfig)
-        assert c[0][0].a.a._get_full_key(0) == "[0][0].a.a[0]"
-
-    # special cases
-    def test_parent_with_missing_item(self) -> None:
-        c = OmegaConf.create(dict(x="???", a=1, b=dict(c=1)))
-        assert isinstance(c, DictConfig)
-        assert c.b._get_full_key("c") == "b.c"
+from typing import Any
+
+import pytest
+
+from omegaconf import OmegaConf
+
+
+@pytest.mark.parametrize(
+    "cfg, select, key, expected",
+    [
+        ({}, "", "a", "a"),
+        # 1
+        # dict
+        ({"a": 1}, "", "a", "a"),
+        ({"a": 1}, "", "bad", "bad"),
+        # dict empty key
+        ({"a": 1}, "", "", ""),
+        ({"a": 1}, "", "bad", "bad"),
+        # list
+        ([1, 2, 3], "", "2", "[2]"),
+        ([1, 2, 3], "", "999", "[999]"),
+        # list empty key
+        ([1, 2, 3], "", "", ""),
+        ([1, 2, 3], "", "999", "[999]"),
+        # 2
+        # dd
+        ({"a": 1, "b": {"c": 1}}, "b", "c", "b.c"),
+        ({"a": 1, "b": {"c": 1}}, "b", "bad", "b.bad"),
+        # dl
+        ({"a": [1, 2, 3]}, "a", 1, "a[1]"),
+        ({"a": [1, 2, 3]}, "a", 999, "a[999]"),
+        # ll
+        ([[1, 2, 3]], "0", "2", "[0][2]"),
+        ([[1, 2, 3]], "0", "999", "[0][999]"),
+        # ld
+        ([1, 2, {"a": 1}], "2", "a", "[2].a"),
+        ([1, 2, {"a": 1}], "2", "bad", "[2].bad"),
+        # dd empty key
+        ({"a": 1, "b": {"c": 1}}, "b", "", "b"),
+        ({"a": 1, "b": {"c": 1}}, "b", "bad", "b.bad"),
+        # 3
+        # ddd
+        ({"a": {"b": {"c": 1}}}, "a.b", "c", "a.b.c"),
+        ({"a": {"b": {"c": 1}}}, "a.b", "bad", "a.b.bad"),
+        # ddl
+        ({"a": {"b": [0, 1]}}, "a.b", 0, "a.b[0]"),
+        ({"a": {"b": [0, 1]}}, "a.b", 999, "a.b[999]"),
+        # dll
+        ({"a": [1, [2]]}, "a.1", 0, "a[1][0]"),
+        ({"a": [1, [2]]}, "a.1", 999, "a[1][999]"),
+        # dld
+        ({"a": [{"b": 2}]}, "a.0", "b", "a[0].b"),
+        ({"a": [{"b": 2}]}, "a.0", "bad", "a[0].bad"),
+        # ldd
+        ([{"a": {"b": 1}}], "0.a", "b", "[0].a.b"),
+        ([{"a": {"b": 1}}], "0.a", "bad", "[0].a.bad"),
+        # ldl
+        ([{"a": [0]}], "0.a", 0, "[0].a[0]"),
+        ([{"a": [0]}], "0.a", 999, "[0].a[999]"),
+        # lld
+        ([[{"a": 1}]], "0.0", "a", "[0][0].a"),
+        ([[{"a": 1}]], "0.0", "bad", "[0][0].bad"),
+        # lll
+        ([[[0]]], "0.0", 0, "[0][0][0]"),
+        # lldddl
+        ([[{"a": {"a": [0]}}]], "0.0.a.a", 0, "[0][0].a.a[0]"),
+        # special cases
+        # parent_with_missing_item
+        ({"x": "???", "a": 1, "b": {"c": 1}}, "b", "c", "b.c"),
+    ],
+)
+class TestGetFullKeyMatrix:
+    def test(self, cfg: Any, select: str, key: Any, expected: Any) -> None:
+        c = OmegaConf.create(cfg)
+        node = c.select(select)
+        assert node._get_full_key(key) == expected
diff --git a/tests/test_interpolation.py b/tests/test_interpolation.py
index abda8b8..9ba726f 100644
--- a/tests/test_interpolation.py
+++ b/tests/test_interpolation.py
@@ -325,7 +325,6 @@ def test_incremental_dict_with_interpolation() -> None:
     conf.b = OmegaConf.create()
     assert isinstance(conf.b, DictConfig)
     conf.b.c = "${a}"
-    # mypy does not understand that interpolation can change the type.
     assert conf.b.c == conf.a  # type: ignore
 
 
diff --git a/tests/test_select.py b/tests/test_select.py
index ddff73c..e55a933 100644
--- a/tests/test_select.py
+++ b/tests/test_select.py
@@ -1,9 +1,11 @@
-from typing import Optional
+from typing import Any, Optional
 
 import pytest
 from pytest import raises
 
-from omegaconf import OmegaConf
+from omegaconf import MissingMandatoryValue, OmegaConf
+
+from . import does_not_raise
 
 
 @pytest.mark.parametrize("struct", [True, False, None])  # type: ignore
@@ -13,50 +15,64 @@ def test_select_key_from_empty(struct: Optional[bool]) -> None:
     assert c.select("not_there") is None
 
 
-def test_select_dotkey_from_empty() -> None:
-    c = OmegaConf.create()
-    assert c.select("not.there") is None
-    assert c.select("still.not.there") is None
+@pytest.mark.parametrize(  # type: ignore
+    "cfg, keys, expected, expectation",
+    [
+        ({}, "nope", None, does_not_raise()),
+        ({}, "not.there", None, does_not_raise()),
+        ({}, "still.not.there", None, does_not_raise()),
+        ({"c": 1}, "c", 1, does_not_raise()),
+        ({"a": {"v": 1}}, "a", {"v": 1}, does_not_raise()),
+        ({"a": {"v": 1}}, "a.v", 1, does_not_raise()),
+        ({"missing": "???"}, "missing", None, does_not_raise()),
+        ([], "0", None, does_not_raise()),
+        ([1, "2"], ("0", "1"), (1, "2"), does_not_raise()),
+        (
+            [1, {"a": 10, "c": ["foo", "bar"]}],
+            ("0", "1.a", "1.b", "1.c.0", "1.c.1"),
+            (1, 10, None, "foo", "bar"),
+            does_not_raise(),
+        ),
+        ([1, 2, 3], "a", None, raises(TypeError)),
+        (
+            {"a": {"v": 1}, "b": {"v": 1}},
+            "",
+            {"a": {"v": 1}, "b": {"v": 1}},
+            does_not_raise(),
+        ),
+        (
+            {"dict": {"one": 1}, "foo": "one=${dict.one}"},
+            "foo",
+            "one=1",
+            does_not_raise(),
+        ),
+        (
+            {"dict": {"foo": "one=${one}"}, "one": 1},
+            "dict.foo",
+            "one=1",
+            does_not_raise(),
+        ),
+        ({"dict": {"foo": "one=${foo:1}"}}, "dict.foo", "one=_1_", does_not_raise()),
+    ],
+)
+def test_select(
+    restore_resolvers: Any, cfg: Any, keys: Any, expected: Any, expectation: Any
+) -> None:
+    if not isinstance(keys, (tuple, list)):
+        keys = [keys]
+    if not isinstance(expected, (tuple, list)):
+        expected = [expected]
+    OmegaConf.register_resolver("foo", lambda x: f"_{x}_")
+
+    c = OmegaConf.create(cfg)
+    with expectation:
+        for idx, key in enumerate(keys):
+            assert c.select(key) == expected[idx]
 
 
 def test_select_from_dict() -> None:
-    c = OmegaConf.create({"a": {"v": 1}, "b": {"v": 1}, "c": 1})
-
-    assert c.select("a") == {"v": 1}
-    assert c.select("a.v") == 1
-    assert c.select("b.v") == 1
-    assert c.select("c") == 1
-    assert c.select("nope") is None
-
-
-def test_select_from_empty_list() -> None:
-    c = OmegaConf.create([])
-    assert c.select("0") is None
-
-
-def test_select_from_primitive_list() -> None:
-    c = OmegaConf.create([1, 2, 3, "4"])
-    assert c.select("0") == 1
-    assert c.select("1") == 2
-    assert c.select("2") == 3
-    assert c.select("3") == "4"
-
-
-def test_select_from_dict_in_list() -> None:
-    c = OmegaConf.create([1, dict(a=10, c=["foo", "bar"])])
-    assert c.select("0") == 1
-    assert c.select("1.a") == 10
-    assert c.select("1.b") is None
-    assert c.select("1.c.0") == "foo"
-    assert c.select("1.c.1") == "bar"
-
-
-def test_list_select_non_int_key() -> None:
-    c = OmegaConf.create([1, 2, 3])
-    with raises(TypeError):
-        c.select("a")
-
-
-def test_select_empty_string_returns_root() -> None:
-    c = OmegaConf.create({"a": {"v": 1}, "b": {"v": 1}})
-    assert c.select("") == c
+    c = OmegaConf.create({"missing": "???"})
+    with pytest.raises(MissingMandatoryValue):
+        c.select("missing", throw_on_missing=True)
+    assert c.select("missing", throw_on_missing=False) is None
+    assert c.select("missing") is None