Repository: omry/omegaconf

Issue:
More informative OmegaConf.get_ref_type
try to return Dict[K, V] and List[T] for DictConfig (unstructured) and ListConfig

Discussion:

Patch:
diff --git a/.coveragerc b/.coveragerc
index 4386da5..4c9bab1 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -12,7 +12,6 @@ exclude_lines =
     raise AssertionError
     raise NotImplementedError
     raise TypeError
-    return NotImplemented
     assert False
     @abstractmethod
     \.\.\.
diff --git a/omegaconf/_utils.py b/omegaconf/_utils.py
index 126fca1..e3c0683 100644
--- a/omegaconf/_utils.py
+++ b/omegaconf/_utils.py
@@ -336,7 +336,18 @@ def is_primitive_dict(obj: Any) -> bool:
 
 def is_dict_annotation(type_: Any) -> bool:
     origin = getattr(type_, "__origin__", None)
-    return origin is Dict or origin is dict
+    if sys.version_info < (3, 7, 0):
+        return origin is Dict or type_ is Dict  # pragma: no cover
+    else:
+        return origin is dict  # pragma: no cover
+
+
+def is_list_annotation(type_: Any) -> bool:
+    origin = getattr(type_, "__origin__", None)
+    if sys.version_info < (3, 7, 0):
+        return origin is List or type_ is List  # pragma: no cover
+    else:
+        return origin is list  # pragma: no cover
 
 
 def is_dict_subclass(type_: Any) -> bool:
@@ -435,6 +446,53 @@ def _get_value(value: Any) -> Any:
     return value
 
 
+def get_ref_type(obj: Any, key: Any = None) -> Optional[Type[Any]]:
+    from omegaconf import DictConfig, ListConfig
+    from omegaconf.base import Container, Node
+    from omegaconf.nodes import ValueNode
+
+    def none_as_any(t: Optional[Type[Any]]) -> Union[Type[Any], Any]:
+        if t is None:
+            return Any
+        else:
+            return t
+
+    if isinstance(obj, Container) and key is not None:
+        obj = obj._get_node(key)
+
+    is_optional = True
+    ref_type = None
+    if isinstance(obj, ValueNode):
+        is_optional = obj._is_optional()
+        ref_type = obj._metadata.ref_type
+    elif isinstance(obj, Container):
+        if isinstance(obj, Node):
+            ref_type = obj._metadata.ref_type
+        if ref_type is Any:
+            pass
+        elif not is_structured_config(ref_type):
+            kt = none_as_any(obj._metadata.key_type)
+            vt = none_as_any(obj._metadata.element_type)
+            if isinstance(obj, DictConfig):
+                ref_type = Dict[kt, vt]  # type: ignore
+            elif isinstance(obj, ListConfig):
+                ref_type = List[vt]  # type: ignore
+        is_optional = obj._is_optional()
+    else:
+        if isinstance(obj, dict):
+            ref_type = Dict[Any, Any]
+        elif isinstance(obj, (list, tuple)):
+            ref_type = List[Any]
+        else:
+            ref_type = get_type_of(obj)
+
+    ref_type = none_as_any(ref_type)
+    if is_optional and ref_type is not Any:
+        ref_type = Optional[ref_type]
+
+    return ref_type
+
+
 def format_and_raise(
     node: Any,
     key: Any,
@@ -480,17 +538,11 @@ def format_and_raise(
 
         full_key = node._get_full_key(key=key)
 
-        ref_type = OmegaConf.get_ref_type(node)
         object_type = OmegaConf.get_type(node)
-
         object_type_str = type_str(object_type)
-        if ref_type is None:
-            ref_type_str = "Any"
-        else:
-            ref_type_str = type_str(ref_type)
 
-        if node._metadata.optional and ref_type_str != "Any":
-            ref_type_str = f"Optional[{ref_type_str}]"
+        ref_type = get_ref_type(node)
+        ref_type_str = type_str(ref_type)
 
     msg = string.Template(msg).substitute(
         REF_TYPE=ref_type_str,
@@ -537,6 +589,9 @@ def format_and_raise(
 
 
 def type_str(t: Any) -> str:
+    is_optional, t = _resolve_optional(t)
+    if t is Any:
+        return "Any"
     if sys.version_info < (3, 7, 0):  # pragma: no cover
         # Python 3.6
         if hasattr(t, "__name__"):
@@ -562,6 +617,10 @@ def type_str(t: Any) -> str:
     args = getattr(t, "__args__", None)
     if args is not None:
         args = ", ".join([type_str(t) for t in (list(t.__args__))])
-        return f"{name}[{args}]"
+        ret = f"{name}[{args}]"
     else:
-        return name
+        ret = name
+    if is_optional:
+        return f"Optional[{ret}]"
+    else:
+        return ret
diff --git a/omegaconf/base.py b/omegaconf/base.py
index 2c25eca..e18302c 100644
--- a/omegaconf/base.py
+++ b/omegaconf/base.py
@@ -32,6 +32,10 @@ class ContainerMetadata(Metadata):
     key_type: Any = None
     element_type: Any = None
 
+    def __post_init__(self) -> None:
+        assert self.key_type is None or isinstance(self.key_type, type)
+        assert self.element_type is None or isinstance(self.element_type, type)
+
 
 class Node(ABC):
     _metadata: Metadata
diff --git a/omegaconf/basecontainer.py b/omegaconf/basecontainer.py
index 96e7dbb..6c5b826 100644
--- a/omegaconf/basecontainer.py
+++ b/omegaconf/basecontainer.py
@@ -13,8 +13,6 @@ from ._utils import (
     _is_interpolation,
     get_value_kind,
     get_yaml_loader,
-    is_dict_annotation,
-    is_dict_subclass,
     is_primitive_container,
     is_primitive_dict,
     is_structured_config,
@@ -325,23 +323,23 @@ class BaseContainer(Container, ABC):
             target_node = isinstance(target_node_ref, ValueNode)
 
         def wrap(key: Any, val: Any) -> Node:
-            if is_dict_annotation(self._metadata.ref_type) or (
-                not is_primitive_dict(self._metadata.ref_type)
-                and is_dict_subclass(self._metadata.ref_type)
-            ):
+            is_optional = True
+            if not is_structured_config(val):
                 ref_type = self._metadata.element_type
             else:
                 target = self._get_node(key)
                 if target is None:
                     if is_structured_config(val):
                         ref_type = OmegaConf.get_type(val)
-                    else:
-                        # assignment
-                        ref_type = OmegaConf.get_ref_type(val)
                 else:
-                    ref_type = OmegaConf.get_ref_type(target)
+                    is_optional = target._is_optional()
+                    ref_type = target._metadata.ref_type
             return _maybe_wrap(
-                ref_type=ref_type, key=key, value=val, is_optional=True, parent=self
+                ref_type=ref_type,
+                key=key,
+                value=val,
+                is_optional=is_optional,
+                parent=self,
             )
 
         def assign(value_key: Any, value_to_assign: Any) -> None:
@@ -454,7 +452,7 @@ class BaseContainer(Container, ABC):
         from .listconfig import ListConfig
         from .omegaconf import _select_one
 
-        if not isinstance(key, (int, str, Enum, slice)):
+        if not isinstance(key, (int, str, Enum, slice, type(None))):
             return ""
 
         def _slice_to_str(x: slice) -> str:
diff --git a/omegaconf/dictconfig.py b/omegaconf/dictconfig.py
index fbac511..f90a894 100644
--- a/omegaconf/dictconfig.py
+++ b/omegaconf/dictconfig.py
@@ -17,7 +17,6 @@ from typing import (
 from ._utils import (
     ValueKind,
     _is_interpolation,
-    get_dict_key_value_types,
     get_structured_config_data,
     get_type_of,
     get_value_kind,
@@ -25,6 +24,7 @@ from ._utils import (
     is_primitive_dict,
     is_structured_config,
     is_structured_config_frozen,
+    type_str,
 )
 from .base import Container, ContainerMetadata, Node
 from .basecontainer import DEFAULT_VALUE_MARKER, BaseContainer
@@ -33,6 +33,7 @@ from .errors import (
     ConfigKeyError,
     KeyValidationError,
     MissingMandatoryValue,
+    OmegaConfBaseException,
     ReadonlyConfigError,
     UnsupportedInterpolationType,
     ValidationError,
@@ -49,10 +50,11 @@ class DictConfig(BaseContainer, MutableMapping[str, Any]):
         content: Union[Dict[str, Any], Any],
         key: Any = None,
         parent: Optional[Container] = None,
-        ref_type: Optional[Type[Any]] = None,
+        ref_type: Union[Type[Any], Any] = None,
+        key_type: Optional[Type[Any]] = None,
+        element_type: Optional[Type[Any]] = None,
         is_optional: bool = True,
     ) -> None:
-        key_type, element_type = get_dict_key_value_types(ref_type)
         super().__init__(
             parent=parent,
             metadata=ContainerMetadata(
@@ -139,7 +141,11 @@ class DictConfig(BaseContainer, MutableMapping[str, Any]):
             if key is not None:
                 node = self._get_node(key)
                 if node is not None and not node._is_optional():
-                    raise ValidationError("field '$FULL_KEY' is not Optional")
+                    self._format_and_raise(
+                        key=key,
+                        value=value,
+                        cause=ValidationError("field '$FULL_KEY' is not Optional"),
+                    )
             else:
                 if not self._is_optional():
                     raise ValidationError("field '$FULL_KEY' is not Optional")
@@ -179,33 +185,30 @@ class DictConfig(BaseContainer, MutableMapping[str, Any]):
         if value is None:
             return
 
-        target_type = OmegaConf.get_ref_type(target)
+        target_type = target._metadata.ref_type
         value_type = OmegaConf.get_type(value)
 
         if is_dict(value_type) and is_dict(target_type):
             return
 
-        if is_assign:
-            # assign
-            validation_error = (
-                target_type is not None
-                and value_type is not None
-                and not issubclass(value_type, target_type)
-            )
-        else:
+        # is assignment illegal?
+        validation_error = (
+            target_type is not None
+            and value_type is not None
+            and not issubclass(value_type, target_type)
+        )
+
+        if not is_assign:
             # merge
-            validation_error = not is_dict(value_type) and (
-                target_type is not None
-                and value_type is not None
-                and not issubclass(value_type, target_type)
-            )
+            # Merging of a dictionary is allowed even if assignment is illegal (merge would do deeper checks)
+            validation_error = not is_dict(value_type) and validation_error
 
         if validation_error:
             assert value_type is not None
             assert target_type is not None
             msg = (
-                f"Invalid type assigned : {value_type.__name__} is not a "
-                f"subclass of {target_type.__name__}. value: {value}"
+                f"Invalid type assigned : {type_str(value_type)} is not a "
+                f"subclass of {type_str(target_type)}. value: {value}"
             )
             raise ValidationError(msg)
 
@@ -222,24 +225,23 @@ class DictConfig(BaseContainer, MutableMapping[str, Any]):
                 except KeyValidationError:
                     pass
             raise KeyValidationError("Incompatible key type '$KEY_TYPE'")
+        elif key_type == str:
+            if not isinstance(key, str):
+                raise KeyValidationError(
+                    f"Key $KEY ($KEY_TYPE) is incompatible with ({key_type.__name__})"
+                )
+            return key
+        elif issubclass(key_type, Enum):
+            try:
+                ret = EnumNode.validate_and_convert_to_enum(key_type, key)
+                assert ret is not None
+                return ret
+            except ValidationError as e:
+                raise KeyValidationError(
+                    f"Key '$KEY' is incompatible with ({key_type.__name__}) : {e}"
+                )
         else:
-            if key_type == str:
-                if not isinstance(key, str):
-                    raise KeyValidationError(
-                        f"Key $KEY ($KEY_TYPE) is incompatible with ({key_type.__name__})"
-                    )
-                return key
-            elif issubclass(key_type, Enum):
-                try:
-                    ret = EnumNode.validate_and_convert_to_enum(key_type, key)
-                    assert ret is not None
-                    return ret
-                except ValidationError as e:
-                    raise KeyValidationError(
-                        f"Key '$KEY' is incompatible with ({key_type.__name__}) : {e}"
-                    )
-            else:
-                assert False
+            assert False
 
     def __setitem__(self, key: Union[str, Enum], value: Any) -> None:
 
@@ -272,6 +274,8 @@ class DictConfig(BaseContainer, MutableMapping[str, Any]):
         try:
             self.__set_impl(key, value)
         except Exception as e:
+            if isinstance(e, OmegaConfBaseException) and e._initialized:
+                raise e
             self._format_and_raise(key=key, value=value, cause=e)
             assert False
 
@@ -506,7 +510,6 @@ class DictConfig(BaseContainer, MutableMapping[str, Any]):
                 self.__dict__["_metadata"] = copy.deepcopy(value._metadata)
 
             elif isinstance(value, dict):
-                self._metadata.object_type = self._metadata.ref_type
                 for k, v in value.items():
                     self.__setitem__(k, v)
             else:
diff --git a/omegaconf/listconfig.py b/omegaconf/listconfig.py
index b6794e6..10ad04f 100644
--- a/omegaconf/listconfig.py
+++ b/omegaconf/listconfig.py
@@ -18,7 +18,6 @@ from ._utils import (
     ValueKind,
     _get_value,
     format_and_raise,
-    get_list_element_type,
     get_value_kind,
     is_primitive_list,
     isint,
@@ -45,15 +44,14 @@ class ListConfig(BaseContainer, MutableSequence[Any]):
         content: Union[List[Any], Tuple[Any, ...], str, None],
         key: Any = None,
         parent: Optional[Container] = None,
-        ref_type: Optional[Type[Any]] = None,
+        element_type: Optional[Type[Any]] = None,
         is_optional: bool = True,
     ) -> None:
         try:
-            element_type = get_list_element_type(ref_type)
             super().__init__(
                 parent=parent,
                 metadata=ContainerMetadata(
-                    ref_type=ref_type,
+                    ref_type=None,
                     object_type=list,
                     key=key,
                     optional=is_optional,
@@ -400,9 +398,9 @@ class ListConfig(BaseContainer, MutableSequence[Any]):
     def _iter_ex(self, resolve: bool) -> Iterator[Any]:
         try:
             if self._is_none():
-                raise TypeError("Cannot iterate on ListConfig object representing None")
+                raise TypeError("Cannot iterate a ListConfig object representing None")
             if self._is_missing():
-                raise MissingMandatoryValue("Cannot iterate on a missing ListConfig")
+                raise MissingMandatoryValue("Cannot iterate a missing ListConfig")
 
             class MyItems(Iterator[Any]):
                 def __init__(self, lst: ListConfig) -> None:
diff --git a/omegaconf/omegaconf.py b/omegaconf/omegaconf.py
index c17fc26..89a3f08 100644
--- a/omegaconf/omegaconf.py
+++ b/omegaconf/omegaconf.py
@@ -28,19 +28,20 @@ from typing_extensions import Protocol
 
 from . import DictConfig, ListConfig
 from ._utils import (
-    ValueKind,
     _get_value,
     decode_primitive,
     format_and_raise,
+    get_dict_key_value_types,
+    get_list_element_type,
     get_type_of,
-    get_value_kind,
+    is_dict_annotation,
+    is_list_annotation,
     is_primitive_container,
     is_primitive_dict,
     is_primitive_list,
     is_structured_config,
     isint,
     type_str,
-    valid_value_annotation_type,
 )
 from .base import Container, Node
 from .basecontainer import BaseContainer
@@ -196,13 +197,33 @@ class OmegaConf:
                     or is_structured_config(obj)
                     or obj is None
                 ):
-                    ref_type = OmegaConf.get_ref_type(obj)
+                    ref_type = None
+                    if is_structured_config(obj):
+                        ref_type = get_type_of(obj)
+                    elif OmegaConf.is_dict(obj):
+                        ref_type = obj._metadata.ref_type
+
                     if ref_type is None:
                         ref_type = OmegaConf.get_type(obj)
-                    return DictConfig(content=obj, parent=parent, ref_type=ref_type)
+
+                    if isinstance(obj, DictConfig):
+                        key_type = obj._metadata.key_type
+                        element_type = obj._metadata.element_type
+                    else:
+                        key_type, element_type = get_dict_key_value_types(ref_type)
+                    return DictConfig(
+                        content=obj,
+                        parent=parent,
+                        ref_type=ref_type,
+                        key_type=key_type,
+                        element_type=element_type,
+                    )
                 elif is_primitive_list(obj) or OmegaConf.is_list(obj):
                     ref_type = OmegaConf.get_type(obj)
-                    return ListConfig(ref_type=ref_type, content=obj, parent=parent)
+                    element_type = get_list_element_type(ref_type)
+                    return ListConfig(
+                        element_type=element_type, content=obj, parent=parent
+                    )
                 else:
                     if isinstance(obj, type):
                         raise ValidationError(
@@ -499,18 +520,6 @@ class OmegaConf:
         else:
             return get_type_of(c)
 
-    @staticmethod
-    def get_ref_type(obj: Any, key: Optional[str] = None) -> Optional[Type[Any]]:
-        if key is not None:
-            c = obj._get_node(key)
-        else:
-            c = obj
-
-        if isinstance(c, Node):
-            return c._metadata.ref_type
-        else:
-            return None
-
     @staticmethod
     def select(cfg: Container, key: str, throw_on_missing: bool = False) -> Any:
         try:
@@ -555,7 +564,6 @@ class OmegaConf:
 register_default_resolvers()
 
 
-# noinspection PyProtectedMember
 @contextmanager
 def flag_override(
     config: Node, name: str, value: Optional[bool]
@@ -568,7 +576,6 @@ def flag_override(
         config._set_flag(name, prev_state)
 
 
-# noinspection PyProtectedMember
 @contextmanager
 def read_write(config: Node) -> Generator[Node, None, None]:
     prev_state = config._get_node_flag("readonly")
@@ -579,7 +586,6 @@ def read_write(config: Node) -> Generator[Node, None, None]:
         OmegaConf.set_readonly(config, prev_state)
 
 
-# noinspection PyProtectedMember
 @contextmanager
 def open_dict(config: Container) -> Generator[Container, None, None]:
     prev_state = config._get_node_flag("struct")
@@ -595,14 +601,42 @@ def open_dict(config: Container) -> Generator[Container, None, None]:
 
 def _node_wrap(
     type_: Any, parent: Optional[BaseContainer], is_optional: bool, value: Any, key: Any
-) -> ValueNode:
-    if not valid_value_annotation_type(type_):
-        raise ValidationError(
-            f"Annotated class '{type_str(type_)}' is not a structured config. "
-            "did you forget to decorate it as a dataclass?"
+) -> Node:
+    node: Node
+    is_dict = type(value) is dict or is_dict_annotation(type_)
+    is_list = type(value) in (list, tuple) or is_list_annotation(type_)
+    if is_dict:
+        key_type, element_type = get_dict_key_value_types(type_)
+        node = DictConfig(
+            content=value,
+            key=key,
+            parent=parent,
+            ref_type=type_,
+            is_optional=is_optional,
+            key_type=key_type,
+            element_type=element_type,
+        )
+    elif is_list:
+        element_type = get_list_element_type(type_)
+        node = ListConfig(
+            content=value,
+            key=key,
+            parent=parent,
+            is_optional=is_optional,
+            element_type=element_type,
+        )
+    elif is_structured_config(type_):
+        key_type, element_type = get_dict_key_value_types(type_)
+        node = DictConfig(
+            ref_type=type_,
+            is_optional=is_optional,
+            content=value,
+            key=key,
+            parent=parent,
+            key_type=key_type,
+            element_type=element_type,
         )
-    node: ValueNode
-    if type_ == Any or type_ is None:
+    elif type_ == Any or type_ is None:
         node = AnyNode(value=value, key=key, parent=parent, is_optional=is_optional)
     elif issubclass(type_, Enum):
         node = EnumNode(
@@ -621,9 +655,7 @@ def _node_wrap(
     elif type_ == str:
         node = StringNode(value=value, key=key, parent=parent, is_optional=is_optional)
     else:
-        raise ValidationError(
-            f"Unexpected object type : {type_.__name__}"
-        )  # pragma: no cover
+        raise ValidationError(f"Unexpected object type : {type_str(type_)}")
     return node
 
 
@@ -634,71 +666,20 @@ def _maybe_wrap(
     is_optional: bool,
     parent: Optional[BaseContainer],
 ) -> Node:
-    from . import DictConfig, ListConfig
-
-    if isinstance(value, ValueNode):
+    # if already a node, update key and parent and return as is.
+    # NOTE: that this mutate the input node!
+    if isinstance(value, Node):
         value._set_key(key)
         value._set_parent(parent)
         return value
-    ret: Node
-    origin_ = getattr(ref_type, "__origin__", None)
-    is_dict = (
-        type(value) in (dict, DictConfig)
-        or origin_ in (dict, DictConfig)
-        or origin_ is Dict
-    )
-    is_list = (
-        type(value) in (list, tuple, ListConfig)
-        or origin_ in (list, tuple, ListConfig)
-        or origin_ is List
-    )
-    value_kind = get_value_kind(value)
-
-    if is_dict:
-        ret = DictConfig(
-            content=value,
-            key=key,
-            parent=parent,
-            ref_type=ref_type,
-            is_optional=is_optional,
-        )
-    elif is_list:
-
-        ret = ListConfig(
-            content=value,
-            key=key,
-            parent=parent,
-            is_optional=is_optional,
-            ref_type=ref_type,
-        )
-    elif (
-        is_structured_config(ref_type)
-        and (
-            is_structured_config(value)
-            or value_kind == ValueKind.MANDATORY_MISSING
-            or value_kind == ValueKind.INTERPOLATION
-            or value is None
-        )
-    ) or is_structured_config(ref_type):
-        from . import DictConfig
-
-        ret = DictConfig(
-            ref_type=ref_type,
-            is_optional=is_optional,
-            content=value,
-            key=key,
-            parent=parent,
-        )
     else:
-        ret = _node_wrap(
+        return _node_wrap(
             type_=ref_type,
             parent=parent,
             is_optional=is_optional,
             value=value,
             key=key,
         )
-    assert isinstance(ret, Node)
-    return ret
 
 
 def _select_one(


Test patch:
diff --git a/tests/structured_conf/test_structured_basic.py b/tests/structured_conf/test_structured_basic.py
index e6e59ca..77c5443 100644
--- a/tests/structured_conf/test_structured_basic.py
+++ b/tests/structured_conf/test_structured_basic.py
@@ -1,10 +1,10 @@
 import re
 from importlib import import_module
-from typing import Any
+from typing import Any, Optional
 
 import pytest
 
-from omegaconf import OmegaConf, ValidationError
+from omegaconf import OmegaConf, ValidationError, _utils
 from omegaconf.errors import ConfigKeyError
 
 
@@ -26,7 +26,10 @@ class TestStructured:
             self, class_type: str
         ) -> None:
             module: Any = import_module(class_type)
-            with pytest.raises(ValidationError, match="structured config"):
+            with pytest.raises(
+                ValidationError,
+                match=re.escape("Unexpected object type : NotStructuredConfig"),
+            ):
                 OmegaConf.structured(module.StructuredWithInvalidField)
 
         def test_assignment_of_subclass(self, class_type: str) -> None:
@@ -117,7 +120,7 @@ class TestStructured:
         module: Any = import_module(class_type)
         cfg1 = OmegaConf.create(module.LinkedList)
         assert OmegaConf.get_type(cfg1) == module.LinkedList
-        assert OmegaConf.get_ref_type(cfg1, "next") == module.LinkedList
+        assert _utils.get_ref_type(cfg1, "next") == Optional[module.LinkedList]
         assert OmegaConf.get_type(cfg1, "next") is None
 
         assert cfg1.next is None
@@ -125,7 +128,7 @@ class TestStructured:
 
         cfg2 = OmegaConf.create(module.MissingTest.Missing1)
         assert OmegaConf.is_missing(cfg2, "head")
-        assert OmegaConf.get_ref_type(cfg2, "head") == module.LinkedList
+        assert _utils.get_ref_type(cfg2, "head") == module.LinkedList
         assert OmegaConf.get_type(cfg2, "head") is None
 
     class TestMissing:
@@ -151,19 +154,26 @@ class TestStructured:
         def test_plugin_holder(self, class_type: str) -> None:
             module: Any = import_module(class_type)
             cfg = OmegaConf.create(module.PluginHolder)
-            assert OmegaConf.get_ref_type(cfg, "none") == module.Plugin
+
+            assert OmegaConf.is_optional(cfg, "none")
+            assert _utils.get_ref_type(cfg, "none") == Optional[module.Plugin]
             assert OmegaConf.get_type(cfg, "none") is None
 
-            assert OmegaConf.get_ref_type(cfg, "missing") == module.Plugin
+            assert not OmegaConf.is_optional(cfg, "missing")
+            assert _utils.get_ref_type(cfg, "missing") == module.Plugin
             assert OmegaConf.get_type(cfg, "missing") is None
 
-            assert OmegaConf.get_ref_type(cfg, "plugin") == module.Plugin
+            assert not OmegaConf.is_optional(cfg, "plugin")
+            assert _utils.get_ref_type(cfg, "plugin") == module.Plugin
             assert OmegaConf.get_type(cfg, "plugin") == module.Plugin
+
             cfg.plugin = module.ConcretePlugin()
-            assert OmegaConf.get_ref_type(cfg, "plugin") == module.Plugin
+            assert not OmegaConf.is_optional(cfg, "plugin")
+            assert _utils.get_ref_type(cfg, "plugin") == module.Plugin
             assert OmegaConf.get_type(cfg, "plugin") == module.ConcretePlugin
 
-            assert OmegaConf.get_ref_type(cfg, "plugin2") == module.Plugin
+            assert not OmegaConf.is_optional(cfg, "plugin2")
+            assert _utils.get_ref_type(cfg, "plugin2") == module.Plugin
             assert OmegaConf.get_type(cfg, "plugin2") == module.ConcretePlugin
 
         def test_native_missing(self, class_type: str) -> None:
diff --git a/tests/structured_conf/test_structured_config.py b/tests/structured_conf/test_structured_config.py
index bdfb843..2e544de 100644
--- a/tests/structured_conf/test_structured_config.py
+++ b/tests/structured_conf/test_structured_config.py
@@ -1,6 +1,6 @@
 import re
 from importlib import import_module
-from typing import Any, Dict
+from typing import Any, Dict, Optional
 
 import pytest
 
@@ -13,6 +13,7 @@ from omegaconf import (
     OmegaConf,
     ReadonlyConfigError,
     ValidationError,
+    _utils,
 )
 from tests import Color
 
@@ -96,7 +97,7 @@ class TestConfigs:
         cfg = OmegaConf.structured(module.NestedWithNone)
         assert cfg == {"plugin": None}
         assert OmegaConf.get_type(cfg, "plugin") is None
-        assert OmegaConf.get_ref_type(cfg, "plugin") == module.Plugin
+        assert _utils.get_ref_type(cfg, "plugin") == Optional[module.Plugin]
 
     def test_nested_config(self, class_type: str) -> None:
         module: Any = import_module(class_type)
@@ -450,11 +451,11 @@ class TestConfigs:
     def test_merge_with_subclass_into_missing(self, class_type: str) -> None:
         module: Any = import_module(class_type)
         base = OmegaConf.structured(module.PluginHolder)
-        assert OmegaConf.get_ref_type(base, "missing") == module.Plugin
+        assert _utils.get_ref_type(base, "missing") == module.Plugin
         assert OmegaConf.get_type(base, "missing") is None
         res = OmegaConf.merge(base, {"missing": module.Plugin})
         assert OmegaConf.get_type(res) == module.PluginHolder
-        assert OmegaConf.get_ref_type(base, "missing") == module.Plugin
+        assert _utils.get_ref_type(base, "missing") == module.Plugin
         assert OmegaConf.get_type(res, "missing") == module.Plugin
 
     def test_merged_with_nons_subclass(self, class_type: str) -> None:
@@ -709,6 +710,18 @@ class TestDictSubclass:
         with pytest.raises(KeyValidationError):
             cfg[Color.RED] = "fail"
 
+    def test_str2str_as_sub_node(self, class_type: str) -> None:
+        module: Any = import_module(class_type)
+        cfg = OmegaConf.create({"foo": module.DictSubclass.Str2Str})
+        assert OmegaConf.get_type(cfg.foo) == module.DictSubclass.Str2Str
+        assert _utils.get_ref_type(cfg.foo) == Optional[module.DictSubclass.Str2Str]
+
+        cfg.foo.hello = "world"
+        assert cfg.foo.hello == "world"
+
+        with pytest.raises(KeyValidationError):
+            cfg.foo[Color.RED] = "fail"
+
     def test_color2str(self, class_type: str) -> None:
         module: Any = import_module(class_type)
         cfg = OmegaConf.structured(module.DictSubclass.Color2Str())
diff --git a/tests/test_base_config.py b/tests/test_base_config.py
index a0c6a0d..d049f67 100644
--- a/tests/test_base_config.py
+++ b/tests/test_base_config.py
@@ -85,10 +85,10 @@ def test_replace_value_node_type_with_another(
     "input_",
     [
         [1, 2, 3],
-        [1, 2, dict(a=3)],
+        [1, 2, {"a": 3}],
         [1, 2, [10, 20]],
-        dict(b=dict(b=10)),
-        dict(b=[1, 2, 3]),
+        {"b": {"b": 10}},
+        {"b": [1, 2, 3]},
     ],
 )
 def test_to_container_returns_primitives(input_: Any) -> None:
@@ -258,7 +258,7 @@ def test_deepcopy_and_merge_and_flags() -> None:
 
 
 @pytest.mark.parametrize(  # type: ignore
-    "cfg", [ListConfig(ref_type=List[int], content=[]), DictConfig(content={})],
+    "cfg", [ListConfig(element_type=int, content=[]), DictConfig(content={})],
 )
 def test_deepcopy_preserves_container_type(cfg: Container) -> None:
     cp: Container = copy.deepcopy(cfg)
diff --git a/tests/test_basic_ops_dict.py b/tests/test_basic_ops_dict.py
index 849d578..6efd0b1 100644
--- a/tests/test_basic_ops_dict.py
+++ b/tests/test_basic_ops_dict.py
@@ -12,6 +12,7 @@ from omegaconf import (
     OmegaConf,
     UnsupportedValueType,
     ValidationError,
+    _utils,
 )
 from omegaconf.basecontainer import BaseContainer
 from omegaconf.errors import KeyValidationError
@@ -569,13 +570,29 @@ def test_get_type() -> None:
     assert OmegaConf.get_type(cfg.inter) == User
 
 
-def test_get_ref_type() -> None:
-    cfg = OmegaConf.create(
-        {"plugin": DictConfig(ref_type=Plugin, content=ConcretePlugin)}
-    )
-
-    assert OmegaConf.get_type(cfg.plugin) == ConcretePlugin
-    assert OmegaConf.get_ref_type(cfg.plugin) == Plugin
+@pytest.mark.parametrize(  # type: ignore
+    "cfg, expected_ref_type",
+    [
+        (
+            OmegaConf.create(
+                {"plugin": DictConfig(ref_type=Plugin, content=ConcretePlugin)}
+            ),
+            Optional[Plugin],
+        ),
+        (
+            OmegaConf.create(
+                {
+                    "plugin": DictConfig(
+                        ref_type=Plugin, content=ConcretePlugin, is_optional=False
+                    )
+                }
+            ),
+            Plugin,
+        ),
+    ],
+)
+def test_get_ref_type(cfg: Any, expected_ref_type: Any) -> None:
+    assert _utils.get_ref_type(cfg.plugin) == expected_ref_type
 
 
 def test_get_ref_type_with_conflict() -> None:
@@ -584,11 +601,11 @@ def test_get_ref_type_with_conflict() -> None:
     )
 
     assert OmegaConf.get_type(cfg.user) == User
-    assert OmegaConf.get_ref_type(cfg.user) == User
+    assert _utils.get_ref_type(cfg.user) == Optional[User]
 
     # Interpolation inherits both type and ref type from the target
     assert OmegaConf.get_type(cfg.inter) == User
-    assert OmegaConf.get_ref_type(cfg.inter) == User
+    assert _utils.get_ref_type(cfg.inter) == Optional[User]
 
 
 def test_is_missing() -> None:
@@ -658,11 +675,11 @@ def test_assign_to_reftype_plugin(
     for value in values:
         cfg = OmegaConf.create({"foo": DictConfig(ref_type=ref_type, content=value)})
         with expectation():
-            assert OmegaConf.get_ref_type(cfg, "foo") == ref_type
+            assert _utils.get_ref_type(cfg, "foo") == Optional[ref_type]
             cfg.foo = assign
             assert cfg.foo == assign
             # validate assignment does not change ref type.
-            assert OmegaConf.get_ref_type(cfg, "foo") == ref_type
+            assert _utils.get_ref_type(cfg, "foo") == Optional[ref_type]
 
         if value is not None:
             cfg = OmegaConf.create(
diff --git a/tests/test_basic_ops_list.py b/tests/test_basic_ops_list.py
index b6ef7e2..b341290 100644
--- a/tests/test_basic_ops_list.py
+++ b/tests/test_basic_ops_list.py
@@ -274,7 +274,7 @@ def validate_list_keys(c: Any) -> None:
             None,
         ),
         (
-            ListConfig(ref_type=List[int], content=[]),
+            ListConfig(element_type=int, content=[]),
             0,
             "foo",
             None,
diff --git a/tests/test_create.py b/tests/test_create.py
index 16718ac..8e06e4e 100644
--- a/tests/test_create.py
+++ b/tests/test_create.py
@@ -126,7 +126,7 @@ def test_create_from_dictconfig_preserves_metadata() -> None:
 
 
 def test_create_from_listconfig_preserves_metadata() -> None:
-    cfg1 = ListConfig(ref_type=List[int], is_optional=False, content=[1, 2, 3])
+    cfg1 = ListConfig(element_type=int, is_optional=False, content=[1, 2, 3])
     OmegaConf.set_struct(cfg1, True)
     OmegaConf.set_readonly(cfg1, True)
     cfg2 = OmegaConf.create(cfg1)
diff --git a/tests/test_errors.py b/tests/test_errors.py
index 2649724..2ada2b0 100644
--- a/tests/test_errors.py
+++ b/tests/test_errors.py
@@ -14,7 +14,6 @@ from omegaconf import (
     UnsupportedValueType,
     ValidationError,
 )
-from omegaconf._utils import type_str
 from omegaconf.errors import (
     ConfigAttributeError,
     ConfigKeyError,
@@ -45,12 +44,15 @@ class Expected:
     key: Any = None
     # "AUTO" : determine automatically based on OmegaConf.get_type(cfg)
     object_type: Any = "AUTO"
+    ref_type: Any = None
+
     # "AUTO: full_key is key
     full_key: Any = "AUTO"
     create: Any = lambda: None
     op: Any = lambda _cfg: None
     child_node: Any = lambda cfg: None
     parent_node: Any = lambda cfg: cfg
+
     object_type_str: Optional[str] = "AUTO"
     ref_type_str: Optional[str] = "AUTO"
     num_lines: int = 4
@@ -68,24 +70,6 @@ class Expected:
                 else:
                     self.full_key = ""
 
-        parent = self.parent_node(cfg)
-        if self.object_type_str == "AUTO":
-            parent_type = OmegaConf.get_type(parent)
-            if parent_type is not None:
-                self.object_type_str = type_str(parent_type)
-            else:
-                self.object_type_str = "Any"
-
-        if self.ref_type_str == "AUTO":
-            parent_type = OmegaConf.get_ref_type(parent)
-            if parent_type is not None:
-                self.ref_type_str = type_str(parent_type)
-                if parent._is_optional():
-                    self.ref_type_str = f"Optional[{self.ref_type_str}]"
-
-            else:
-                self.ref_type_str = "Any"
-
 
 params = [
     ##############
@@ -101,6 +85,7 @@ params = [
             parent_node=lambda cfg: cfg,
             child_node=lambda cfg: cfg._get_node("num"),
             object_type=StructuredWithMissing,
+            ref_type=Optional[StructuredWithMissing],
             key="num",
         ),
         id="structured:update_with_invalid_value",
@@ -114,6 +99,7 @@ params = [
             parent_node=lambda cfg: cfg,
             child_node=lambda cfg: cfg._get_node("num"),
             object_type=StructuredWithMissing,
+            ref_type=Optional[StructuredWithMissing],
             key="num",
         ),
         id="structured:update:none_to_non_optional",
@@ -124,6 +110,7 @@ params = [
             op=lambda cfg: OmegaConf.update(cfg, "a", IllegalType()),
             key="a",
             exception_type=UnsupportedValueType,
+            ref_type=Optional[Dict[Any, Any]],
             msg="Value 'IllegalType' is not a supported primitive type",
         ),
         id="dict:update:object_of_illegal_type",
@@ -136,6 +123,7 @@ params = [
             key="foo",
             child_node=lambda cfg: cfg._get_node("foo"),
             exception_type=ReadonlyConfigError,
+            ref_type=Optional[Dict[Any, Any]],
             msg="Cannot pop from read-only node",
         ),
         id="dict,readonly:pop",
@@ -170,6 +158,7 @@ params = [
             msg="Key 'fail' not in 'ConcretePlugin'",
             key="fail",
             object_type=ConcretePlugin,
+            ref_type=Optional[ConcretePlugin],
         ),
         id="structured:access_invalid_attribute",
     ),
@@ -225,6 +214,7 @@ params = [
             msg="Invalid type assigned : int is not a subclass of FoobarParams. value: 20",
             key="params",
             object_type=ConcretePlugin,
+            ref_type=Optional[ConcretePlugin],
             child_node=lambda cfg: cfg.params,
         ),
         id="structured:setattr,invalid_type_assigned_to_structured",
@@ -249,9 +239,10 @@ params = [
             exception_type=ValidationError,
             msg="field 'foo' is not Optional",
             key="foo",
+            full_key="foo",
             child_node=lambda cfg: cfg.foo,
         ),
-        id="dict,none_optional:set_none",
+        id="dict,not_optional:set_none",
     ),
     pytest.param(
         Expected(
@@ -291,25 +282,25 @@ params = [
     ),
     pytest.param(
         Expected(
-            create=lambda: DictConfig(ref_type=Dict[Color, str], content={}),
+            create=lambda: DictConfig(key_type=Color, element_type=str, content={}),
             op=lambda cfg: cfg.__getitem__("foo"),
             exception_type=KeyValidationError,
             msg="Key 'foo' is incompatible with (Color)",
             key="foo",
             num_lines=7,
         ),
-        id="dict,reftype=Dict[Color,str]:,getitem_str_key",
+        id="DictConfig[Color,str]:getitem_str_key",
     ),
     pytest.param(
         Expected(
-            create=lambda: DictConfig(ref_type=Dict[str, str], content={}),
+            create=lambda: DictConfig(key_type=str, element_type=str, content={}),
             op=lambda cfg: cfg.__getitem__(Color.RED),
             exception_type=KeyValidationError,
             msg="Key Color.RED (Color) is incompatible with (str)",
             full_key="RED",
             key=Color.RED,
         ),
-        id="dict,reftype=Dict[str,str]:,getitem_color_key",
+        id="DictConfig[str,str]:getitem_color_key",
     ),
     # merge
     pytest.param(
@@ -435,20 +426,22 @@ params = [
             exception_type=ValidationError,
             msg="Invalid type assigned : int is not a subclass of ConcretePlugin. value: 1",
             low_level=True,
+            ref_type=Optional[ConcretePlugin],
         ),
         id="dict:set_value:reftype_mismatch",
     ),
     pytest.param(
         Expected(
             create=lambda: DictConfig(
-                ref_type=Dict[str, int], content={"foo": 10, "bar": 20}
+                key_type=str, element_type=int, content={"foo": 10, "bar": 20}
             ),
             op=lambda cfg: cfg.__setitem__("baz", "fail"),
             exception_type=ValidationError,
             msg="Value 'fail' could not be converted to Integer",
             key="baz",
+            ref_type=Optional[Dict[str, int]],
         ),
-        id="dict,int_element_type:assigned_str_value",
+        id="DictConfig[str,int]:assigned_str_value",
     ),
     # delete
     pytest.param(
@@ -477,7 +470,7 @@ params = [
         ),
         id="list:setattr",
     ),
-    # # setattr
+    # setattr
     pytest.param(
         Expected(
             create=lambda: OmegaConf.create([1, 2, 3]),
@@ -498,6 +491,7 @@ params = [
             key="foo",
             full_key="[foo]",
             msg="ListConfig indices must be integers or slices, not str",
+            ref_type=Optional[List[Any]],
         ),
         id="list:get_nox_ex:invalid_index_type",
     ),
@@ -520,6 +514,7 @@ params = [
             msg="Cannot get_node from a ListConfig object representing None",
             key=20,
             full_key="[20]",
+            ref_type=Optional[List[Any]],
         ),
         id="list:get_node_none",
     ),
@@ -698,16 +693,30 @@ params = [
         ),
         id="list,readonly:setitem",
     ),
+    # _set_value
+    pytest.param(
+        Expected(
+            create=lambda: ListConfig(is_optional=False, element_type=int, content=[]),
+            op=lambda cfg: cfg._set_value(None),
+            exception_type=ValidationError,
+            object_type=None,
+            msg="Non optional ListConfig cannot be constructed from None",
+            ref_type=List[int],
+            low_level=True,
+        ),
+        id="list:create_not_optional:_set_value(None)",
+    ),
     # assign
     pytest.param(
         Expected(
-            create=lambda: ListConfig(ref_type=List[int], content=[1, 2, 3]),
+            create=lambda: ListConfig(element_type=int, content=[1, 2, 3]),
             op=lambda cfg: cfg.__setitem__(0, "foo"),
             exception_type=ValidationError,
             msg="Value 'foo' could not be converted to Integer",
             key=0,
             full_key="[0]",
             child_node=lambda cfg: cfg[0],
+            ref_type=Optional[List[int]],
         ),
         id="list,int_elements:assigned_str_element",
     ),
@@ -715,7 +724,7 @@ params = [
         Expected(
             # make sure OmegaConf.create is not losing critical metadata.
             create=lambda: OmegaConf.create(
-                ListConfig(ref_type=List[int], content=[1, 2, 3])
+                ListConfig(element_type=int, content=[1, 2, 3])
             ),
             op=lambda cfg: cfg.__setitem__(0, "foo"),
             exception_type=ValidationError,
@@ -723,6 +732,7 @@ params = [
             key=0,
             full_key="[0]",
             child_node=lambda cfg: cfg[0],
+            ref_type=Optional[List[int]],
         ),
         id="list,int_elements:assigned_str_element",
     ),
@@ -737,6 +747,7 @@ params = [
             key=0,
             full_key="[0]",
             child_node=lambda cfg: cfg[0],
+            ref_type=Optional[List[Any]],
         ),
         id="list,not_optional:null_assignment",
     ),
@@ -783,6 +794,7 @@ params = [
             key=1,
             full_key="[1]",
             child_node=lambda _cfg: None,
+            ref_type=Optional[List[Any]],
         ),
         id="list:insert_into_missing",
     ),
@@ -795,6 +807,7 @@ params = [
             msg="Cannot get from a ListConfig object representing None",
             key=0,
             full_key="[0]",
+            ref_type=Optional[List[Any]],
         ),
         id="list:get_from_none",
     ),
@@ -852,7 +865,7 @@ params = [
             create=lambda: ListConfig(content=None),
             op=lambda cfg: iter(cfg),
             exception_type=TypeError,
-            msg="Cannot iterate on ListConfig object representing None",
+            msg="Cannot iterate a ListConfig object representing None",
         ),
         id="list:iter_none",
     ),
@@ -861,7 +874,7 @@ params = [
             create=lambda: ListConfig(content="???"),
             op=lambda cfg: iter(cfg),
             exception_type=MissingMandatoryValue,
-            msg="Cannot iterate on a missing ListConfig",
+            msg="Cannot iterate a missing ListConfig",
         ),
         id="list:iter_missing",
     ),
@@ -908,9 +921,9 @@ def test_errors(expected: Expected, monkeypatch: Any) -> None:
     with pytest.raises(expected.exception_type, match=re.escape(msg)) as einfo:
         try:
             expected.op(cfg)
-        except Exception:
+        except Exception as e:
             # helps in debugging
-            raise
+            raise e
     ex = einfo.value
 
     assert ex.object_type == expected.object_type
@@ -922,8 +935,12 @@ def test_errors(expected: Expected, monkeypatch: Any) -> None:
         assert ex.child_node == expected.child_node(cfg)
         assert ex.full_key == expected.full_key
         assert isinstance(expected.full_key, str)
-        assert ex.object_type_str == expected.object_type_str
-        assert ex.ref_type_str == expected.ref_type_str
+
+        if expected.ref_type is not None:
+            assert ex.ref_type == expected.ref_type
+
+        if expected.object_type is not None:
+            assert ex.object_type == expected.object_type
 
         with monkeypatch.context() as m:
             m.setenv("OC_CAUSE", "1")
diff --git a/tests/test_get_full_key.py b/tests/test_get_full_key.py
index e7058e0..1aef2a1 100644
--- a/tests/test_get_full_key.py
+++ b/tests/test_get_full_key.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict
+from typing import Any
 
 import pytest
 
@@ -73,14 +73,18 @@ from . import Color
         ({"foo": {"bar": IntegerNode(value=10)}}, "foo", "bar", "foo.bar"),
         # enum
         pytest.param(
-            DictConfig(ref_type=Dict[Color, str], content={Color.RED: "red"}),
+            DictConfig(key_type=Color, element_type=str, content={Color.RED: "red"}),
             "RED",
             "",
             "RED",
             id="get_full_key_with_enum_key",
         ),
         pytest.param(
-            {"foo": DictConfig(ref_type=Dict[Color, str], content={Color.RED: "red"})},
+            {
+                "foo": DictConfig(
+                    key_type=Color, element_type=str, content={Color.RED: "red"}
+                )
+            },
             "foo",
             "RED",
             "foo.RED",
diff --git a/tests/test_omegaconf.py b/tests/test_omegaconf.py
index e37e0a9..eb39622 100644
--- a/tests/test_omegaconf.py
+++ b/tests/test_omegaconf.py
@@ -17,14 +17,7 @@ from omegaconf import (
 )
 from omegaconf.errors import UnsupportedInterpolationType
 
-from . import (
-    Color,
-    ConcretePlugin,
-    IllegalType,
-    Plugin,
-    StructuredWithMissing,
-    does_not_raise,
-)
+from . import Color, ConcretePlugin, IllegalType, StructuredWithMissing, does_not_raise
 
 
 @pytest.mark.parametrize(  # type: ignore
@@ -384,36 +377,6 @@ def test_get_type_on_raw(obj: Any, type_: Any) -> None:
     assert OmegaConf.get_type(obj) == type_
 
 
-@pytest.mark.parametrize(  # type: ignore
-    "cfg, type_",
-    [
-        ({}, None),  # missing foo key
-        ({"foo": 10}, None),
-        ({"foo": 10.0}, None),
-        ({"foo": True}, None),
-        ({"foo": "bar"}, None),
-        ({"foo": None}, None),
-        ({"foo": IntegerNode(10)}, int),
-        ({"foo": FloatNode(10.0)}, float),
-        ({"foo": BooleanNode(True)}, bool),
-        ({"foo": StringNode("bar")}, str),
-        ({"foo": EnumNode(enum_type=Color, value=Color.RED)}, Color),
-        ({"foo": ConcretePlugin()}, ConcretePlugin),
-        ({"foo": ConcretePlugin}, ConcretePlugin),
-        ({"foo": DictConfig(ref_type=Plugin, content=ConcretePlugin)}, Plugin),
-        ({"foo": {}}, None),
-        pytest.param(
-            {"foo": OmegaConf.create()}, dict, id="create_with_nested_dictconfig"
-        ),
-        ({"foo": []}, None),
-        ({"foo": OmegaConf.create([])}, list),
-    ],
-)
-def test_get_ref_type(cfg: Any, type_: Any) -> None:
-    cfg = OmegaConf.create(cfg)
-    assert OmegaConf.get_ref_type(cfg, "foo") == type_
-
-
 def test_is_issubclass() -> None:
     cfg = OmegaConf.structured(ConcretePlugin)
     t = OmegaConf.get_type(cfg)
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 06ede88..275f21b 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,76 +1,105 @@
 from dataclasses import dataclass, field
 from enum import Enum
-from typing import Any, Dict, List, Union
+from typing import Any, Dict, List, Optional, Union
 
 import attr
 import pytest
 
-from omegaconf import DictConfig, ListConfig, OmegaConf, _utils
+from omegaconf import DictConfig, ListConfig, Node, OmegaConf, _utils
+from omegaconf._utils import is_dict_annotation, is_list_annotation
 from omegaconf.errors import KeyValidationError, ValidationError
-from omegaconf.nodes import StringNode
+from omegaconf.nodes import (
+    AnyNode,
+    BooleanNode,
+    EnumNode,
+    FloatNode,
+    IntegerNode,
+    StringNode,
+)
+from omegaconf.omegaconf import _node_wrap
 
-from . import Color, IllegalType, Plugin, does_not_raise
+from . import Color, ConcretePlugin, IllegalType, Plugin, does_not_raise
 
 
 @pytest.mark.parametrize(  # type: ignore
-    "target_type, value, expectation",
+    "target_type, value, expected",
     [
         # Any
-        (Any, "foo", None),
-        (Any, True, None),
-        (Any, 1, None),
-        (Any, 1.0, None),
-        (Any, Color.RED, None),
+        pytest.param(Any, "foo", AnyNode("foo"), id="any"),
+        pytest.param(Any, True, AnyNode(True), id="any"),
+        pytest.param(Any, 1, AnyNode(1), id="any"),
+        pytest.param(Any, 1.0, AnyNode(1.0), id="any"),
+        pytest.param(Any, Color.RED, AnyNode(Color.RED), id="any"),
+        pytest.param(Any, {}, DictConfig(content={}), id="any_as_dict"),
+        pytest.param(Any, [], ListConfig(content=[]), id="any_as_list"),
         # int
-        (int, "foo", ValidationError),
-        (int, True, ValidationError),
-        (int, 1, None),
-        (int, 1.0, ValidationError),
-        (int, Color.RED, ValidationError),
+        pytest.param(int, "foo", ValidationError, id="int"),
+        pytest.param(int, True, ValidationError, id="int"),
+        pytest.param(int, 1, IntegerNode(1), id="int"),
+        pytest.param(int, 1.0, ValidationError, id="int"),
+        pytest.param(int, Color.RED, ValidationError, id="int"),
         # float
-        (float, "foo", ValidationError),
-        (float, True, ValidationError),
-        (float, 1, None),
-        (float, 1.0, None),
-        (float, Color.RED, ValidationError),
-        # bool
-        (bool, "foo", ValidationError),
-        (bool, True, None),
-        (bool, 1, None),
-        (bool, 0, None),
-        (bool, 1.0, ValidationError),
-        (bool, Color.RED, ValidationError),
-        (bool, "true", None),
-        (bool, "false", None),
-        (bool, "on", None),
-        (bool, "off", None),
+        pytest.param(float, "foo", ValidationError, id="float"),
+        pytest.param(float, True, ValidationError, id="float"),
+        pytest.param(float, 1, FloatNode(1), id="float"),
+        pytest.param(float, 1.0, FloatNode(1.0), id="float"),
+        pytest.param(float, Color.RED, ValidationError, id="float"),
+        # # bool
+        pytest.param(bool, "foo", ValidationError, id="bool"),
+        pytest.param(bool, True, BooleanNode(True), id="bool"),
+        pytest.param(bool, 1, BooleanNode(True), id="bool"),
+        pytest.param(bool, 0, BooleanNode(False), id="bool"),
+        pytest.param(bool, 1.0, ValidationError, id="bool"),
+        pytest.param(bool, Color.RED, ValidationError, id="bool"),
+        pytest.param(bool, "true", BooleanNode(True), id="bool"),
+        pytest.param(bool, "false", BooleanNode(False), id="bool"),
+        pytest.param(bool, "on", BooleanNode(True), id="bool"),
+        pytest.param(bool, "off", BooleanNode(False), id="bool"),
         # str
-        (str, "foo", None),
-        (str, True, None),
-        (str, 1, None),
-        (str, 1.0, None),
-        (str, Color.RED, None),
+        pytest.param(str, "foo", StringNode("foo"), id="str"),
+        pytest.param(str, True, StringNode("True"), id="str"),
+        pytest.param(str, 1, StringNode("1"), id="str"),
+        pytest.param(str, 1.0, StringNode("1.0"), id="str"),
+        pytest.param(str, Color.RED, StringNode("Color.RED"), id="str"),
         # Color
-        (Color, "foo", ValidationError),
-        (Color, True, ValidationError),
-        (Color, 1, None),
-        (Color, 1.0, ValidationError),
-        (Color, Color.RED, None),
-        (Color, "RED", None),
-        (Color, "Color.RED", None),
+        pytest.param(Color, "foo", ValidationError, id="Color"),
+        pytest.param(Color, True, ValidationError, id="Color"),
+        pytest.param(Color, 1, EnumNode(enum_type=Color, value=Color.RED), id="Color"),
+        pytest.param(Color, 1.0, ValidationError, id="Color"),
+        pytest.param(
+            Color, Color.RED, EnumNode(enum_type=Color, value=Color.RED), id="Color"
+        ),
+        pytest.param(
+            Color, "RED", EnumNode(enum_type=Color, value=Color.RED), id="Color"
+        ),
+        pytest.param(
+            Color, "Color.RED", EnumNode(enum_type=Color, value=Color.RED), id="Color"
+        ),
         # bad type
-        (IllegalType, "nope", ValidationError),
+        pytest.param(IllegalType, "nope", ValidationError, id="bad_type"),
+        # DictConfig
+        pytest.param(
+            dict, {"foo": "bar"}, DictConfig(content={"foo": "bar"}), id="DictConfig"
+        ),
+        pytest.param(
+            Plugin, Plugin(), DictConfig(content=Plugin()), id="DictConfig[Plugin]"
+        ),
+        # ListConfig
+        pytest.param(list, [1, 2, 3], ListConfig(content=[1, 2, 3]), id="ListConfig"),
     ],
 )
-def test_maybe_wrap(target_type: type, value: Any, expectation: Any) -> None:
+def test_node_wrap(target_type: Any, value: Any, expected: Any) -> None:
     from omegaconf.omegaconf import _maybe_wrap
 
-    if expectation is None:
-        _maybe_wrap(
-            ref_type=target_type, key=None, value=value, is_optional=False, parent=None,
+    if isinstance(expected, Node):
+        res = _node_wrap(
+            type_=target_type, key="foo", value=value, is_optional=False, parent=None,
         )
+        assert type(res) == type(expected)
+        assert res == expected
+        assert res._key() == "foo"
     else:
-        with pytest.raises(expectation):
+        with pytest.raises(expected):
             _maybe_wrap(
                 ref_type=target_type,
                 key=None,
@@ -295,6 +324,7 @@ def test_is_primitive_type(type_: Any, is_primitive: bool) -> None:
     assert _utils.is_primitive_type(type_) == is_primitive
 
 
+@pytest.mark.parametrize("optional", [False, True])  # type: ignore
 @pytest.mark.parametrize(  # type: ignore
     "type_, expected",
     [
@@ -312,8 +342,142 @@ def test_is_primitive_type(type_: Any, is_primitive: bool) -> None:
         (List[str], "List[str]"),
         (List[Color], "List[Color]"),
         (List[Dict[str, Color]], "List[Dict[str, Color]]"),
+    ],
+)
+def test_type_str(type_: Any, expected: str, optional: bool) -> None:
+    if optional:
+        assert _utils.type_str(Optional[type_]) == f"Optional[{expected}]"
+    else:
+        assert _utils.type_str(type_) == expected
+
+
+@pytest.mark.parametrize(  # type: ignore
+    "type_, expected",
+    [
+        (Optional[int], "Optional[int]"),
         (Union[str, int, Color], "Union[str, int, Color]"),
+        (Optional[Union[int]], "Optional[int]"),
+        (Optional[Union[int, str]], "Union[int, str, NoneType]"),
     ],
 )
-def test_type_str(type_: Any, expected: str) -> None:
+def test_type_str_union(type_: Any, expected: str) -> None:
     assert _utils.type_str(type_) == expected
+
+
+@pytest.mark.parametrize(  # type: ignore
+    "type_, expected",
+    [
+        (Dict[str, int], True),
+        (Dict[str, float], True),
+        (Dict[IllegalType, bool], True),
+        (Dict[str, IllegalType], True),
+        (Dict[int, Color], True),
+        (Dict[Plugin, Plugin], True),
+        (Dict[IllegalType, int], True),
+        (Dict, True),
+        (List, False),
+        (dict, False),
+        (DictConfig, False),
+    ],
+)
+def test_is_dict_annotation(type_: Any, expected: Any) -> Any:
+    assert is_dict_annotation(type_=type_) == expected
+
+
+@pytest.mark.parametrize(  # type: ignore
+    "type_, expected",
+    [
+        (List[int], True),
+        (List[float], True),
+        (List[bool], True),
+        (List[str], True),
+        (List[Color], True),
+        (List[Plugin], True),
+        (List[IllegalType], True),
+        (Dict, False),
+        (List, True),
+        (list, False),
+        (tuple, False),
+        (ListConfig, False),
+    ],
+)
+def test_is_list_annotation(type_: Any, expected: Any) -> Any:
+    assert is_list_annotation(type_=type_) == expected
+
+
+@pytest.mark.parametrize(  # type: ignore
+    "obj, expected",
+    [
+        # Unwrapped values
+        pytest.param(10, Optional[int], id="int"),
+        pytest.param(10.0, Optional[float], id="float"),
+        pytest.param(True, Optional[bool], id="bool"),
+        pytest.param("bar", Optional[str], id="str"),
+        pytest.param(None, type(None), id="NoneType"),
+        pytest.param({}, Optional[Dict[Any, Any]], id="dict"),
+        pytest.param([], Optional[List[Any]], id="List[Any]"),
+        pytest.param(tuple(), Optional[List[Any]], id="List[Any]"),
+        pytest.param(ConcretePlugin(), Optional[ConcretePlugin], id="ConcretePlugin"),
+        pytest.param(ConcretePlugin, Optional[ConcretePlugin], id="ConcretePlugin"),
+        # Optional value nodes
+        pytest.param(IntegerNode(10), Optional[int], id="IntegerNode"),
+        pytest.param(FloatNode(10.0), Optional[float], id="FloatNode"),
+        pytest.param(BooleanNode(True), Optional[bool], id="BooleanNode"),
+        pytest.param(StringNode("bar"), Optional[str], id="StringNode"),
+        pytest.param(
+            EnumNode(enum_type=Color, value=Color.RED),
+            Optional[Color],
+            id="EnumNode[Color]",
+        ),
+        # Non-optional value nodes:
+        pytest.param(IntegerNode(10, is_optional=False), int, id="IntegerNode"),
+        pytest.param(FloatNode(10.0, is_optional=False), float, id="FloatNode"),
+        pytest.param(BooleanNode(True, is_optional=False), bool, id="BooleanNode"),
+        pytest.param(StringNode("bar", is_optional=False), str, id="StringNode"),
+        pytest.param(
+            EnumNode(enum_type=Color, value=Color.RED, is_optional=False),
+            Color,
+            id="EnumNode[Color]",
+        ),
+        # DictConfig
+        pytest.param(DictConfig(content={}), Optional[Dict[Any, Any]], id="DictConfig"),
+        pytest.param(
+            DictConfig(key_type=int, element_type=Color, content={}),
+            Optional[Dict[int, Color]],
+            id="DictConfig[int,Color]",
+        ),
+        pytest.param(
+            DictConfig(key_type=Color, element_type=int, content={}),
+            Optional[Dict[Color, int]],
+            id="DictConfig[Color,int]",
+        ),
+        pytest.param(
+            DictConfig(ref_type=Any, content=ConcretePlugin),
+            Any,
+            id="DictConfig[ConcretePlugin]_Any_reftype",
+        ),
+        pytest.param(
+            DictConfig(ref_type=Plugin, content=ConcretePlugin),
+            Optional[Plugin],
+            id="DictConfig[Plugin]",
+        ),
+        pytest.param(
+            DictConfig(ref_type=Plugin, content=ConcretePlugin),
+            Optional[Plugin],
+            id="Plugin",
+        ),
+        # Non optional DictConfig
+        pytest.param(
+            DictConfig(ref_type=Plugin, content=ConcretePlugin, is_optional=False),
+            Plugin,
+            id="Plugin",
+        ),
+        # ListConfig
+        pytest.param(ListConfig([]), Optional[List[Any]], id="ListConfig[Any]"),
+        pytest.param(
+            ListConfig([], element_type=int), Optional[List[int]], id="ListConfig[int]",
+        ),
+    ],
+)
+def test_get_ref_type(obj: Any, expected: Any) -> None:
+    assert _utils.get_ref_type(obj) == expected