Repository: Cog-Creators/Red-DiscordBot

Issue:
[V3 CogManager] Paths being added implicitly and incorrectly
# Other bugs

Paths (specifically the core cogs path and the cog install path) are being added to CogManager's config implicitly and incorrectly whenever `CogManager.add_path()` or `CogManager.remove_path()` is called. Basically, the methods get the existing list of paths with `CogManger.paths()`, which returns all valid path directories including the cog install path and the core cogs path. The new/old path is then added/removed from this list and the list is set again.

This is undesirable since:
 1. The install path will stay in the list of valid cog paths even after it's been changed
 2. The core cogs path should only be valid within the scope of a single *install*, not the scope of an *instance* (multiple installs can use one instance)

#### How can we reproduce this issue?

1. Use `[p]addpath` or `[p]removepath` once in any valid way
2. Use `[p]paths` and take note of the install path
3. Use `[p]installpath` to change the install path
4. Use `[p]paths` again and notice that the install path is still in the list of valid paths
5. Also notice that the core cogs path (located at red's install location) is listed as a valid path, even if you change the install location.



Discussion:

Resolving patch:
diff --git a/redbot/core/cog_manager.py b/redbot/core/cog_manager.py
--- a/redbot/core/cog_manager.py
+++ b/redbot/core/cog_manager.py
@@ -35,12 +35,13 @@ class CogManager:
     bot directory.
     """
 
+    CORE_PATH = Path(redbot.cogs.__path__[0])
+
     def __init__(self, paths: Tuple[str] = ()):
         self.conf = Config.get_conf(self, 2938473984732, True)
         tmp_cog_install_path = cog_data_path(self) / "cogs"
         tmp_cog_install_path.mkdir(parents=True, exist_ok=True)
-        self.conf.register_global(paths=(), install_path=str(tmp_cog_install_path))
-
+        self.conf.register_global(paths=[], install_path=str(tmp_cog_install_path))
         self._paths = [Path(p) for p in paths]
 
     async def paths(self) -> Tuple[Path, ...]:
@@ -54,18 +55,13 @@ async def paths(self) -> Tuple[Path, ...]:
         """
         conf_paths = [Path(p) for p in await self.conf.paths()]
         other_paths = self._paths
-        core_paths = await self.core_paths()
 
-        all_paths = _deduplicate(list(conf_paths) + list(other_paths) + core_paths)
+        all_paths = _deduplicate(list(conf_paths) + list(other_paths) + [self.CORE_PATH])
 
         if self.install_path not in all_paths:
             all_paths.insert(0, await self.install_path())
         return tuple(p.resolve() for p in all_paths if p.is_dir())
 
-    async def core_paths(self) -> List[Path]:
-        core_paths = [Path(p) for p in redbot.cogs.__path__]
-        return core_paths
-
     async def install_path(self) -> Path:
         """Get the install path for 3rd party cogs.
 
@@ -155,10 +151,12 @@ async def add_path(self, path: Union[Path, str]):
 
         if path == await self.install_path():
             raise ValueError("Cannot add the install path as an additional path.")
+        if path == self.CORE_PATH:
+            raise ValueError("Cannot add the core path as an additional path.")
 
-        all_paths = _deduplicate(await self.paths() + (path,))
-        # noinspection PyTypeChecker
-        await self.set_paths(all_paths)
+        async with self.conf.paths() as paths:
+            if not any(Path(p) == path for p in paths):
+                paths.append(str(path))
 
     async def remove_path(self, path: Union[Path, str]) -> Tuple[Path, ...]:
         """Remove a path from the current paths list.
@@ -174,12 +172,14 @@ async def remove_path(self, path: Union[Path, str]) -> Tuple[Path, ...]:
             Tuple of new valid paths.
 
         """
-        path = self._ensure_path_obj(path)
-        all_paths = list(await self.paths())
-        if path in all_paths:
-            all_paths.remove(path)  # Modifies in place
-            await self.set_paths(all_paths)
-        return tuple(all_paths)
+        path = self._ensure_path_obj(path).resolve()
+
+        paths = [Path(p) for p in await self.conf.paths()]
+        if path in paths:
+            paths.remove(path)
+            await self.set_paths(paths)
+
+        return tuple(paths)
 
     async def set_paths(self, paths_: List[Path]):
         """Set the current paths list.
@@ -213,9 +213,8 @@ async def _find_ext_cog(self, name: str) -> ModuleSpec:
             When no matching spec can be found.
         """
         resolved_paths = _deduplicate(await self.paths())
-        core_paths = _deduplicate(await self.core_paths())
 
-        real_paths = [str(p) for p in resolved_paths if p not in core_paths]
+        real_paths = [str(p) for p in resolved_paths if p != self.CORE_PATH]
 
         for finder, module_name, _ in pkgutil.iter_modules(real_paths):
             if name == module_name:
@@ -228,7 +227,8 @@ async def _find_ext_cog(self, name: str) -> ModuleSpec:
             " in any available path.".format(name)
         )
 
-    async def _find_core_cog(self, name: str) -> ModuleSpec:
+    @staticmethod
+    async def _find_core_cog(name: str) -> ModuleSpec:
         """
         Attempts to find a spec for a core cog.
 
@@ -310,7 +310,8 @@ def invalidate_caches():
 class CogManagerUI:
     """Commands to interface with Red's cog manager."""
 
-    async def visible_paths(self, ctx):
+    @staticmethod
+    async def visible_paths(ctx):
         install_path = await ctx.bot.cog_mgr.install_path()
         cog_paths = await ctx.bot.cog_mgr.paths()
         cog_paths = [p for p in cog_paths if p != install_path]
@@ -322,11 +323,15 @@ async def paths(self, ctx: commands.Context):
         """
         Lists current cog paths in order of priority.
         """
-        install_path = await ctx.bot.cog_mgr.install_path()
-        cog_paths = await ctx.bot.cog_mgr.paths()
-        cog_paths = [p for p in cog_paths if p != install_path]
-
-        msg = _("Install Path: {}\n\n").format(install_path)
+        cog_mgr = ctx.bot.cog_mgr
+        install_path = await cog_mgr.install_path()
+        core_path = cog_mgr.CORE_PATH
+        cog_paths = await cog_mgr.paths()
+        cog_paths = [p for p in cog_paths if p not in (install_path, core_path)]
+
+        msg = _("Install Path: {install_path}\nCore Path: {core_path}\n\n").format(
+            install_path=install_path, core_path=core_path
+        )
 
         partial = []
         for i, p in enumerate(cog_paths, start=1):
@@ -428,16 +433,16 @@ async def cogs(self, ctx: commands.Context):
         """
         loaded = set(ctx.bot.extensions.keys())
 
-        all = set(await ctx.bot.cog_mgr.available_modules())
+        all_cogs = set(await ctx.bot.cog_mgr.available_modules())
 
-        unloaded = all - loaded
+        unloaded = all_cogs - loaded
 
         loaded = sorted(list(loaded), key=str.lower)
         unloaded = sorted(list(unloaded), key=str.lower)
 
         if await ctx.embed_requested():
-            loaded = ("**{} loaded:**\n").format(len(loaded)) + ", ".join(loaded)
-            unloaded = ("**{} unloaded:**\n").format(len(unloaded)) + ", ".join(unloaded)
+            loaded = _("**{} loaded:**\n").format(len(loaded)) + ", ".join(loaded)
+            unloaded = _("**{} unloaded:**\n").format(len(unloaded)) + ", ".join(unloaded)
 
             for page in pagify(loaded, delims=[", ", "\n"], page_length=1800):
                 e = discord.Embed(description=page, colour=discord.Colour.dark_green())
@@ -447,9 +452,9 @@ async def cogs(self, ctx: commands.Context):
                 e = discord.Embed(description=page, colour=discord.Colour.dark_red())
                 await ctx.send(embed=e)
         else:
-            loaded_count = "**{} loaded:**\n".format(len(loaded))
+            loaded_count = _("**{} loaded:**\n").format(len(loaded))
             loaded = ", ".join(loaded)
-            unloaded_count = "**{} unloaded:**\n".format(len(unloaded))
+            unloaded_count = _("**{} unloaded:**\n").format(len(unloaded))
             unloaded = ", ".join(unloaded)
             loaded_count_sent = False
             unloaded_count_sent = False


Test patch: