Repository: omry/omegaconf

Issue:
Error merging dict into a Dict[str, str]
```python
from dataclasses import dataclass, field
from typing import Dict
from omegaconf import OmegaConf


@dataclass
class JobConf:
    env: Dict[str, str] = field(default_factory=dict)


cfg = OmegaConf.create({"job": JobConf})
res = OmegaConf.merge(cfg, {"job": {"env": {"abc": "ABC"}}})
assert res.job == {"env": {"abc": "ABC"}}
```

Results in:
```
AssertionError: Unsupported value type : <class 'str'>
        full_key: job.env
        reference_type=Dict[str, str]
        object_type=dict

        full_key: job
        reference_type=Optional[JobConf]
        object_type=JobConf

        full_key: 
        reference_type=Optional[Dict[Any, Any]]
        object_type=dict
```

Discussion:

Patch:
diff --git a/news/246.bugfix b/news/246.bugfix
new file mode 100644
index 0000000..0f57fd0
--- /dev/null
+++ b/news/246.bugfix
@@ -0,0 +1,1 @@
+Fixes merging of dict into a Dict[str, str]
\ No newline at end of file
diff --git a/omegaconf/basecontainer.py b/omegaconf/basecontainer.py
index eb22fe7..cafef05 100644
--- a/omegaconf/basecontainer.py
+++ b/omegaconf/basecontainer.py
@@ -239,31 +239,30 @@ class BaseContainer(Container, ABC):
 
         dest._validate_set_merge_impl(key=None, value=src, is_assign=False)
         for key, src_value in src.items_ex(resolve=False):
-            dest_element_type = dest._metadata.element_type
-            element_typed = dest_element_type not in (None, Any)
             if OmegaConf.is_missing(dest, key):
                 if isinstance(src_value, DictConfig):
                     if OmegaConf.is_missing(dest, key):
                         dest[key] = src_value
 
             dest_node = dest._get_node(key, validate_access=False)
-            if dest_node is not None and dest_node._is_interpolation():
-                target_node = dest_node._dereference_node(
-                    throw_on_resolution_failure=False
-                )
-                if isinstance(target_node, Container):
-                    dest[key] = target_node
-                    dest_node = dest._get_node(key)
+            if dest_node is not None:
+                if dest_node._is_interpolation():
+                    target_node = dest_node._dereference_node(
+                        throw_on_resolution_failure=False
+                    )
+                    if isinstance(target_node, Container):
+                        dest[key] = target_node
+                        dest_node = dest._get_node(key)
 
-            if dest_node is not None or element_typed:
-                if dest_node is None and element_typed:
-                    dest[key] = DictConfig(content=dest_element_type, parent=dest)
-                    dest_node = dest._get_node(key)
+            if is_structured_config(dest._metadata.element_type):
+                dest[key] = DictConfig(content=dest._metadata.element_type, parent=dest)
+                dest_node = dest._get_node(key)
 
+            if dest_node is not None:
                 if isinstance(dest_node, BaseContainer):
                     if isinstance(src_value, BaseContainer):
                         dest._validate_merge(key=key, value=src_value)
-                        dest_node.merge_with(src_value)
+                        dest_node._merge_with(src_value)
                     else:
                         dest.__setitem__(key, src_value)
                 else:
@@ -284,43 +283,45 @@ class BaseContainer(Container, ABC):
     def merge_with(
         self,
         *others: Union["BaseContainer", Dict[str, Any], List[Any], Tuple[Any], Any],
+    ) -> None:
+        try:
+            self._merge_with(*others)
+        except Exception as e:
+            self._format_and_raise(key=None, value=None, cause=e)
+
+    def _merge_with(
+        self,
+        *others: Union["BaseContainer", Dict[str, Any], List[Any], Tuple[Any], Any],
     ) -> None:
         from .dictconfig import DictConfig
         from .listconfig import ListConfig
         from .omegaconf import OmegaConf
 
-        try:
-            """merge a list of other Config objects into this one, overriding as needed"""
-            for other in others:
-                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):
-                    if self._get_flag("readonly"):
-                        raise ReadonlyConfigError(self._get_full_key(""))
-                    if (
-                        self._is_none()
-                        or self._is_missing()
-                        or self._is_interpolation()
-                    ):
-                        self.__dict__["_content"] = []
-                    else:
-                        self.__dict__["_content"].clear()
-                    for item in other:
-                        self.append(item)
+        """merge a list of other Config objects into this one, overriding as needed"""
+        for other in others:
+            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):
+                if self._get_flag("readonly"):
+                    raise ReadonlyConfigError(self._get_full_key(""))
+                if self._is_none() or self._is_missing() or self._is_interpolation():
+                    self.__dict__["_content"] = []
                 else:
-                    raise TypeError("Cannot merge DictConfig with ListConfig")
+                    self.__dict__["_content"].clear()
+                for item in other:
+                    self.append(item)
+            else:
+                raise TypeError("Cannot merge DictConfig with ListConfig")
 
-            # recursively correct the parent hierarchy after the merge
-            self._re_parent()
-        except Exception as e:
-            self._format_and_raise(key=None, value=None, cause=e)
+        # recursively correct the parent hierarchy after the merge
+        self._re_parent()
 
     # noinspection PyProtectedMember
     def _set_item_impl(self, key: Any, value: Any) -> None:
diff --git a/omegaconf/version.py b/omegaconf/version.py
index a723726..c24d79d 100644
--- a/omegaconf/version.py
+++ b/omegaconf/version.py
@@ -1,6 +1,6 @@
 import sys  # pragma: no cover
 
-__version__ = "2.0.0"
+__version__ = "2.0.1rc1"
 
 msg = """OmegaConf 2.0 and above is compatible with Python 3.6 and newer.
 You have the following options:


Test patch:
diff --git a/tests/structured_conf/test_structured_config.py b/tests/structured_conf/test_structured_config.py
index 2e544de..73d446d 100644
--- a/tests/structured_conf/test_structured_config.py
+++ b/tests/structured_conf/test_structured_config.py
@@ -465,6 +465,12 @@ class TestConfigs:
         with pytest.raises(ValidationError):
             OmegaConf.merge(c1, c2)
 
+    def test_merge_into_Dict(self, class_type: str) -> None:
+        module: Any = import_module(class_type)
+        cfg = OmegaConf.structured(module.DictExamples)
+        res = OmegaConf.merge(cfg, {"strings": {"x": "abc"}})
+        assert res.strings == {"a": "foo", "b": "bar", "x": "abc"}
+
     def test_typed_dict_key_error(self, class_type: str) -> None:
         module: Any = import_module(class_type)
         input_ = module.ErrorDictIntKey
@@ -532,11 +538,6 @@ class TestConfigs:
     def test_dict_examples(self, class_type: str) -> None:
         module: Any = import_module(class_type)
         conf = OmegaConf.structured(module.DictExamples)
-        # any: Dict = {"a": 1, "b": "foo"}
-        # ints: Dict[str, int] = {"a": 10, "b": 20}
-        # strings: Dict[str, str] = {"a": "foo", "b": "bar"}
-        # booleans: Dict[str, bool] = {"a": True, "b": False}
-        # colors: Dict[str, Color] = {"red": Color.RED, "green": "GREEN", "blue": 3}
 
         def test_any(name: str) -> None:
             conf[name].c = True
diff --git a/tests/test_merge.py b/tests/test_merge.py
index 5aea074..56f80fe 100644
--- a/tests/test_merge.py
+++ b/tests/test_merge.py
@@ -125,11 +125,16 @@ from . import (
             [Users, {"name2user": {"joe": User(name="joe")}}],
             {"name2user": {"joe": {"name": "joe", "age": MISSING}}},
         ),
-        (
+        pytest.param(
             [Users, {"name2user": {"joe": {"name": "joe"}}}],
             {"name2user": {"joe": {"name": "joe", "age": MISSING}}},
+            id="users_merge_with_missing_age",
+        ),
+        pytest.param(
+            [ConfWithMissingDict, {"dict": {"foo": "bar"}}],
+            {"dict": {"foo": "bar"}},
+            id="conf_missing_dict",
         ),
-        ([ConfWithMissingDict, {"dict": {"foo": "bar"}}], {"dict": {"foo": "bar"}}),
         pytest.param(
             [{}, ConfWithMissingDict],
             {"dict": "???"},