Repository: Cog-Creators/Red-DiscordBot

Issue:
[V3 JSON] Direct reference to mutable datastore being returned
# Other bugs
The JSON driver is returning direct references to the driver's cache. This is should not be happening as mutating the returned value will cause the mutation to also occur in the cache. This can be shown by the following:
```py
>>> dict1 = await self.conf.dict()
>>> dict1
{}
>>> dict1["somekey"] = "somevalue"
>>> await self.conf.dict()
{"somekey": "somevalue"}
```

IMO it's the driver's responsibility to return a copy from its `get()` method, not Config's.


Discussion:

Resolving patch:
diff --git a/redbot/core/config.py b/redbot/core/config.py
--- a/redbot/core/config.py
+++ b/redbot/core/config.py
@@ -335,7 +335,7 @@ async def get_raw(self, *nested_path: str, default=...):
                 default = poss_default
 
         try:
-            return deepcopy(await self.driver.get(*self.identifiers, *path))
+            return await self.driver.get(*self.identifiers, *path)
         except KeyError:
             if default is not ...:
                 return default
@@ -365,7 +365,7 @@ def nested_update(self, current, defaults=None):
 
         """
         if not defaults:
-            defaults = deepcopy(self.defaults)
+            defaults = self.defaults
 
         for key, value in current.items():
             if isinstance(value, collections.Mapping):
diff --git a/redbot/core/drivers/red_json.py b/redbot/core/drivers/red_json.py
--- a/redbot/core/drivers/red_json.py
+++ b/redbot/core/drivers/red_json.py
@@ -1,5 +1,6 @@
 from pathlib import Path
 from typing import Tuple
+import copy
 import weakref
 import logging
 
@@ -97,7 +98,7 @@ async def get(self, *identifiers: Tuple[str]):
         full_identifiers = (self.unique_cog_identifier, *identifiers)
         for i in full_identifiers:
             partial = partial[i]
-        return partial
+        return copy.deepcopy(partial)
 
     async def set(self, *identifiers: str, value=None):
         partial = self.data
@@ -107,7 +108,7 @@ async def set(self, *identifiers: str, value=None):
                 partial[i] = {}
             partial = partial[i]
 
-        partial[full_identifiers[-1]] = value
+        partial[full_identifiers[-1]] = copy.deepcopy(value)
         await self.jsonIO._threadsafe_save_json(self.data)
 
     async def clear(self, *identifiers: str):


Test patch:
diff --git a/tests/core/test_config.py b/tests/core/test_config.py
--- a/tests/core/test_config.py
+++ b/tests/core/test_config.py
@@ -379,13 +379,9 @@ async def test_value_ctxmgr_saves(config):
 async def test_value_ctxmgr_immutable(config):
     config.register_global(foo=True)
 
-    try:
+    with pytest.raises(TypeError):
         async with config.foo() as foo:
             foo = False
-    except TypeError:
-        pass
-    else:
-        raise AssertionError
 
     foo = await config.foo()
     assert foo is True
@@ -401,3 +397,25 @@ async def test_ctxmgr_no_shared_default(config, member_factory):
         foo.append(1)
 
     assert 1 not in await config.member(m2).foo()
+
+
+@pytest.mark.asyncio
+async def test_get_then_mutate(config):
+    """Tests that mutating an object after getting it as a value doesn't mutate the data store."""
+    config.register_global(list1=[])
+    await config.list1.set([])
+    list1 = await config.list1()
+    list1.append("foo")
+    list1 = await config.list1()
+    assert "foo" not in list1
+
+
+@pytest.mark.asyncio
+async def test_set_then_mutate(config):
+    """Tests that mutating an object after setting it as a value doesn't mutate the data store."""
+    config.register_global(list1=[])
+    list1 = []
+    await config.list1.set(list1)
+    list1.append("foo")
+    list1 = await config.list1()
+    assert "foo" not in list1