Repository: Cog-Creators/Red-DiscordBot

Issue:
[Config] Calling get_conf twice for the same instance corrupts the database
# Other bugs

There is an issue when calling `Config.get_conf` two times for the same instance (from another cog or an eval). Both config instances will conflict, as I see, one isn't aware of what the other do, which means that if we do something with our second instance, the first one will be corrupted (didn't manage to recover my data after a restart).

#### What were you trying to do?

Edit a Config instance of a loaded cog with an eval command:

```py
from redbot.core import Config
conf = Config.get_conf(None, 42, cog_name="whatever")
# here you can pretty much do anything, either reading or writing, the result will be the same
await conf.foo.set("no")
return await conf.bar()
```

#### What were you expecting to happen?

Read/write the value.

#### What actually happened?

The read/write is successful, but the cog's config instance will be corrupted, giving errors for pretty much anything related to config. Those errors are the most common:

*Note: the key is the Red server's ID, trying to access `conf.guild(ctx.guild)` which was never called before (no data for that guild)*
```py
Traceback (most recent call last):
  File "C:\Users\Laggron\AppData\Local\Programs\Python\Python37\lib\site-packages\discord\ext\commands\core.py", line 79, in wrapped
    ret = await coro(*args, **kwargs)
  File "<string>", line 7, in readconf
  File "C:\Users\Laggron\AppData\Local\Programs\Python\Python37\lib\site-packages\redbot\core\config.py", line 477, in get_raw
    raw = await self.driver.get(identifier_data)
  File "C:\Users\Laggron\AppData\Local\Programs\Python\Python37\lib\site-packages\redbot\core\drivers\json.py", line 132, in get
    partial = partial[i]
KeyError: '363008468602454017'
```

*Trying to get a random value*
```py
Traceback (most recent call last):
  File "C:\Users\Laggron\AppData\Local\Programs\Python\Python37\lib\site-packages\discord\ext\commands\core.py", line 79, in wrapped
    ret = await coro(*args, **kwargs)
  File "<string>", line 7, in readconf
  File "C:\Users\Laggron\AppData\Local\Programs\Python\Python37\lib\site-packages\redbot\core\config.py", line 477, in get_raw
    raw = await self.driver.get(identifier_data)
  File "C:\Users\Laggron\AppData\Local\Programs\Python\Python37\lib\site-packages\redbot\core\drivers\json.py", line 132, in get
    partial = partial[i]
KeyError: 'embed_description_user'
```

*That one is probably the most common: attribute error on "Value" object*
```py
Traceback (most recent call last):
  File "C:\Users\Laggron\AppData\Local\Programs\Python\Python37\lib\site-packages\discord\ext\commands\core.py", line 79, in wrapped
    ret = await coro(*args, **kwargs)
  File "D:\OneDrive\Documents\Laggrons-Dumb-Cogs\warnsystem\settings.py", line 574, in warnset_settings
    modlog_channels = await self.api.get_modlog_channel(guild, "all")
  File "D:\OneDrive\Documents\Laggrons-Dumb-Cogs\warnsystem\api.py", line 518, in get_modlog_channel
    return await self.data.guild(guild).channels.all()
AttributeError: 'Value' object has no attribute 'all'
```

*I got that one after calling `conf.clear_all()` with eval, then tried some commands*
```py
Traceback (most recent call last):
  File "C:\Users\Laggron\AppData\Local\Programs\Python\Python37\lib\site-packages\discord\ext\commands\core.py", line 79, in wrapped
    ret = await coro(*args, **kwargs)
  File "C:\Users\Laggron\AppData\Local\Programs\Python\Python37\lib\site-packages\redbot\cogs\customcom\customcom.py", line 347, in cc_list
    cc_dict = await CommandObj.get_commands(self.config.guild(ctx.guild))
  File "C:\Users\Laggron\AppData\Local\Programs\Python\Python37\lib\site-packages\redbot\cogs\customcom\customcom.py", line 48, in get_commands
    return {k: v for k, v in _commands.items() if _commands[k]}
AttributeError: 'NoneType' object has no attribute 'items'
```

Those errors occurs with future evals or the cog itself.

Most of those are from my WarnSystem cog. However, I tried with the core custom commands cog (see the steps I did below).

#### How can we reproduce this issue?

For breaking the core custom commands cog (tested on the dev build):

1. Create a custom command and test it (did it on a fresh instance)

2. Eval the following code:
    ```py
    from redbot.core import Config
    c = Config.get_conf(None, 414589031223512, cog_name="CustomCommands")
    return await c.guild(guild).commands()
    ```
    Output on my side:
    ```py
    {'hello': {'author': {'id': 348415857728159745, 'name': 'El Laggron#0260'}, 'command': 'hello', 'cooldowns': {}, 'created_at': '18/11/2019 09:18:21', 'editors': [], 'response': 'no'}}
    ```

3. Try to invoke the custom command again, and see:
    ```py
    Ignoring exception in on_message
    Traceback (most recent call last):
      File "c:\users\laggron\appdata\local\programs\python\python37\lib\site-packages\discord\client.py", line 270, in _run_event
        await coro(*args, **kwargs)
      File "c:\users\laggron\appdata\local\programs\python\python37\lib\site-packages\redbot\cogs\customcom\customcom.py", line 461, in on_message
        message=message, command=ctx.invoked_with
      File "c:\users\laggron\appdata\local\programs\python\python37\lib\site-packages\redbot\cogs\customcom\customcom.py", line 89, in get
        ccinfo = await self.db(message.guild).commands.get_raw(command, default=None)
    AttributeError: 'Value' object has no attribute 'get_raw'
    ```




Discussion:
I can reproduce this on commit https://github.com/Cog-Creators/Red-DiscordBot/commit/b3363acf77e834a46e2948ea24feb38603a61ee2. 

1. `a.customcom create laggron betterlaggron`
2. Eval command
```
a.eval from redbot.core import Config
c = Config.get_conf(None, 414589031223512, cog_name="CustomCommands")
return await c.guild(guild).commands()
```
3. Error
```py
Ignoring exception in on_message
Traceback (most recent call last):
  File "/data/venv/lib/python3.7/site-packages/discord/client.py", line 270, in _run_event
    await coro(*args, **kwargs)
  File "/data/venv/lib/python3.7/site-packages/redbot/cogs/customcom/customcom.py", line 459, in on_message
    message=message, command=ctx.invoked_with
  File "/data/venv/lib/python3.7/site-packages/redbot/cogs/customcom/customcom.py", line 89, in get
    ccinfo = await self.db(message.guild).commands.get_raw(command, default=None)
AttributeError: 'Value' object has no attribute 'get_raw'
```

Indeed I tested Laggron issue and I got exactly the same error.
For context I am on same commit as @Stonedestroyer.

Resolving patch:
diff --git a/redbot/core/config.py b/redbot/core/config.py
--- a/redbot/core/config.py
+++ b/redbot/core/config.py
@@ -30,6 +30,33 @@
 _retrieved = weakref.WeakSet()
 
 
+class ConfigMeta(type):
+    """
+    We want to prevent re-initializing existing config instances while having a singleton
+    """
+
+    def __call__(
+        cls,
+        cog_name: str,
+        unique_identifier: str,
+        driver: BaseDriver,
+        force_registration: bool = False,
+        defaults: dict = None,
+    ):
+        if cog_name is None:
+            raise ValueError("You must provide either the cog instance or a cog name.")
+
+        key = (cog_name, unique_identifier)
+        if key in _config_cache:
+            return _config_cache[key]
+
+        instance = super(ConfigMeta, cls).__call__(
+            cog_name, unique_identifier, driver, force_registration, defaults
+        )
+        _config_cache[key] = instance
+        return instance
+
+
 def get_latest_confs() -> Tuple["Config"]:
     global _retrieved
     ret = set(_config_cache.values()) - set(_retrieved)
@@ -562,7 +589,7 @@ async def set_raw(self, *nested_path: Any, value):
         await self.driver.set(identifier_data, value=value)
 
 
-class Config:
+class Config(metaclass=ConfigMeta):
     """Configuration manager for cogs and Red.
 
     You should always use `get_conf` to instantiate a Config object. Use
@@ -605,19 +632,6 @@ class Config:
     USER = "USER"
     MEMBER = "MEMBER"
 
-    def __new__(cls, cog_name, unique_identifier, *args, **kwargs):
-        key = (cog_name, unique_identifier)
-
-        if key[0] is None:
-            raise ValueError("You must provide either the cog instance or a cog name.")
-
-        if key in _config_cache:
-            conf = _config_cache[key]
-        else:
-            conf = object.__new__(cls)
-            _config_cache[key] = conf
-        return conf
-
     def __init__(
         self,
         cog_name: str,
@@ -893,10 +907,10 @@ def init_custom(self, group_identifier: str, identifier_count: int):
         """
         Initializes a custom group for usage. This method must be called first!
         """
-        if group_identifier in self.custom_groups:
-            raise ValueError(f"Group identifier already registered: {group_identifier}")
-
-        self.custom_groups[group_identifier] = identifier_count
+        if identifier_count != self.custom_groups.setdefault(group_identifier, identifier_count):
+            raise ValueError(
+                f"Cannot change identifier count of already registered group: {group_identifier}"
+            )
 
     def _get_base_group(self, category: str, *primary_keys: str) -> Group:
         """


Test patch: