Repository: omry/omegaconf

Issue:
AssertionError constructued Structured Config with nested MISSING fields
my script ```my_test.py```

```python3
from dataclasses import MISSING, dataclass
from typing import Optional
from omegaconf import OmegaConf

@dataclass
class AConf:
    count: Optional[int] = MISSING

@dataclass
class SimpleConf:
    aconf: AConf = MISSING

def my_app() -> None:
    conf1 = OmegaConf.structured(SimpleConf)

if __name__ == "__main__":
    my_app()

```

run ```python my_test.py```

stack trace:
```bash
(hydra37) jieru-mbp:hydra jieru$ python /Users/jieru/workspace/hydra-fork/hydra/plugins/hydra_ray_launcher/my_test.py
Traceback (most recent call last):
  File "/Users/jieru/workspace/hydra-fork/hydra/plugins/hydra_ray_launcher/my_test.py", line 25, in <module>
    my_app()
  File "/Users/jieru/workspace/hydra-fork/hydra/plugins/hydra_ray_launcher/my_test.py", line 20, in my_app
    conf1 = OmegaConf.structured(SimpleConf)
  File "/Users/jieru/workspace/omegaconf/omegaconf/omegaconf.py", line 133, in structured
    return OmegaConf.create(obj, parent)
  File "/Users/jieru/workspace/omegaconf/omegaconf/omegaconf.py", line 160, in create
    return OmegaConf._create_impl(obj=obj, parent=parent)
  File "/Users/jieru/workspace/omegaconf/omegaconf/omegaconf.py", line 194, in _create_impl
    return DictConfig(content=obj, parent=parent, ref_type=ref_type)
  File "/Users/jieru/workspace/omegaconf/omegaconf/dictconfig.py", line 69, in __init__
    self._set_value(content)
  File "/Users/jieru/workspace/omegaconf/omegaconf/dictconfig.py", line 505, in _set_value
    data = get_structured_config_data(value)
  File "/Users/jieru/workspace/omegaconf/omegaconf/_utils.py", line 225, in get_structured_config_data
    return get_dataclass_data(obj)
  File "/Users/jieru/workspace/omegaconf/omegaconf/_utils.py", line 169, in get_dataclass_data
    ref_type=type_, is_optional=is_optional, key=name, value=value, parent=None,
  File "/Users/jieru/workspace/omegaconf/omegaconf/omegaconf.py", line 681, in _maybe_wrap
    parent=parent,
  File "/Users/jieru/workspace/omegaconf/omegaconf/dictconfig.py", line 69, in __init__
    self._set_value(content)
  File "/Users/jieru/workspace/omegaconf/omegaconf/dictconfig.py", line 490, in _set_value
    self._validate_set(key=None, value=value)
  File "/Users/jieru/workspace/omegaconf/omegaconf/dictconfig.py", line 127, in _validate_set
    self._validate_set_merge_impl(key, value, is_assign=True)
  File "/Users/jieru/workspace/omegaconf/omegaconf/dictconfig.py", line 186, in _validate_set_merge_impl
    value_type = OmegaConf.get_type(value)
  File "/Users/jieru/workspace/omegaconf/omegaconf/omegaconf.py", line 491, in get_type
    assert False, f"Unexpected type : {c}"
AssertionError: Unexpected type : <dataclasses._MISSING_TYPE object at 0x7f8f1004ef90>
```

If I fill both the MISSING values, this issue goes away.

```python
from dataclasses import MISSING, dataclass, field
from typing import Optional
from omegaconf import OmegaConf

@dataclass
class AConf:
    count: Optional[int] = 1

@dataclass
class SimpleConf:
    aconf: AConf = field(default_factory=AConf)

def my_app() -> None:
    conf1 = OmegaConf.structured(SimpleConf)

if __name__ == "__main__":
    my_app()

```

Discussion:

Patch:
diff --git a/.coveragerc b/.coveragerc
index 8b322c3..4386da5 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -15,6 +15,7 @@ exclude_lines =
     return NotImplemented
     assert False
     @abstractmethod
+    \.\.\.
 
 
 [html]
diff --git a/omegaconf/_utils.py b/omegaconf/_utils.py
index b42271e..8c7a6e4 100644
--- a/omegaconf/_utils.py
+++ b/omegaconf/_utils.py
@@ -104,6 +104,13 @@ def _resolve_forward(type_: Type[Any], module: str) -> Type[Any]:
         return type_
 
 
+def _raise_missing_error(obj: Any, name: str) -> None:
+    raise ValueError(
+        f"Missing default value for {get_type_of(obj).__name__}.{name}, to indicate "
+        "default must be populated later use OmegaConf.MISSING"
+    )
+
+
 def get_attr_data(obj: Any) -> Dict[str, Any]:
     from omegaconf.omegaconf import _maybe_wrap
 
@@ -122,10 +129,8 @@ def get_attr_data(obj: Any) -> Dict[str, Any]:
                 if is_nested:
                     value = type_
                 else:
-                    raise ValueError(
-                        f"Missing default value for {name}, to indicate "
-                        "default must be populated later use OmegaConf.MISSING"
-                    )
+                    _raise_missing_error(obj, name)
+                    assert False
         if _is_union(type_):
             e = ConfigValueError(
                 f"Union types are not supported:\n{name}: {type_str(type_)}"
@@ -147,19 +152,21 @@ def get_dataclass_data(obj: Any) -> Dict[str, Any]:
         is_optional, type_ = _resolve_optional(field.type)
         type_ = _resolve_forward(type_, obj.__module__)
         is_nested = is_structured_config(type_)
+
         if hasattr(obj, name):
             value = getattr(obj, name)
+            if value == dataclasses.MISSING:
+                _raise_missing_error(obj, name)
+                assert False
         else:
-            if field.default_factory != dataclasses.MISSING:  # type: ignore
-                value = field.default_factory()  # type: ignore
-            else:
+            if field.default_factory == dataclasses.MISSING:  # type: ignore
                 if is_nested:
                     value = type_
                 else:
-                    raise ValueError(
-                        f"Missing default value for {name}, to indicate "
-                        "default must be populated later use OmegaConf.MISSING"
-                    )
+                    _raise_missing_error(obj, name)
+                    assert False
+            else:
+                value = field.default_factory()  # type: ignore
 
         if _is_union(type_):
             e = ConfigValueError(
diff --git a/omegaconf/base.py b/omegaconf/base.py
index f1916d5..2c25eca 100644
--- a/omegaconf/base.py
+++ b/omegaconf/base.py
@@ -99,7 +99,7 @@ class Node(ABC):
 
     @abstractmethod
     def _get_full_key(self, key: Union[str, Enum, int, None]) -> str:
-        ...  # pragma: no cover
+        ...
 
     def _dereference_node(
         self, throw_on_missing: bool = False, throw_on_resolution_failure: bool = True
@@ -150,39 +150,39 @@ class Node(ABC):
 
     @abstractmethod
     def __eq__(self, other: Any) -> bool:
-        ...  # pragma: no cover
+        ...
 
     @abstractmethod
     def __ne__(self, other: Any) -> bool:
-        ...  # pragma: no cover
+        ...
 
     @abstractmethod
     def __hash__(self) -> int:
-        ...  # pragma: no cover
+        ...
 
     @abstractmethod
     def _value(self) -> Any:
-        ...  # pragma: no cover
+        ...
 
     @abstractmethod
     def _set_value(self, value: Any) -> None:
-        ...  # pragma: no cover
+        ...
 
     @abstractmethod
     def _is_none(self) -> bool:
-        ...  # pragma: no cover
+        ...
 
     @abstractmethod
     def _is_optional(self) -> bool:
-        ...  # pragma: no cover
+        ...
 
     @abstractmethod
     def _is_missing(self) -> bool:
-        ...  # pragma: no cover
+        ...
 
     @abstractmethod
     def _is_interpolation(self) -> bool:
-        ...  # pragma: no cover
+        ...
 
     def _key(self) -> Any:
         return self._metadata.key
@@ -200,34 +200,34 @@ class Container(Node):
 
     @abstractmethod
     def pretty(self, resolve: bool = False, sort_keys: bool = False) -> str:
-        ...  # pragma: no cover
+        ...
 
     @abstractmethod
     def update_node(self, key: str, value: Any = None) -> None:
-        ...  # pragma: no cover
+        ...
 
     @abstractmethod
     def select(self, key: str, throw_on_missing: bool = False) -> Any:
-        ...  # pragma: no cover
+        ...
 
     def _get_node(self, key: Any) -> Optional[Node]:
-        ...  # pragma: no cover
+        ...
 
     @abstractmethod
     def __delitem__(self, key: Any) -> None:
-        ...  # pragma: no cover
+        ...
 
     @abstractmethod
     def __setitem__(self, key: Any, value: Any) -> None:
-        ...  # pragma: no cover
+        ...
 
     @abstractmethod
     def __iter__(self) -> Iterator[str]:
-        ...  # pragma: no cover
+        ...
 
     @abstractmethod
     def __getitem__(self, key_or_index: Any) -> Any:
-        ...  # pragma: no cover
+        ...
 
     def _get_root(self) -> "Container":
         root: Optional[Container] = self._get_parent()
diff --git a/omegaconf/basecontainer.py b/omegaconf/basecontainer.py
index f074827..96e7dbb 100644
--- a/omegaconf/basecontainer.py
+++ b/omegaconf/basecontainer.py
@@ -90,7 +90,7 @@ class BaseContainer(Container, ABC):
 
     @abstractmethod
     def __delitem__(self, key: Any) -> None:
-        ...  # pragma: no cover
+        ...
 
     def __len__(self) -> int:
         return self.__dict__["_content"].__len__()  # type: ignore
@@ -267,11 +267,13 @@ class BaseContainer(Container, ABC):
         try:
             """merge a list of other Config objects into this one, overriding as needed"""
             for other in others:
-                if is_primitive_container(other) or is_structured_config(other):
-                    other = OmegaConf.create(other)
-
                 if other is None:
                     raise ValueError("Cannot merge with a None config")
+                if is_primitive_container(other):
+                    assert isinstance(other, (list, dict))
+                    other = OmegaConf.create(other)
+                elif is_structured_config(other):
+                    other = OmegaConf.structured(other)
                 if isinstance(self, DictConfig) and isinstance(other, DictConfig):
                     BaseContainer._map_merge(self, other)
                 elif isinstance(self, ListConfig) and isinstance(other, ListConfig):
@@ -439,11 +441,11 @@ class BaseContainer(Container, ABC):
 
     @abstractmethod
     def _validate_get(self, key: Any, value: Any = None) -> None:
-        ...  # pragma: no cover
+        ...
 
     @abstractmethod
     def _validate_set(self, key: Any, value: Any) -> None:
-        ...  # pragma: no cover
+        ...
 
     def _value(self) -> Any:
         return self.__dict__["_content"]
diff --git a/omegaconf/omegaconf.py b/omegaconf/omegaconf.py
index 6652a1e..c17fc26 100644
--- a/omegaconf/omegaconf.py
+++ b/omegaconf/omegaconf.py
@@ -37,7 +37,6 @@ from ._utils import (
     is_primitive_container,
     is_primitive_dict,
     is_primitive_list,
-    is_primitive_type,
     is_structured_config,
     isint,
     type_str,
@@ -88,22 +87,22 @@ def SI(interpolation: str) -> Any:
 
 class Resolver0(Protocol):
     def __call__(self) -> Any:
-        ...  # pragma: no cover
+        ...
 
 
 class Resolver1(Protocol):
     def __call__(self, __x1: str) -> Any:
-        ...  # pragma: no cover
+        ...
 
 
 class Resolver2(Protocol):
     def __call__(self, __x1: str, __x2: str) -> Any:
-        ...  # pragma: no cover
+        ...
 
 
 class Resolver3(Protocol):
     def __call__(self, __x1: str, __x2: str, __x3: str) -> Any:
-        ...  # pragma: no cover
+        ...
 
 
 Resolver = Union[Resolver0, Resolver1, Resolver2, Resolver3]
@@ -132,26 +131,36 @@ class OmegaConf:
     def structured(obj: Any, parent: Optional[BaseContainer] = None) -> Any:
         return OmegaConf.create(obj, parent)
 
+    @staticmethod
+    @overload
+    def create(
+        obj: str, parent: Optional[BaseContainer] = None
+    ) -> Union[DictConfig, ListConfig]:
+        ...
+
     @staticmethod
     @overload
     def create(
         obj: Union[List[Any], Tuple[Any, ...]], parent: Optional[BaseContainer] = None
     ) -> ListConfig:
-        ...  # pragma: no cover
+        ...
 
     @staticmethod
     @overload
-    def create(
-        obj: Union[BaseContainer, str], parent: Optional[BaseContainer] = None
-    ) -> Union[DictConfig, ListConfig]:
-        ...  # pragma: no cover
+    def create(obj: DictConfig, parent: Optional[BaseContainer] = None) -> DictConfig:
+        ...
+
+    @staticmethod
+    @overload
+    def create(obj: ListConfig, parent: Optional[BaseContainer] = None) -> ListConfig:
+        ...
 
     @staticmethod
     @overload
     def create(
-        obj: Union[Dict[str, Any], None] = None, parent: Optional[BaseContainer] = None
+        obj: Union[Dict[str, Any], None] = None, parent: Optional[BaseContainer] = None,
     ) -> DictConfig:
-        ...  # pragma: no cover
+        ...
 
     @staticmethod
     def create(  # noqa F811
@@ -271,8 +280,11 @@ class OmegaConf:
         """Merge a list of previously created configs into a single one"""
         assert len(others) > 0
         target = copy.deepcopy(others[0])
-        if is_primitive_container(target) or is_structured_config(target):
+        if is_primitive_container(target):
+            assert isinstance(target, (list, dict))
             target = OmegaConf.create(target)
+        elif is_structured_config(target):
+            target = OmegaConf.structured(target)
         assert isinstance(target, (DictConfig, ListConfig))
         with flag_override(target, "readonly", False):
             target.merge_with(*others[1:])
@@ -454,20 +466,19 @@ class OmegaConf:
 
     @staticmethod
     def get_type(obj: Any, key: Optional[str] = None) -> Optional[Type[Any]]:
-        if is_structured_config(obj):
-            return get_type_of(obj)
-        else:
-            if obj is None:
-                return None
-            if is_primitive_type(obj):
-                return get_type_of(obj)
-
         if key is not None:
             c = obj._get_node(key)
         else:
             c = obj
+        return OmegaConf._get_obj_type(c)
 
-        if isinstance(c, DictConfig):
+    @staticmethod
+    def _get_obj_type(c: Any) -> Optional[Type[Any]]:
+        if is_structured_config(c):
+            return get_type_of(c)
+        elif c is None:
+            return None
+        elif isinstance(c, DictConfig):
             if c._is_none():
                 return None
             elif c._is_missing():
@@ -486,7 +497,7 @@ class OmegaConf:
         elif isinstance(c, (list, tuple)):
             return list
         else:
-            assert False
+            return get_type_of(c)
 
     @staticmethod
     def get_ref_type(obj: Any, key: Optional[str] = None) -> Optional[Type[Any]]:


Test patch:
diff --git a/tests/structured_conf/data/attr_classes.py b/tests/structured_conf/data/attr_classes.py
index c9767b1..b46de5c 100644
--- a/tests/structured_conf/data/attr_classes.py
+++ b/tests/structured_conf/data/attr_classes.py
@@ -1,6 +1,6 @@
 from typing import Any, Dict, List, Optional, Tuple, Union
 
-import attr  # noqaE402
+import attr
 import pytest
 
 from omegaconf import II, MISSING, SI
@@ -392,3 +392,8 @@ class NestedWithNone:
 @attr.s(auto_attribs=True)
 class UnionError:
     x: Union[int, str] = 10
+
+
+@attr.s(auto_attribs=True)
+class WithNativeMISSING:
+    num: int = attr.NOTHING  # type: ignore
diff --git a/tests/structured_conf/data/dataclasses.py b/tests/structured_conf/data/dataclasses.py
index 6851d0c..2f66266 100644
--- a/tests/structured_conf/data/dataclasses.py
+++ b/tests/structured_conf/data/dataclasses.py
@@ -1,4 +1,5 @@
-from dataclasses import dataclass, field  # noqaE402
+import dataclasses
+from dataclasses import dataclass, field
 from typing import Any, Dict, List, Optional, Tuple, Union
 
 import pytest
@@ -399,3 +400,8 @@ class NestedWithNone:
 @dataclass
 class UnionError:
     x: Union[int, str] = 10
+
+
+@dataclass
+class WithNativeMISSING:
+    num: int = dataclasses.MISSING  # type: ignore
diff --git a/tests/structured_conf/test_structured_basic.py b/tests/structured_conf/test_structured_basic.py
index a9df11f..e6e59ca 100644
--- a/tests/structured_conf/test_structured_basic.py
+++ b/tests/structured_conf/test_structured_basic.py
@@ -165,3 +165,14 @@ class TestStructured:
 
             assert OmegaConf.get_ref_type(cfg, "plugin2") == module.Plugin
             assert OmegaConf.get_type(cfg, "plugin2") == module.ConcretePlugin
+
+        def test_native_missing(self, class_type: str) -> None:
+            module: Any = import_module(class_type)
+            with pytest.raises(
+                ValueError,
+                match=re.escape(
+                    "Missing default value for WithNativeMISSING.num,"
+                    " to indicate default must be populated later use OmegaConf.MISSING"
+                ),
+            ):
+                OmegaConf.create(module.WithNativeMISSING)
diff --git a/tests/structured_conf/test_structured_config.py b/tests/structured_conf/test_structured_config.py
index 98a4977..bdfb843 100644
--- a/tests/structured_conf/test_structured_config.py
+++ b/tests/structured_conf/test_structured_config.py
@@ -148,8 +148,8 @@ class TestConfigs:
         with pytest.raises(
             ValueError,
             match=re.escape(
-                "Missing default value for no_default, "
-                "to indicate default must be populated later use OmegaConf.MISSING"
+                "Missing default value for NoDefaultErrors.no_default,"
+                " to indicate default must be populated later use OmegaConf.MISSING"
             ),
         ):
             OmegaConf.structured(module.NoDefaultErrors)
@@ -447,6 +447,16 @@ class TestConfigs:
         res = OmegaConf.merge(c1, c2)
         assert OmegaConf.get_type(res) == module.ConcretePlugin
 
+    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 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 OmegaConf.get_type(res, "missing") == module.Plugin
+
     def test_merged_with_nons_subclass(self, class_type: str) -> None:
         module: Any = import_module(class_type)
         c1 = OmegaConf.structured(module.Plugin)
diff --git a/tests/test_omegaconf.py b/tests/test_omegaconf.py
index 880c2e3..e37e0a9 100644
--- a/tests/test_omegaconf.py
+++ b/tests/test_omegaconf.py
@@ -17,7 +17,14 @@ from omegaconf import (
 )
 from omegaconf.errors import UnsupportedInterpolationType
 
-from . import Color, ConcretePlugin, Plugin, StructuredWithMissing, does_not_raise
+from . import (
+    Color,
+    ConcretePlugin,
+    IllegalType,
+    Plugin,
+    StructuredWithMissing,
+    does_not_raise,
+)
 
 
 @pytest.mark.parametrize(  # type: ignore
@@ -360,6 +367,23 @@ def test_get_type(cfg: Any, type_: Any) -> None:
     assert OmegaConf.get_type(cfg, "foo") == type_
 
 
+@pytest.mark.parametrize(  # type: ignore
+    "obj, type_",
+    [
+        (10, int),
+        (10.0, float),
+        (True, bool),
+        ("foo", str),
+        (DictConfig(content={}), dict),
+        (ListConfig(content=[]), list),
+        (IllegalType, IllegalType),
+        (IllegalType(), IllegalType),
+    ],
+)
+def test_get_type_on_raw(obj: Any, type_: Any) -> None:
+    assert OmegaConf.get_type(obj) == type_
+
+
 @pytest.mark.parametrize(  # type: ignore
     "cfg, type_",
     [