From 815f2b7f97cf86196b903d65eb18e52e21fd1a60 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 1 Oct 2019 23:27:22 -0700 Subject: [PATCH 01/19] Rename the "cogs" extension & cog to "extensions" --- bot/__main__.py | 2 +- bot/cogs/{cogs.py => extensions.py} | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) rename bot/cogs/{cogs.py => extensions.py} (93%) diff --git a/bot/__main__.py b/bot/__main__.py index f256937349..347d2ea71e 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -43,7 +43,7 @@ bot.load_extension("bot.cogs.antispam") bot.load_extension("bot.cogs.bot") bot.load_extension("bot.cogs.clean") -bot.load_extension("bot.cogs.cogs") +bot.load_extension("bot.cogs.extensions") bot.load_extension("bot.cogs.help") # Only load this in production diff --git a/bot/cogs/cogs.py b/bot/cogs/extensions.py similarity index 93% rename from bot/cogs/cogs.py rename to bot/cogs/extensions.py index 1f6ccd09ca..612a5aad2b 100644 --- a/bot/cogs/cogs.py +++ b/bot/cogs/extensions.py @@ -12,11 +12,11 @@ log = logging.getLogger(__name__) -KEEP_LOADED = ["bot.cogs.cogs", "bot.cogs.modlog"] +KEEP_LOADED = ["bot.cogs.extensions", "bot.cogs.modlog"] -class Cogs(Cog): - """Cog management commands.""" +class Extensions(Cog): + """Extension management commands.""" def __init__(self, bot: Bot): self.bot = bot @@ -34,13 +34,13 @@ def __init__(self, bot: Bot): # Allow reverse lookups by reversing the pairs self.cogs.update({v: k for k, v in self.cogs.items()}) - @group(name='cogs', aliases=('c',), invoke_without_command=True) + @group(name='extensions', aliases=('c', 'ext', 'exts'), invoke_without_command=True) @with_role(*MODERATION_ROLES, Roles.core_developer) - async def cogs_group(self, ctx: Context) -> None: + async def extensions_group(self, ctx: Context) -> None: """Load, unload, reload, and list active cogs.""" - await ctx.invoke(self.bot.get_command("help"), "cogs") + await ctx.invoke(self.bot.get_command("help"), "extensions") - @cogs_group.command(name='load', aliases=('l',)) + @extensions_group.command(name='load', aliases=('l',)) @with_role(*MODERATION_ROLES, Roles.core_developer) async def load_command(self, ctx: Context, cog: str) -> None: """ @@ -91,7 +91,7 @@ async def load_command(self, ctx: Context, cog: str) -> None: await ctx.send(embed=embed) - @cogs_group.command(name='unload', aliases=('ul',)) + @extensions_group.command(name='unload', aliases=('ul',)) @with_role(*MODERATION_ROLES, Roles.core_developer) async def unload_command(self, ctx: Context, cog: str) -> None: """ @@ -141,7 +141,7 @@ async def unload_command(self, ctx: Context, cog: str) -> None: await ctx.send(embed=embed) - @cogs_group.command(name='reload', aliases=('r',)) + @extensions_group.command(name='reload', aliases=('r',)) @with_role(*MODERATION_ROLES, Roles.core_developer) async def reload_command(self, ctx: Context, cog: str) -> None: """ @@ -245,7 +245,7 @@ async def reload_command(self, ctx: Context, cog: str) -> None: await ctx.send(embed=embed) - @cogs_group.command(name='list', aliases=('all',)) + @extensions_group.command(name='list', aliases=('all',)) @with_role(*MODERATION_ROLES, Roles.core_developer) async def list_command(self, ctx: Context) -> None: """ @@ -293,6 +293,6 @@ async def list_command(self, ctx: Context) -> None: def setup(bot: Bot) -> None: - """Cogs cog load.""" - bot.add_cog(Cogs(bot)) - log.info("Cog loaded: Cogs") + """Load the Extensions cog.""" + bot.add_cog(Extensions(bot)) + log.info("Cog loaded: Extensions") From 4f75160a8a66861eb92a0da97c1bc4ffca86402e Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 13:18:32 -0700 Subject: [PATCH 02/19] Add enum for extension actions --- bot/cogs/extensions.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py index 612a5aad2b..10f4d38e3f 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -1,5 +1,6 @@ import logging import os +from enum import Enum from discord import Colour, Embed from discord.ext.commands import Bot, Cog, Context, group @@ -15,6 +16,14 @@ KEEP_LOADED = ["bot.cogs.extensions", "bot.cogs.modlog"] +class Action(Enum): + """Represents an action to perform on an extension.""" + + LOAD = (Bot.load_extension,) + UNLOAD = (Bot.unload_extension,) + RELOAD = (Bot.unload_extension, Bot.load_extension) + + class Extensions(Cog): """Extension management commands.""" From 6cdda6a6efcb6201d56d036c21a056621533380f Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 14:47:16 -0700 Subject: [PATCH 03/19] Simplify extension discovery using pkgutil The cog now keeps a set of full qualified names of all extensions. --- bot/cogs/extensions.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py index 10f4d38e3f..468c350bbe 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -1,13 +1,12 @@ import logging import os from enum import Enum +from pkgutil import iter_modules from discord import Colour, Embed from discord.ext.commands import Bot, Cog, Context, group -from bot.constants import ( - Emojis, MODERATION_ROLES, Roles, URLs -) +from bot.constants import Emojis, MODERATION_ROLES, Roles, URLs from bot.decorators import with_role from bot.pagination import LinePaginator @@ -29,19 +28,10 @@ class Extensions(Cog): def __init__(self, bot: Bot): self.bot = bot - self.cogs = {} - # Load up the cog names - log.info("Initializing cog names...") - for filename in os.listdir("bot/cogs"): - if filename.endswith(".py") and "_" not in filename: - if os.path.isfile(f"bot/cogs/{filename}"): - cog = filename[:-3] - - self.cogs[cog] = f"bot.cogs.{cog}" - - # Allow reverse lookups by reversing the pairs - self.cogs.update({v: k for k, v in self.cogs.items()}) + log.info("Initialising extension names...") + modules = iter_modules(("bot/cogs", "bot.cogs")) + self.cogs = set(ext for ext in modules if ext.name[-1] != "_") @group(name='extensions', aliases=('c', 'ext', 'exts'), invoke_without_command=True) @with_role(*MODERATION_ROLES, Roles.core_developer) From f7109cc9617c0484b6f7742c58961383ef83ddd6 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 14:51:48 -0700 Subject: [PATCH 04/19] Replace with_role decorator with a cog_check --- bot/cogs/extensions.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py index 468c350bbe..58ab45ca9b 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -7,8 +7,8 @@ from discord.ext.commands import Bot, Cog, Context, group from bot.constants import Emojis, MODERATION_ROLES, Roles, URLs -from bot.decorators import with_role from bot.pagination import LinePaginator +from bot.utils.checks import with_role_check log = logging.getLogger(__name__) @@ -34,13 +34,11 @@ def __init__(self, bot: Bot): self.cogs = set(ext for ext in modules if ext.name[-1] != "_") @group(name='extensions', aliases=('c', 'ext', 'exts'), invoke_without_command=True) - @with_role(*MODERATION_ROLES, Roles.core_developer) async def extensions_group(self, ctx: Context) -> None: """Load, unload, reload, and list active cogs.""" await ctx.invoke(self.bot.get_command("help"), "extensions") @extensions_group.command(name='load', aliases=('l',)) - @with_role(*MODERATION_ROLES, Roles.core_developer) async def load_command(self, ctx: Context, cog: str) -> None: """ Load up an unloaded cog, given the module containing it. @@ -91,7 +89,6 @@ async def load_command(self, ctx: Context, cog: str) -> None: await ctx.send(embed=embed) @extensions_group.command(name='unload', aliases=('ul',)) - @with_role(*MODERATION_ROLES, Roles.core_developer) async def unload_command(self, ctx: Context, cog: str) -> None: """ Unload an already-loaded cog, given the module containing it. @@ -141,7 +138,6 @@ async def unload_command(self, ctx: Context, cog: str) -> None: await ctx.send(embed=embed) @extensions_group.command(name='reload', aliases=('r',)) - @with_role(*MODERATION_ROLES, Roles.core_developer) async def reload_command(self, ctx: Context, cog: str) -> None: """ Reload an unloaded cog, given the module containing it. @@ -245,7 +241,6 @@ async def reload_command(self, ctx: Context, cog: str) -> None: await ctx.send(embed=embed) @extensions_group.command(name='list', aliases=('all',)) - @with_role(*MODERATION_ROLES, Roles.core_developer) async def list_command(self, ctx: Context) -> None: """ Get a list of all cogs, including their loaded status. @@ -290,6 +285,11 @@ async def list_command(self, ctx: Context) -> None: log.debug(f"{ctx.author} requested a list of all cogs. Returning a paginated list.") await LinePaginator.paginate(lines, ctx, embed, max_size=300, empty=False) + # This cannot be static (must have a __func__ attribute). + def cog_check(self, ctx: Context) -> bool: + """Only allow moderators and core developers to invoke the commands in this cog.""" + return with_role_check(ctx, *MODERATION_ROLES, Roles.core_developer) + def setup(bot: Bot) -> None: """Load the Extensions cog.""" From 19ad4392fe50c4c50676fdb509b7208692d48026 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 15:12:44 -0700 Subject: [PATCH 05/19] Add a generic method to manage loading/unloading extensions --- bot/cogs/extensions.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py index 58ab45ca9b..83048bb76b 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -1,5 +1,6 @@ import logging import os +import typing as t from enum import Enum from pkgutil import iter_modules @@ -285,6 +286,36 @@ async def list_command(self, ctx: Context) -> None: log.debug(f"{ctx.author} requested a list of all cogs. Returning a paginated list.") await LinePaginator.paginate(lines, ctx, embed, max_size=300, empty=False) + def manage(self, ext: str, action: Action) -> t.Tuple[str, t.Optional[str]]: + """Apply an action to an extension and return the status message and any error message.""" + verb = action.name.lower() + error_msg = None + + if ext not in self.cogs: + return f":x: Extension {ext} does not exist.", None + + if ( + (action is Action.LOAD and ext not in self.bot.extensions) + or (action is Action.UNLOAD and ext in self.bot.extensions) + or action is Action.RELOAD + ): + try: + for func in action.value: + func(self.bot, ext) + except Exception as e: + log.exception(f"Extension '{ext}' failed to {verb}.") + + error_msg = f"{e.__class__.__name__}: {e}" + msg = f":x: Failed to {verb} extension `{ext}`:\n```{error_msg}```" + else: + msg = f":ok_hand: Extension successfully {verb}ed: `{ext}`." + log.debug(msg[10:]) + else: + msg = f":x: Extension `{ext}` is already {verb}ed." + log.debug(msg[4:]) + + return msg, error_msg + # This cannot be static (must have a __func__ attribute). def cog_check(self, ctx: Context) -> bool: """Only allow moderators and core developers to invoke the commands in this cog.""" From a01a969512b8eb11a337b9c5292bae1d678429a2 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 19:06:27 -0700 Subject: [PATCH 06/19] Add a custom converter for extensions The converter fully qualifies the extension's name and ensures the extension exists. * Make the extensions set a module constant instead of an instant attribute and make it a frozenset. * Add a cog error handler to handle BadArgument locally and prevent the help command from showing for such errors. --- bot/cogs/extensions.py | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py index 83048bb76b..e50ef55530 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -5,7 +5,7 @@ from pkgutil import iter_modules from discord import Colour, Embed -from discord.ext.commands import Bot, Cog, Context, group +from discord.ext.commands import BadArgument, Bot, Cog, Context, Converter, group from bot.constants import Emojis, MODERATION_ROLES, Roles, URLs from bot.pagination import LinePaginator @@ -14,6 +14,7 @@ log = logging.getLogger(__name__) KEEP_LOADED = ["bot.cogs.extensions", "bot.cogs.modlog"] +EXTENSIONS = frozenset(ext for ext in iter_modules(("bot/cogs", "bot.cogs")) if ext.name[-1] != "_") class Action(Enum): @@ -24,16 +25,36 @@ class Action(Enum): RELOAD = (Bot.unload_extension, Bot.load_extension) +class Extension(Converter): + """ + Fully qualify the name of an extension and ensure it exists. + + The * and ** values bypass this when used with the reload command. + """ + + async def convert(self, ctx: Context, argument: str) -> str: + """Fully qualify the name of an extension and ensure it exists.""" + # Special values to reload all extensions + if ctx.command.name == "reload" and (argument == "*" or argument == "**"): + return argument + + argument = argument.lower() + + if "." not in argument: + argument = f"bot.cogs.{argument}" + + if argument in EXTENSIONS: + return argument + else: + raise BadArgument(f":x: Could not find the extension `{argument}`.") + + class Extensions(Cog): """Extension management commands.""" def __init__(self, bot: Bot): self.bot = bot - log.info("Initialising extension names...") - modules = iter_modules(("bot/cogs", "bot.cogs")) - self.cogs = set(ext for ext in modules if ext.name[-1] != "_") - @group(name='extensions', aliases=('c', 'ext', 'exts'), invoke_without_command=True) async def extensions_group(self, ctx: Context) -> None: """Load, unload, reload, and list active cogs.""" @@ -291,9 +312,6 @@ def manage(self, ext: str, action: Action) -> t.Tuple[str, t.Optional[str]]: verb = action.name.lower() error_msg = None - if ext not in self.cogs: - return f":x: Extension {ext} does not exist.", None - if ( (action is Action.LOAD and ext not in self.bot.extensions) or (action is Action.UNLOAD and ext in self.bot.extensions) @@ -321,6 +339,13 @@ def cog_check(self, ctx: Context) -> bool: """Only allow moderators and core developers to invoke the commands in this cog.""" return with_role_check(ctx, *MODERATION_ROLES, Roles.core_developer) + # This cannot be static (must have a __func__ attribute). + async def cog_command_error(self, ctx: Context, error: Exception) -> None: + """Handle BadArgument errors locally to prevent the help command from showing.""" + if isinstance(error, BadArgument): + await ctx.send(str(error)) + error.handled = True + def setup(bot: Bot) -> None: """Load the Extensions cog.""" From 4342f978f4b526a8c6850ccce7f3a3e33a04b1c3 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 19:18:24 -0700 Subject: [PATCH 07/19] Fix the values in the extensions set * Store just the names rather than entire ModuleInfo objects * Fix prefix argument --- bot/cogs/extensions.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py index e50ef55530..c3d6fae273 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -14,7 +14,11 @@ log = logging.getLogger(__name__) KEEP_LOADED = ["bot.cogs.extensions", "bot.cogs.modlog"] -EXTENSIONS = frozenset(ext for ext in iter_modules(("bot/cogs", "bot.cogs")) if ext.name[-1] != "_") +EXTENSIONS = frozenset( + ext.name + for ext in iter_modules(("bot/cogs",), "bot.cogs.") + if ext.name[-1] != "_" +) class Action(Enum): From 0c0fd629192170988ab6bce81144a453e91f7a1d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 20:14:14 -0700 Subject: [PATCH 08/19] Use manage method for extensions commands * Rewrite docstrings for commands * Rename KEEP_LOADED to UNLOAD_BLACKLIST and make it a set * Change single quotes to double quotes * Add "cogs" as an alias to the extensions group --- bot/cogs/extensions.py | 267 +++++++++++------------------------------ 1 file changed, 69 insertions(+), 198 deletions(-) diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py index c3d6fae273..e24e95e6da 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -1,5 +1,5 @@ import logging -import os +import textwrap import typing as t from enum import Enum from pkgutil import iter_modules @@ -13,7 +13,7 @@ log = logging.getLogger(__name__) -KEEP_LOADED = ["bot.cogs.extensions", "bot.cogs.modlog"] +UNLOAD_BLACKLIST = {"bot.cogs.extensions", "bot.cogs.modlog"} EXTENSIONS = frozenset( ext.name for ext in iter_modules(("bot/cogs",), "bot.cogs.") @@ -59,214 +59,45 @@ class Extensions(Cog): def __init__(self, bot: Bot): self.bot = bot - @group(name='extensions', aliases=('c', 'ext', 'exts'), invoke_without_command=True) + @group(name="extensions", aliases=("ext", "exts", "c", "cogs"), invoke_without_command=True) async def extensions_group(self, ctx: Context) -> None: - """Load, unload, reload, and list active cogs.""" + """Load, unload, reload, and list loaded extensions.""" await ctx.invoke(self.bot.get_command("help"), "extensions") - @extensions_group.command(name='load', aliases=('l',)) - async def load_command(self, ctx: Context, cog: str) -> None: - """ - Load up an unloaded cog, given the module containing it. - - You can specify the cog name for any cogs that are placed directly within `!cogs`, or specify the - entire module directly. - """ - cog = cog.lower() - - embed = Embed() - embed.colour = Colour.red() - - embed.set_author( - name="Python Bot (Cogs)", - url=URLs.github_bot_repo, - icon_url=URLs.bot_avatar - ) - - if cog in self.cogs: - full_cog = self.cogs[cog] - elif "." in cog: - full_cog = cog + @extensions_group.command(name="load", aliases=("l",)) + async def load_command(self, ctx: Context, extension: Extension) -> None: + """Load an extension given its fully qualified or unqualified name.""" + msg, _ = self.manage(extension, Action.LOAD) + await ctx.send(msg) + + @extensions_group.command(name="unload", aliases=("ul",)) + async def unload_command(self, ctx: Context, extension: Extension) -> None: + """Unload a currently loaded extension given its fully qualified or unqualified name.""" + if extension in UNLOAD_BLACKLIST: + msg = f":x: The extension `{extension}` may not be unloaded." else: - full_cog = None - log.warning(f"{ctx.author} requested we load the '{cog}' cog, but that cog doesn't exist.") - embed.description = f"Unknown cog: {cog}" - - if full_cog: - if full_cog not in self.bot.extensions: - try: - self.bot.load_extension(full_cog) - except ImportError: - log.exception(f"{ctx.author} requested we load the '{cog}' cog, " - f"but the cog module {full_cog} could not be found!") - embed.description = f"Invalid cog: {cog}\n\nCould not find cog module {full_cog}" - except Exception as e: - log.exception(f"{ctx.author} requested we load the '{cog}' cog, " - "but the loading failed") - embed.description = f"Failed to load cog: {cog}\n\n{e.__class__.__name__}: {e}" - else: - log.debug(f"{ctx.author} requested we load the '{cog}' cog. Cog loaded!") - embed.description = f"Cog loaded: {cog}" - embed.colour = Colour.green() - else: - log.warning(f"{ctx.author} requested we load the '{cog}' cog, but the cog was already loaded!") - embed.description = f"Cog {cog} is already loaded" + msg, _ = self.manage(extension, Action.UNLOAD) - await ctx.send(embed=embed) + await ctx.send(msg) - @extensions_group.command(name='unload', aliases=('ul',)) - async def unload_command(self, ctx: Context, cog: str) -> None: + @extensions_group.command(name="reload", aliases=("r",)) + async def reload_command(self, ctx: Context, extension: Extension) -> None: """ - Unload an already-loaded cog, given the module containing it. + Reload an extension given its fully qualified or unqualified name. - You can specify the cog name for any cogs that are placed directly within `!cogs`, or specify the - entire module directly. + If `*` is given as the name, all currently loaded extensions will be reloaded. + If `**` is given as the name, all extensions, including unloaded ones, will be reloaded. """ - cog = cog.lower() - - embed = Embed() - embed.colour = Colour.red() - - embed.set_author( - name="Python Bot (Cogs)", - url=URLs.github_bot_repo, - icon_url=URLs.bot_avatar - ) - - if cog in self.cogs: - full_cog = self.cogs[cog] - elif "." in cog: - full_cog = cog + if extension == "*": + msg = await self.reload_all() + elif extension == "**": + msg = await self.reload_all(True) else: - full_cog = None - log.warning(f"{ctx.author} requested we unload the '{cog}' cog, but that cog doesn't exist.") - embed.description = f"Unknown cog: {cog}" - - if full_cog: - if full_cog in KEEP_LOADED: - log.warning(f"{ctx.author} requested we unload `{full_cog}`, that sneaky pete. We said no.") - embed.description = f"You may not unload `{full_cog}`!" - elif full_cog in self.bot.extensions: - try: - self.bot.unload_extension(full_cog) - except Exception as e: - log.exception(f"{ctx.author} requested we unload the '{cog}' cog, " - "but the unloading failed") - embed.description = f"Failed to unload cog: {cog}\n\n```{e}```" - else: - log.debug(f"{ctx.author} requested we unload the '{cog}' cog. Cog unloaded!") - embed.description = f"Cog unloaded: {cog}" - embed.colour = Colour.green() - else: - log.warning(f"{ctx.author} requested we unload the '{cog}' cog, but the cog wasn't loaded!") - embed.description = f"Cog {cog} is not loaded" - - await ctx.send(embed=embed) + msg, _ = self.manage(extension, Action.RELOAD) - @extensions_group.command(name='reload', aliases=('r',)) - async def reload_command(self, ctx: Context, cog: str) -> None: - """ - Reload an unloaded cog, given the module containing it. + await ctx.send(msg) - You can specify the cog name for any cogs that are placed directly within `!cogs`, or specify the - entire module directly. - - If you specify "*" as the cog, every cog currently loaded will be unloaded, and then every cog present in the - bot/cogs directory will be loaded. - """ - cog = cog.lower() - - embed = Embed() - embed.colour = Colour.red() - - embed.set_author( - name="Python Bot (Cogs)", - url=URLs.github_bot_repo, - icon_url=URLs.bot_avatar - ) - - if cog == "*": - full_cog = cog - elif cog in self.cogs: - full_cog = self.cogs[cog] - elif "." in cog: - full_cog = cog - else: - full_cog = None - log.warning(f"{ctx.author} requested we reload the '{cog}' cog, but that cog doesn't exist.") - embed.description = f"Unknown cog: {cog}" - - if full_cog: - if full_cog == "*": - all_cogs = [ - f"bot.cogs.{fn[:-3]}" for fn in os.listdir("bot/cogs") - if os.path.isfile(f"bot/cogs/{fn}") and fn.endswith(".py") and "_" not in fn - ] - - failed_unloads = {} - failed_loads = {} - - unloaded = 0 - loaded = 0 - - for loaded_cog in self.bot.extensions.copy().keys(): - try: - self.bot.unload_extension(loaded_cog) - except Exception as e: - failed_unloads[loaded_cog] = f"{e.__class__.__name__}: {e}" - else: - unloaded += 1 - - for unloaded_cog in all_cogs: - try: - self.bot.load_extension(unloaded_cog) - except Exception as e: - failed_loads[unloaded_cog] = f"{e.__class__.__name__}: {e}" - else: - loaded += 1 - - lines = [ - "**All cogs reloaded**", - f"**Unloaded**: {unloaded} / **Loaded**: {loaded}" - ] - - if failed_unloads: - lines.append("\n**Unload failures**") - - for cog, error in failed_unloads: - lines.append(f"{Emojis.status_dnd} **{cog}:** `{error}`") - - if failed_loads: - lines.append("\n**Load failures**") - - for cog, error in failed_loads.items(): - lines.append(f"{Emojis.status_dnd} **{cog}:** `{error}`") - - log.debug(f"{ctx.author} requested we reload all cogs. Here are the results: \n" - f"{lines}") - - await LinePaginator.paginate(lines, ctx, embed, empty=False) - return - - elif full_cog in self.bot.extensions: - try: - self.bot.unload_extension(full_cog) - self.bot.load_extension(full_cog) - except Exception as e: - log.exception(f"{ctx.author} requested we reload the '{cog}' cog, " - "but the unloading failed") - embed.description = f"Failed to reload cog: {cog}\n\n```{e}```" - else: - log.debug(f"{ctx.author} requested we reload the '{cog}' cog. Cog reloaded!") - embed.description = f"Cog reload: {cog}" - embed.colour = Colour.green() - else: - log.warning(f"{ctx.author} requested we reload the '{cog}' cog, but the cog wasn't loaded!") - embed.description = f"Cog {cog} is not loaded" - - await ctx.send(embed=embed) - - @extensions_group.command(name='list', aliases=('all',)) + @extensions_group.command(name="list", aliases=("all",)) async def list_command(self, ctx: Context) -> None: """ Get a list of all cogs, including their loaded status. @@ -311,6 +142,46 @@ async def list_command(self, ctx: Context) -> None: log.debug(f"{ctx.author} requested a list of all cogs. Returning a paginated list.") await LinePaginator.paginate(lines, ctx, embed, max_size=300, empty=False) + async def reload_all(self, reload_unloaded: bool = False) -> str: + """Reload all loaded (and optionally unloaded) extensions and return an output message.""" + unloaded = [] + unload_failures = {} + load_failures = {} + + to_unload = self.bot.extensions.copy().keys() + for extension in to_unload: + _, error = self.manage(extension, Action.UNLOAD) + if error: + unload_failures[extension] = error + else: + unloaded.append(extension) + + if reload_unloaded: + unloaded = EXTENSIONS + + for extension in unloaded: + _, error = self.manage(extension, Action.LOAD) + if error: + load_failures[extension] = error + + msg = textwrap.dedent(f""" + **All extensions reloaded** + Unloaded: {len(to_unload) - len(unload_failures)} / {len(to_unload)} + Loaded: {len(unloaded) - len(load_failures)} / {len(unloaded)} + """).strip() + + if unload_failures: + failures = '\n'.join(f'{ext}\n {err}' for ext, err in unload_failures) + msg += f'\nUnload failures:```{failures}```' + + if load_failures: + failures = '\n'.join(f'{ext}\n {err}' for ext, err in load_failures) + msg += f'\nLoad failures:```{failures}```' + + log.debug(f'Reloaded all extensions.') + + return msg + def manage(self, ext: str, action: Action) -> t.Tuple[str, t.Optional[str]]: """Apply an action to an extension and return the status message and any error message.""" verb = action.name.lower() From c05d0dbf01f7357ee20a8b7dcc7ca07939ea28c4 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 20:19:27 -0700 Subject: [PATCH 09/19] Show original exception, if available, when an extension fails to load --- bot/cogs/extensions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py index e24e95e6da..0e223b2a3b 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -196,6 +196,9 @@ def manage(self, ext: str, action: Action) -> t.Tuple[str, t.Optional[str]]: for func in action.value: func(self.bot, ext) except Exception as e: + if hasattr(e, "original"): + e = e.original + log.exception(f"Extension '{ext}' failed to {verb}.") error_msg = f"{e.__class__.__name__}: {e}" From 22c9aaa30c907ceda5e436fa532d8889db73afbc Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 20:20:42 -0700 Subject: [PATCH 10/19] Fix concatenation of error messages for extension reloads --- bot/cogs/extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py index 0e223b2a3b..53952b1a75 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -171,11 +171,11 @@ async def reload_all(self, reload_unloaded: bool = False) -> str: """).strip() if unload_failures: - failures = '\n'.join(f'{ext}\n {err}' for ext, err in unload_failures) + failures = '\n'.join(f'{ext}\n {err}' for ext, err in unload_failures.items()) msg += f'\nUnload failures:```{failures}```' if load_failures: - failures = '\n'.join(f'{ext}\n {err}' for ext, err in load_failures) + failures = '\n'.join(f'{ext}\n {err}' for ext, err in load_failures.items()) msg += f'\nLoad failures:```{failures}```' log.debug(f'Reloaded all extensions.') From 37040baf0f3c3cf9c7e4668a6c4a2b3736031dab Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 20:35:30 -0700 Subject: [PATCH 11/19] Support giving multiple extensions to reload * Rename reload_all to batch_reload --- bot/cogs/extensions.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py index 53952b1a75..5e0bd29bf1 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -81,19 +81,19 @@ async def unload_command(self, ctx: Context, extension: Extension) -> None: await ctx.send(msg) @extensions_group.command(name="reload", aliases=("r",)) - async def reload_command(self, ctx: Context, extension: Extension) -> None: + async def reload_command(self, ctx: Context, *extensions: Extension) -> None: """ - Reload an extension given its fully qualified or unqualified name. + Reload extensions given their fully qualified or unqualified names. If `*` is given as the name, all currently loaded extensions will be reloaded. If `**` is given as the name, all extensions, including unloaded ones, will be reloaded. """ - if extension == "*": - msg = await self.reload_all() - elif extension == "**": - msg = await self.reload_all(True) + if "**" in extensions: + msg = await self.batch_reload(reload_unloaded=True) + elif "*" in extensions or len(extensions) > 1: + msg = await self.batch_reload(*extensions) else: - msg, _ = self.manage(extension, Action.RELOAD) + msg, _ = self.manage(extensions[0], Action.RELOAD) await ctx.send(msg) @@ -142,13 +142,20 @@ async def list_command(self, ctx: Context) -> None: log.debug(f"{ctx.author} requested a list of all cogs. Returning a paginated list.") await LinePaginator.paginate(lines, ctx, embed, max_size=300, empty=False) - async def reload_all(self, reload_unloaded: bool = False) -> str: - """Reload all loaded (and optionally unloaded) extensions and return an output message.""" + async def batch_reload(self, *extensions: str, reload_unloaded: bool = False) -> str: + """Reload given extensions or all loaded ones and return a message with the results.""" unloaded = [] unload_failures = {} load_failures = {} - to_unload = self.bot.extensions.copy().keys() + if "*" in extensions: + to_unload = set(self.bot.extensions.keys()) | set(extensions) + to_unload.remove("*") + elif extensions: + to_unload = extensions + else: + to_unload = self.bot.extensions.copy().keys() + for extension in to_unload: _, error = self.manage(extension, Action.UNLOAD) if error: From 1fda5f7e1d7fc3bd7002bf047cd975dae5eb1c25 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 21:37:03 -0700 Subject: [PATCH 12/19] Use reload_extension() instead of calling unload and reload * Simplify output format of batch reload with only 1 list of failures * Show success/failure emoji for batch reloads * Simplify logic in the manage() function * Clean up some imports --- bot/cogs/extensions.py | 123 +++++++++++++++++++---------------------- 1 file changed, 56 insertions(+), 67 deletions(-) diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py index 5e0bd29bf1..0d2cc726ea 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -1,11 +1,12 @@ +import functools import logging -import textwrap import typing as t from enum import Enum from pkgutil import iter_modules from discord import Colour, Embed -from discord.ext.commands import BadArgument, Bot, Cog, Context, Converter, group +from discord.ext import commands +from discord.ext.commands import Bot, Context, group from bot.constants import Emojis, MODERATION_ROLES, Roles, URLs from bot.pagination import LinePaginator @@ -24,12 +25,13 @@ class Action(Enum): """Represents an action to perform on an extension.""" - LOAD = (Bot.load_extension,) - UNLOAD = (Bot.unload_extension,) - RELOAD = (Bot.unload_extension, Bot.load_extension) + # Need to be partial otherwise they are considered to be function definitions. + LOAD = functools.partial(Bot.load_extension) + UNLOAD = functools.partial(Bot.unload_extension) + RELOAD = functools.partial(Bot.reload_extension) -class Extension(Converter): +class Extension(commands.Converter): """ Fully qualify the name of an extension and ensure it exists. @@ -50,10 +52,10 @@ async def convert(self, ctx: Context, argument: str) -> str: if argument in EXTENSIONS: return argument else: - raise BadArgument(f":x: Could not find the extension `{argument}`.") + raise commands.BadArgument(f":x: Could not find the extension `{argument}`.") -class Extensions(Cog): +class Extensions(commands.Cog): """Extension management commands.""" def __init__(self, bot: Bot): @@ -85,12 +87,12 @@ async def reload_command(self, ctx: Context, *extensions: Extension) -> None: """ Reload extensions given their fully qualified or unqualified names. + If an extension fails to be reloaded, it will be rolled-back to the prior working state. + If `*` is given as the name, all currently loaded extensions will be reloaded. If `**` is given as the name, all extensions, including unloaded ones, will be reloaded. """ - if "**" in extensions: - msg = await self.batch_reload(reload_unloaded=True) - elif "*" in extensions or len(extensions) > 1: + if len(extensions) > 1: msg = await self.batch_reload(*extensions) else: msg, _ = self.manage(extensions[0], Action.RELOAD) @@ -142,48 +144,37 @@ async def list_command(self, ctx: Context) -> None: log.debug(f"{ctx.author} requested a list of all cogs. Returning a paginated list.") await LinePaginator.paginate(lines, ctx, embed, max_size=300, empty=False) - async def batch_reload(self, *extensions: str, reload_unloaded: bool = False) -> str: - """Reload given extensions or all loaded ones and return a message with the results.""" - unloaded = [] - unload_failures = {} - load_failures = {} + async def batch_reload(self, *extensions: str) -> str: + """ + Reload given extensions and return a message with the results. + + If `*` is given, all currently loaded extensions will be reloaded along with any other + specified extensions. If `**` is given, all extensions, including unloaded ones, will be + reloaded. + """ + failures = {} - if "*" in extensions: - to_unload = set(self.bot.extensions.keys()) | set(extensions) - to_unload.remove("*") + if "**" in extensions: + to_reload = EXTENSIONS + elif "*" in extensions: + to_reload = set(self.bot.extensions.keys()) | set(extensions) + to_reload.remove("*") elif extensions: - to_unload = extensions + to_reload = extensions else: - to_unload = self.bot.extensions.copy().keys() + to_reload = self.bot.extensions.copy().keys() - for extension in to_unload: - _, error = self.manage(extension, Action.UNLOAD) + for extension in to_reload: + _, error = self.manage(extension, Action.RELOAD) if error: - unload_failures[extension] = error - else: - unloaded.append(extension) + failures[extension] = error - if reload_unloaded: - unloaded = EXTENSIONS - - for extension in unloaded: - _, error = self.manage(extension, Action.LOAD) - if error: - load_failures[extension] = error + emoji = ":x:" if failures else ":ok_hand:" + msg = f"{emoji} {len(to_reload) - len(failures)} / {len(to_reload)} extensions reloaded." - msg = textwrap.dedent(f""" - **All extensions reloaded** - Unloaded: {len(to_unload) - len(unload_failures)} / {len(to_unload)} - Loaded: {len(unloaded) - len(load_failures)} / {len(unloaded)} - """).strip() - - if unload_failures: - failures = '\n'.join(f'{ext}\n {err}' for ext, err in unload_failures.items()) - msg += f'\nUnload failures:```{failures}```' - - if load_failures: - failures = '\n'.join(f'{ext}\n {err}' for ext, err in load_failures.items()) - msg += f'\nLoad failures:```{failures}```' + if failures: + failures = '\n'.join(f'{ext}\n {err}' for ext, err in failures.items()) + msg += f'\nFailures:```{failures}```' log.debug(f'Reloaded all extensions.') @@ -194,28 +185,26 @@ def manage(self, ext: str, action: Action) -> t.Tuple[str, t.Optional[str]]: verb = action.name.lower() error_msg = None - if ( - (action is Action.LOAD and ext not in self.bot.extensions) - or (action is Action.UNLOAD and ext in self.bot.extensions) - or action is Action.RELOAD - ): - try: - for func in action.value: - func(self.bot, ext) - except Exception as e: - if hasattr(e, "original"): - e = e.original - - log.exception(f"Extension '{ext}' failed to {verb}.") - - error_msg = f"{e.__class__.__name__}: {e}" - msg = f":x: Failed to {verb} extension `{ext}`:\n```{error_msg}```" - else: - msg = f":ok_hand: Extension successfully {verb}ed: `{ext}`." - log.debug(msg[10:]) - else: + try: + action.value(self.bot, ext) + except (commands.ExtensionAlreadyLoaded, commands.ExtensionNotLoaded): + if action is Action.RELOAD: + # When reloading, just load the extension if it was not loaded. + return self.manage(ext, Action.LOAD) + msg = f":x: Extension `{ext}` is already {verb}ed." log.debug(msg[4:]) + except Exception as e: + if hasattr(e, "original"): + e = e.original + + log.exception(f"Extension '{ext}' failed to {verb}.") + + error_msg = f"{e.__class__.__name__}: {e}" + msg = f":x: Failed to {verb} extension `{ext}`:\n```{error_msg}```" + else: + msg = f":ok_hand: Extension successfully {verb}ed: `{ext}`." + log.debug(msg[10:]) return msg, error_msg @@ -227,7 +216,7 @@ def cog_check(self, ctx: Context) -> bool: # This cannot be static (must have a __func__ attribute). async def cog_command_error(self, ctx: Context, error: Exception) -> None: """Handle BadArgument errors locally to prevent the help command from showing.""" - if isinstance(error, BadArgument): + if isinstance(error, commands.BadArgument): await ctx.send(str(error)) error.handled = True From 0f63028bfc1fea19209342cdd1acbbf57d586e18 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 21:44:18 -0700 Subject: [PATCH 13/19] Fix extensions alias * Rename accordingly from cogs to extensions * Use the Extension converter * Make the argument variable instead of keyword-only --- bot/cogs/alias.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/cogs/alias.py b/bot/cogs/alias.py index 0f49a400c4..6648805e94 100644 --- a/bot/cogs/alias.py +++ b/bot/cogs/alias.py @@ -5,6 +5,7 @@ from discord import Colour, Embed, Member, User from discord.ext.commands import Bot, Cog, Command, Context, clean_content, command, group +from bot.cogs.extensions import Extension from bot.cogs.watchchannels.watchchannel import proxy_user from bot.converters import TagNameConverter from bot.pagination import LinePaginator @@ -84,9 +85,9 @@ async def site_rules_alias(self, ctx: Context) -> None: await self.invoke(ctx, "site rules") @command(name="reload", hidden=True) - async def cogs_reload_alias(self, ctx: Context, *, cog_name: str) -> None: - """Alias for invoking cogs reload [cog_name].""" - await self.invoke(ctx, "cogs reload", cog_name) + async def extensions_reload_alias(self, ctx: Context, *extensions: Extension) -> None: + """Alias for invoking extensions reload [extensions...].""" + await self.invoke(ctx, "extensions reload", *extensions) @command(name="defon", hidden=True) async def defcon_enable_alias(self, ctx: Context) -> None: From 82fb11c0e08f6913ce5273a49b269a80c5dd2be4 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 22:15:21 -0700 Subject: [PATCH 14/19] Invoke the help command when reload is called without args --- bot/cogs/extensions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py index 0d2cc726ea..f848b8a52b 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -92,6 +92,10 @@ async def reload_command(self, ctx: Context, *extensions: Extension) -> None: If `*` is given as the name, all currently loaded extensions will be reloaded. If `**` is given as the name, all extensions, including unloaded ones, will be reloaded. """ + if not extensions: + await ctx.invoke(self.bot.get_command("help"), "extensions reload") + return + if len(extensions) > 1: msg = await self.batch_reload(*extensions) else: From cbccb1e594295bb24983641ae32717f2f002a09b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 22:31:46 -0700 Subject: [PATCH 15/19] Refactor the extensions list command --- bot/cogs/extensions.py | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py index f848b8a52b..3cbaa810a1 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -106,44 +106,29 @@ async def reload_command(self, ctx: Context, *extensions: Extension) -> None: @extensions_group.command(name="list", aliases=("all",)) async def list_command(self, ctx: Context) -> None: """ - Get a list of all cogs, including their loaded status. + Get a list of all extensions, including their loaded status. - Gray indicates that the cog is unloaded. Green indicates that the cog is currently loaded. + Grey indicates that the extension is unloaded. + Green indicates that the extension is currently loaded. """ embed = Embed() lines = [] - cogs = {} embed.colour = Colour.blurple() embed.set_author( - name="Python Bot (Cogs)", + name="Extensions List", url=URLs.github_bot_repo, icon_url=URLs.bot_avatar ) - for key, _value in self.cogs.items(): - if "." not in key: - continue - - if key in self.bot.extensions: - cogs[key] = True - else: - cogs[key] = False - - for key in self.bot.extensions.keys(): - if key not in self.cogs: - cogs[key] = True - - for cog, loaded in sorted(cogs.items(), key=lambda x: x[0]): - if cog in self.cogs: - cog = self.cogs[cog] - - if loaded: + for ext in sorted(list(EXTENSIONS)): + if ext in self.bot.extensions: status = Emojis.status_online else: status = Emojis.status_offline - lines.append(f"{status} {cog}") + ext = ext.rsplit(".", 1)[1] + lines.append(f"{status} {ext}") log.debug(f"{ctx.author} requested a list of all cogs. Returning a paginated list.") await LinePaginator.paginate(lines, ctx, embed, max_size=300, empty=False) From 2ece22e0c8b58290e7d90d71849d01272d138fe8 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 7 Oct 2019 09:35:48 -0700 Subject: [PATCH 16/19] Use quotes instead of back ticks around asterisk in docstrings --- bot/cogs/extensions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py index 3cbaa810a1..a385e50d5e 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -89,8 +89,8 @@ async def reload_command(self, ctx: Context, *extensions: Extension) -> None: If an extension fails to be reloaded, it will be rolled-back to the prior working state. - If `*` is given as the name, all currently loaded extensions will be reloaded. - If `**` is given as the name, all extensions, including unloaded ones, will be reloaded. + If '*' is given as the name, all currently loaded extensions will be reloaded. + If '**' is given as the name, all extensions, including unloaded ones, will be reloaded. """ if not extensions: await ctx.invoke(self.bot.get_command("help"), "extensions reload") @@ -137,8 +137,8 @@ async def batch_reload(self, *extensions: str) -> str: """ Reload given extensions and return a message with the results. - If `*` is given, all currently loaded extensions will be reloaded along with any other - specified extensions. If `**` is given, all extensions, including unloaded ones, will be + If '*' is given, all currently loaded extensions will be reloaded along with any other + specified extensions. If '**' is given, all extensions, including unloaded ones, will be reloaded. """ failures = {} From 77216353a87bcf2dbf67cfe028f9f38ba7a2406e Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 7 Oct 2019 10:23:13 -0700 Subject: [PATCH 17/19] Support wildcards and multiple extensions for load and unload commands * Rename batch_reload() to batch_manage() and make it accept an action as a parameter so that it can be a generic function. * Switch parameter order for manage() to make it consistent with batch_manage(). * Always call batch_manage() and make it defer to manage() when only 1 extension is given. * Make batch_manage() a regular method instead of a coroutine. --- bot/cogs/extensions.py | 84 ++++++++++++++++++++++++------------------ 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py index a385e50d5e..5f9b4aef4c 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -41,7 +41,7 @@ class Extension(commands.Converter): async def convert(self, ctx: Context, argument: str) -> str: """Fully qualify the name of an extension and ensure it exists.""" # Special values to reload all extensions - if ctx.command.name == "reload" and (argument == "*" or argument == "**"): + if argument == "*" or argument == "**": return argument argument = argument.lower() @@ -67,18 +67,34 @@ async def extensions_group(self, ctx: Context) -> None: await ctx.invoke(self.bot.get_command("help"), "extensions") @extensions_group.command(name="load", aliases=("l",)) - async def load_command(self, ctx: Context, extension: Extension) -> None: - """Load an extension given its fully qualified or unqualified name.""" - msg, _ = self.manage(extension, Action.LOAD) + async def load_command(self, ctx: Context, *extensions: Extension) -> None: + """ + Load extensions given their fully qualified or unqualified names. + + If '*' or '**' is given as the name, all unloaded extensions will be loaded. + """ + if "*" in extensions or "**" in extensions: + extensions = set(EXTENSIONS) - set(self.bot.extensions.keys()) + + msg = self.batch_manage(Action.LOAD, *extensions) await ctx.send(msg) @extensions_group.command(name="unload", aliases=("ul",)) - async def unload_command(self, ctx: Context, extension: Extension) -> None: - """Unload a currently loaded extension given its fully qualified or unqualified name.""" - if extension in UNLOAD_BLACKLIST: - msg = f":x: The extension `{extension}` may not be unloaded." + async def unload_command(self, ctx: Context, *extensions: Extension) -> None: + """ + Unload currently loaded extensions given their fully qualified or unqualified names. + + If '*' or '**' is given as the name, all loaded extensions will be unloaded. + """ + blacklisted = "\n".join(UNLOAD_BLACKLIST & set(extensions)) + + if blacklisted: + msg = f":x: The following extension(s) may not be unloaded:```{blacklisted}```" else: - msg, _ = self.manage(extension, Action.UNLOAD) + if "*" in extensions or "**" in extensions: + extensions = set(self.bot.extensions.keys()) - UNLOAD_BLACKLIST + + msg = self.batch_manage(Action.UNLOAD, *extensions) await ctx.send(msg) @@ -96,10 +112,13 @@ async def reload_command(self, ctx: Context, *extensions: Extension) -> None: await ctx.invoke(self.bot.get_command("help"), "extensions reload") return - if len(extensions) > 1: - msg = await self.batch_reload(*extensions) - else: - msg, _ = self.manage(extensions[0], Action.RELOAD) + if "**" in extensions: + extensions = EXTENSIONS + elif "*" in extensions: + extensions = set(self.bot.extensions.keys()) | set(extensions) + extensions.remove("*") + + msg = self.batch_manage(Action.RELOAD, *extensions) await ctx.send(msg) @@ -133,43 +152,36 @@ async def list_command(self, ctx: Context) -> None: log.debug(f"{ctx.author} requested a list of all cogs. Returning a paginated list.") await LinePaginator.paginate(lines, ctx, embed, max_size=300, empty=False) - async def batch_reload(self, *extensions: str) -> str: + def batch_manage(self, action: Action, *extensions: str) -> str: """ - Reload given extensions and return a message with the results. + Apply an action to multiple extensions and return a message with the results. - If '*' is given, all currently loaded extensions will be reloaded along with any other - specified extensions. If '**' is given, all extensions, including unloaded ones, will be - reloaded. + If only one extension is given, it is deferred to `manage()`. """ - failures = {} + if len(extensions) == 1: + msg, _ = self.manage(action, extensions[0]) + return msg - if "**" in extensions: - to_reload = EXTENSIONS - elif "*" in extensions: - to_reload = set(self.bot.extensions.keys()) | set(extensions) - to_reload.remove("*") - elif extensions: - to_reload = extensions - else: - to_reload = self.bot.extensions.copy().keys() + verb = action.name.lower() + failures = {} - for extension in to_reload: - _, error = self.manage(extension, Action.RELOAD) + for extension in extensions: + _, error = self.manage(action, extension) if error: failures[extension] = error emoji = ":x:" if failures else ":ok_hand:" - msg = f"{emoji} {len(to_reload) - len(failures)} / {len(to_reload)} extensions reloaded." + msg = f"{emoji} {len(extensions) - len(failures)} / {len(extensions)} extensions {verb}ed." if failures: - failures = '\n'.join(f'{ext}\n {err}' for ext, err in failures.items()) - msg += f'\nFailures:```{failures}```' + failures = "\n".join(f"{ext}\n {err}" for ext, err in failures.items()) + msg += f"\nFailures:```{failures}```" - log.debug(f'Reloaded all extensions.') + log.debug(f"Batch {verb}ed extensions.") return msg - def manage(self, ext: str, action: Action) -> t.Tuple[str, t.Optional[str]]: + def manage(self, action: Action, ext: str) -> t.Tuple[str, t.Optional[str]]: """Apply an action to an extension and return the status message and any error message.""" verb = action.name.lower() error_msg = None @@ -179,7 +191,7 @@ def manage(self, ext: str, action: Action) -> t.Tuple[str, t.Optional[str]]: except (commands.ExtensionAlreadyLoaded, commands.ExtensionNotLoaded): if action is Action.RELOAD: # When reloading, just load the extension if it was not loaded. - return self.manage(ext, Action.LOAD) + return self.manage(Action.LOAD, ext) msg = f":x: Extension `{ext}` is already {verb}ed." log.debug(msg[4:]) From da7b23cddc22a27c5b1091bbf25a6ae714b07a8c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 7 Oct 2019 10:30:50 -0700 Subject: [PATCH 18/19] Escape asterisks in extensions docstrings --- bot/cogs/extensions.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py index 5f9b4aef4c..3c59ad8c27 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -71,8 +71,8 @@ async def load_command(self, ctx: Context, *extensions: Extension) -> None: """ Load extensions given their fully qualified or unqualified names. - If '*' or '**' is given as the name, all unloaded extensions will be loaded. - """ + If '\*' or '\*\*' is given as the name, all unloaded extensions will be loaded. + """ # noqa: W605 if "*" in extensions or "**" in extensions: extensions = set(EXTENSIONS) - set(self.bot.extensions.keys()) @@ -84,8 +84,8 @@ async def unload_command(self, ctx: Context, *extensions: Extension) -> None: """ Unload currently loaded extensions given their fully qualified or unqualified names. - If '*' or '**' is given as the name, all loaded extensions will be unloaded. - """ + If '\*' or '\*\*' is given as the name, all loaded extensions will be unloaded. + """ # noqa: W605 blacklisted = "\n".join(UNLOAD_BLACKLIST & set(extensions)) if blacklisted: @@ -105,9 +105,9 @@ async def reload_command(self, ctx: Context, *extensions: Extension) -> None: If an extension fails to be reloaded, it will be rolled-back to the prior working state. - If '*' is given as the name, all currently loaded extensions will be reloaded. - If '**' is given as the name, all extensions, including unloaded ones, will be reloaded. - """ + If '\*' is given as the name, all currently loaded extensions will be reloaded. + If '\*\*' is given as the name, all extensions, including unloaded ones, will be reloaded. + """ # noqa: W605 if not extensions: await ctx.invoke(self.bot.get_command("help"), "extensions reload") return From 319cf13c1946715cff5fbadfdaa301e86849547c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 9 Oct 2019 16:42:48 -0700 Subject: [PATCH 19/19] Show help when ext load/unload are invoked without arguments --- bot/cogs/extensions.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py index 3c59ad8c27..bb66e0b8ea 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -73,6 +73,10 @@ async def load_command(self, ctx: Context, *extensions: Extension) -> None: If '\*' or '\*\*' is given as the name, all unloaded extensions will be loaded. """ # noqa: W605 + if not extensions: + await ctx.invoke(self.bot.get_command("help"), "extensions load") + return + if "*" in extensions or "**" in extensions: extensions = set(EXTENSIONS) - set(self.bot.extensions.keys()) @@ -86,6 +90,10 @@ async def unload_command(self, ctx: Context, *extensions: Extension) -> None: If '\*' or '\*\*' is given as the name, all loaded extensions will be unloaded. """ # noqa: W605 + if not extensions: + await ctx.invoke(self.bot.get_command("help"), "extensions unload") + return + blacklisted = "\n".join(UNLOAD_BLACKLIST & set(extensions)) if blacklisted: