Repository: omry/omegaconf

Issue:
OmegaConf object doesn't support a .copy method
A dict is supposed to have a .copy() method (https://docs.python.org/3/library/stdtypes.html#dict.copy). For an OmegaConf object to be compatible with a dict's API, it should also support the copy() method -

```
>>> from omegaconf import OmegaConf
>>> conf = OmegaConf.create({"a": 2, "b": [1,2,3]})
>>> conf
{'a': 2, 'b': [1, 2, 3]}
>>> conf.copy()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable
```

Comments:

Patch:
diff --git a/omegaconf/dictconfig.py b/omegaconf/dictconfig.py
index 7c180f8..808a687 100644
--- a/omegaconf/dictconfig.py
+++ b/omegaconf/dictconfig.py
@@ -1,4 +1,5 @@
 from .config import Config
+import copy
 from .errors import (
     ReadonlyConfigError,
     MissingMandatoryValue,
@@ -21,6 +22,15 @@ class DictConfig(Config):
         self._deepcopy_impl(res)
         return res
 
+    def __copy__(self):
+        res = DictConfig({})
+        res.__dict__["content"] = copy.copy(self.__dict__["content"])
+        res.__dict__["parent"] = self.__dict__["parent"]
+        return res
+
+    def copy(self):
+        return copy.copy(self)
+
     def __setitem__(self, key, value):
         assert isinstance(key, str)
 
diff --git a/omegaconf/listconfig.py b/omegaconf/listconfig.py
index 993ba31..3d56753 100644
--- a/omegaconf/listconfig.py
+++ b/omegaconf/listconfig.py
@@ -121,7 +121,7 @@ class ListConfig(Config):
         return c
 
     def copy(self):
-        return self[:]
+        return copy.copy(self)
 
     if six.PY2:
 


Test patch:
diff --git a/tests/test_base_config.py b/tests/test_base_config.py
index a35f714..747b4e4 100644
--- a/tests/test_base_config.py
+++ b/tests/test_base_config.py
@@ -332,3 +332,45 @@ def test_open_dict_restore(flag_name, ctx):
         cfg.foo.bar = 20
     assert cfg._get_node_flag(flag_name)
     assert not cfg.foo._get_node_flag(flag_name)
+
+
+@pytest.mark.parametrize(
+    "copy_method", [lambda x: copy.copy(x), lambda x: x.copy()],
+)
+class TestCopy:
+    @pytest.mark.parametrize(
+        "src", [[], [1, 2], ["a", "b", "c"], {}, {"a": "b"}, {"a": {"b": []}}],
+    )
+    def test_copy(self, copy_method, src):
+        src = OmegaConf.create(src)
+        cp = copy_method(src)
+        assert id(src) != id(cp)
+        assert src == cp
+
+    @pytest.mark.parametrize(
+        "src,interpolating_key,interpolated_key",
+        [([1, 2, "${0}"], 2, 0), ({"a": 10, "b": "${a}"}, "b", "a")],
+    )
+    def test_copy_with_interpolation(
+        self, copy_method, src, interpolating_key, interpolated_key
+    ):
+        cfg = OmegaConf.create(src)
+        assert cfg[interpolated_key] == cfg[interpolating_key]
+        cp = copy_method(cfg)
+        assert id(cfg) != id(cp)
+        assert cp[interpolated_key] == cp[interpolating_key]
+        assert cfg[interpolated_key] == cp[interpolating_key]
+
+        # Interpolation is preserved in original
+        cfg[interpolated_key] = "XXX"
+        assert cfg[interpolated_key] == cfg[interpolating_key]
+
+        # Test interpolation is preserved in copy
+        cp[interpolated_key] = "XXX"
+        assert cp[interpolated_key] == cp[interpolating_key]
+
+    def test_list_copy_is_shallow(self, copy_method):
+        cfg = OmegaConf.create([[10, 20]])
+        cp = copy_method(cfg)
+        assert id(cfg) != id(cp)
+        assert id(cfg[0]) == id(cp[0])
diff --git a/tests/test_basic_ops_list.py b/tests/test_basic_ops_list.py
index 9060228..32ec4db 100644
--- a/tests/test_basic_ops_list.py
+++ b/tests/test_basic_ops_list.py
@@ -252,14 +252,6 @@ def test_count(src, item, count):
     assert src.count(item) == count
 
 
-@pytest.mark.parametrize("src", [[], [1, 2], ["a", "b", "c"]])
-def test_copy(src):
-    src = OmegaConf.create(src)
-    cp = src.copy()
-    assert id(src) != id(cp)
-    assert src == cp
-
-
 def test_sort():
     c = OmegaConf.create(["bbb", "aa", "c"])
     c.sort()