Repository: Cog-Creators/Red-DiscordBot

Issue:
[V3] Cog path operations create duplicate entries
Please be sure to read through other issues as well to make sure what you are suggesting/reporting has not already
been suggested/reported

### Type:

- [ ] Suggestion
- [X] Bug

### Brief description of the problem
!addpath, !removepath, and !reorderpath do not behave as expected, creating duplicate entries.

### Steps to reproduce

1. Start a new Red instance and run `!paths`. This should yield the output in Discord
```
Install Path: /home/$USER/.local/share/Red-DiscordBot/cogs/CogManager/cogs

1. /home/$USER/.local/share/virtualenvs/$BOT_ENV/lib/python3.5/site-packages/redbot/cogs
```

2. `!addpath /tmp/test/path` (after making that path so that Red accepts it as an existing folder)
    * Expected: `!paths` gives the above output, plus `2. /tmp/test/path`
    * Actual: `!paths` gives
```
Install Path: /home/$USER/.local/share/Red-DiscordBot/cogs/CogManager/cogs

1. /tmp/test/path
2. /home/$USER/.local/share/virtualenvs/$BOT_ENV/lib/python3.5/site-packages/redbot/cogs
3. /home/$USER/.local/share/virtualenvs/$BOT_ENV/lib/python3.5/site-packages/redbot/cogs
```

3. `!removepath 3`
    * Expected: `!paths` gives the step 2 output but with the duplicate path removed.
    * Actual: `!paths` gives
```
Install Path: /home/$USER/.local/share/Red-DiscordBot/cogs/CogManager/cogs

1. /home/$USER/.local/share/virtualenvs/$BOT_ENV/lib/python3.5/site-packages/redbot/cogs
2. /tmp/test/path
3. /home/$USER/.local/share/virtualenvs/$BOT_ENV/lib/python3.5/site-packages/redbot/cogs
```


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
@@ -3,7 +3,7 @@
 from importlib import import_module, invalidate_caches
 from importlib.machinery import ModuleSpec
 from pathlib import Path
-from typing import Tuple, Union, List, overload
+from typing import Tuple, Union, List
 
 import redbot.cogs
 import discord
@@ -19,6 +19,14 @@
 __all__ = ["CogManager"]
 
 
+def _deduplicate(xs):
+    ret = []
+    for x in xs:
+        if x not in ret:
+            ret.append(x)
+    return ret
+
+
 class CogManager:
     """Directory manager for Red's cogs.
 
@@ -36,7 +44,7 @@ def __init__(self, paths: Tuple[str]=()):
             install_path=str(tmp_cog_install_path)
         )
 
-        self._paths = list(paths)
+        self._paths = [Path(p) for p in paths]
 
     async def paths(self) -> Tuple[Path, ...]:
         """Get all currently valid path directories.
@@ -47,16 +55,15 @@ async def paths(self) -> Tuple[Path, ...]:
             All valid cog paths.
 
         """
-        conf_paths = await self.conf.paths()
+        conf_paths = [Path(p) for p in await self.conf.paths()]
         other_paths = self._paths
         core_paths = await self.core_paths()
 
-        all_paths = set(list(conf_paths) + list(other_paths) + core_paths)
+        all_paths = _deduplicate(list(conf_paths) + list(other_paths) + core_paths)
 
-        paths = [Path(p) for p in all_paths]
-        if self.install_path not in paths:
-            paths.insert(0, await self.install_path())
-        return tuple(p.resolve() for p in paths if p.is_dir())
+        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__]
@@ -152,7 +159,7 @@ 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.")
 
-        all_paths = set(await self.paths() + (path, ))
+        all_paths = _deduplicate(await self.paths() + (path, ))
         # noinspection PyTypeChecker
         await self.set_paths(all_paths)
 
@@ -208,10 +215,10 @@ async def _find_ext_cog(self, name: str) -> ModuleSpec:
         RuntimeError
             When no matching spec can be found.
         """
-        resolved_paths = set(await self.paths())
-        core_paths = set(await self.core_paths())
+        resolved_paths = _deduplicate(await self.paths())
+        core_paths = _deduplicate(await self.core_paths())
 
-        real_paths = [str(p) for p in (resolved_paths - core_paths)]
+        real_paths = [str(p) for p in resolved_paths if p not in core_paths]
 
         for finder, module_name, _ in pkgutil.iter_modules(real_paths):
             if name == module_name:
@@ -300,6 +307,12 @@ def invalidate_caches():
 
 
 class CogManagerUI:
+    async def visible_paths(self, 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]
+        return cog_paths
+
     @commands.command()
     @checks.is_owner()
     async def paths(self, ctx: commands.Context):
@@ -344,9 +357,10 @@ async def removepath(self, ctx: commands.Context, path_number: int):
         Removes a path from the available cog paths given the path_number
             from !paths
         """
-        cog_paths = await ctx.bot.cog_mgr.paths()
+        path_number -= 1
+        cog_paths = await self.visible_paths(ctx)
         try:
-            to_remove = cog_paths[path_number]
+            to_remove = cog_paths.pop(path_number)
         except IndexError:
             await ctx.send(_("That is an invalid path number."))
             return
@@ -364,7 +378,7 @@ async def reorderpath(self, ctx: commands.Context, from_: int, to: int):
         from_ -= 1
         to -= 1
 
-        all_paths = list(await ctx.bot.cog_mgr.paths())
+        all_paths = await self.visible_paths(ctx)
         try:
             to_move = all_paths.pop(from_)
         except IndexError:


Test patch: