diff --git a/redbot/core/_cog_manager.py b/redbot/core/_cog_manager.py index ad9287c72e9..6457a4bbe37 100644 --- a/redbot/core/_cog_manager.py +++ b/redbot/core/_cog_manager.py @@ -2,9 +2,9 @@ import keyword import pkgutil from importlib import import_module, invalidate_caches -from importlib.machinery import ModuleSpec +from importlib.machinery import FileFinder, ModuleSpec from pathlib import Path -from typing import Union, List, Optional +from typing import Union, List, Optional, Tuple import redbot.cogs from redbot.core.commands import positive_int @@ -288,15 +288,37 @@ async def find_cog(self, name: str) -> Optional[ModuleSpec]: with contextlib.suppress(NoSuchCog): return await self._find_core_cog(name) - async def available_modules(self) -> List[str]: - """Finds the names of all available modules to load.""" - paths = list(map(str, await self.paths())) - - ret = [] + def _iter_cogs(self, paths: List[str]) -> List[Tuple[str, bool]]: + """Find the names of all available cogs to load from given paths.""" for finder, module_name, _ in pkgutil.iter_modules(paths): # reject package names that can't be valid python identifiers if module_name.isidentifier() and not keyword.iskeyword(module_name): - ret.append(module_name) + yield finder, module_name + + def available_core_cogs(self) -> List[str]: + """Find the names of all available core cogs to load.""" + return [module_name for _, module_name in self._iter_cogs([self.CORE_PATH])] + + async def available_cogs(self) -> List[Tuple[str, bool]]: + """ + Find the names of all available cog packages to load. + + Includes info about whether the cog would be loaded from a core path. + + Returns + ------- + List[Tuple[str, bool]] + A list of (str, bool) pairs where the first item is the cog package name + and the second item is a bool indicating whether the cog would be loaded + from a core path. + """ + paths = list(map(str, await self.paths())) + + ret = [] + core_path = str(self.CORE_PATH) + for finder, module_name in self._iter_cogs(paths): + is_from_core_path = isinstance(finder, FileFinder) and finder.path == core_path + ret.append((module_name, is_from_core_path)) return ret @staticmethod @@ -456,12 +478,18 @@ async def cogs(self, ctx: commands.Context): """ loaded = set(ctx.bot.extensions.keys()) - all_cogs = set(await ctx.bot._cog_mgr.available_modules()) + core_cogs = set(ctx.bot._cog_mgr.available_core_cogs()) + all_cogs = set() + overridden_core_cogs = set() + for cog_name, is_from_core_path in await ctx.bot._cog_mgr.available_cogs(): + all_cogs.add(cog_name) + if not is_from_core_path and cog_name in core_cogs: + overridden_core_cogs.add(cog_name) unloaded = all_cogs - loaded - loaded = sorted(list(loaded), key=str.lower) - unloaded = sorted(list(unloaded), key=str.lower) + loaded = sorted(loaded, key=str.lower) + unloaded = sorted(unloaded, key=str.lower) if await ctx.embed_requested(): loaded = _("**{} loaded:**\n").format(len(loaded)) + ", ".join(loaded) diff --git a/redbot/core/_debuginfo.py b/redbot/core/_debuginfo.py index 589d406d1ed..166cd66d7d7 100644 --- a/redbot/core/_debuginfo.py +++ b/redbot/core/_debuginfo.py @@ -165,6 +165,12 @@ async def _get_red_vars_section(self) -> DebugInfoSection: # and calling repr() on prefix strings ensures that the list isn't ambiguous. prefixes = ", ".join(map(repr, await self.bot._config.prefix())) parts.append(f"Global prefix(es): {prefixes}") + core_cogs = set(self.bot._cog_mgr.available_core_cogs()) + overridden_core_cogs = set() + for cog_name, is_from_core_path in await self.bot._cog_mgr.available_cogs(): + if not is_from_core_path and cog_name in core_cogs: + overridden_core_cogs.add(cog_name) + parts.append(f"Overridden core cogs: {', '.join(overridden_core_cogs) or 'None'}") if self.is_logged_in: owners = []