From ee71c2c78050649d4608962398daa5e70ad35e23 Mon Sep 17 00:00:00 2001 From: Jens Date: Thu, 3 Oct 2019 17:36:32 +0200 Subject: Prepare cogs on cog init & wait for bot ready flag --- bot/cogs/antispam.py | 6 ++++-- bot/cogs/defcon.py | 6 ++++-- bot/cogs/doc.py | 6 ++++-- bot/cogs/logging.py | 6 ++++-- bot/cogs/moderation.py | 6 ++++-- bot/cogs/off_topic_names.py | 6 ++++-- bot/cogs/reddit.py | 6 ++++-- bot/cogs/reminders.py | 6 ++++-- bot/cogs/sync/cog.py | 6 ++++-- 9 files changed, 36 insertions(+), 18 deletions(-) diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index 8dfa0ad05..68b3cf91b 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -107,14 +107,16 @@ class AntiSpam(Cog): self.message_deletion_queue = dict() self.queue_consumption_tasks = dict() + bot.loop.create_task(self.prepare_cog()) + @property def mod_log(self) -> ModLog: """Allows for easy access of the ModLog cog.""" return self.bot.get_cog("ModLog") - @Cog.listener() - async def on_ready(self) -> None: + async def prepare_cog(self) -> None: """Unloads the cog and alerts admins if configuration validation failed.""" + await self.bot.wait_until_ready() if self.validation_errors: body = "**The following errors were encountered:**\n" body += "\n".join(f"- {error}" for error in self.validation_errors.values()) diff --git a/bot/cogs/defcon.py b/bot/cogs/defcon.py index 048d8a683..93d84e6b5 100644 --- a/bot/cogs/defcon.py +++ b/bot/cogs/defcon.py @@ -35,14 +35,16 @@ class Defcon(Cog): self.channel = None self.days = timedelta(days=0) + bot.loop.create_task(self.prepare_cog()) + @property def mod_log(self) -> ModLog: """Get currently loaded ModLog cog instance.""" return self.bot.get_cog("ModLog") - @Cog.listener() - async def on_ready(self) -> None: + async def prepare_cog(self) -> None: """On cog load, try to synchronize DEFCON settings to the API.""" + self.bot.wait_until_ready() self.channel = await self.bot.fetch_channel(Channels.defcon) try: response = await self.bot.api_client.get('bot/bot-settings/defcon') diff --git a/bot/cogs/doc.py b/bot/cogs/doc.py index e5c51748f..d503ea4c1 100644 --- a/bot/cogs/doc.py +++ b/bot/cogs/doc.py @@ -126,9 +126,11 @@ class Doc(commands.Cog): self.bot = bot self.inventories = {} - @commands.Cog.listener() - async def on_ready(self) -> None: + bot.loop.create_task(self.prepare_cog()) + + async def prepare_cog(self) -> None: """Refresh documentation inventory.""" + await self.bot.wait_until_ready() await self.refresh_inventory() async def update_single( diff --git a/bot/cogs/logging.py b/bot/cogs/logging.py index 8e47bcc36..25b7d77cc 100644 --- a/bot/cogs/logging.py +++ b/bot/cogs/logging.py @@ -15,9 +15,11 @@ class Logging(Cog): def __init__(self, bot: Bot): self.bot = bot - @Cog.listener() - async def on_ready(self) -> None: + bot.loop.create_task(self.prepare_cog()) + + async def prepare_cog(self) -> None: """Announce our presence to the configured devlog channel.""" + await self.bot.wait_until_ready() log.info("Bot connected!") embed = Embed(description="Connected!") diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index b596f36e6..79502ee1c 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -64,14 +64,16 @@ class Moderation(Scheduler, Cog): self._muted_role = Object(constants.Roles.muted) super().__init__() + bot.loop.create_task(self.prepare_cog()) + @property def mod_log(self) -> ModLog: """Get currently loaded ModLog cog instance.""" return self.bot.get_cog("ModLog") - @Cog.listener() - async def on_ready(self) -> None: + async def prepare_cog(self) -> None: """Schedule expiration for previous infractions.""" + await self.bot.wait_until_ready() # Schedule expiration for previous infractions infractions = await self.bot.api_client.get( 'bot/infractions', params={'active': 'true'} diff --git a/bot/cogs/off_topic_names.py b/bot/cogs/off_topic_names.py index 16717d523..eb966c737 100644 --- a/bot/cogs/off_topic_names.py +++ b/bot/cogs/off_topic_names.py @@ -75,14 +75,16 @@ class OffTopicNames(Cog): self.bot = bot self.updater_task = None + bot.loop.create_task(self.prepare_cog()) + def cog_unload(self) -> None: """Cancel any running updater tasks on cog unload.""" if self.updater_task is not None: self.updater_task.cancel() - @Cog.listener() - async def on_ready(self) -> None: + async def prepare_cog(self) -> None: """Start off-topic channel updating event loop if it hasn't already started.""" + self.bot.wait_until_ready() if self.updater_task is None: coro = update_names(self.bot) self.updater_task = self.bot.loop.create_task(coro) diff --git a/bot/cogs/reddit.py b/bot/cogs/reddit.py index 63a57c5c6..ba926e166 100644 --- a/bot/cogs/reddit.py +++ b/bot/cogs/reddit.py @@ -33,6 +33,8 @@ class Reddit(Cog): self.new_posts_task = None self.top_weekly_posts_task = None + bot.loop.create_task(self.prepare_cog()) + async def fetch_posts(self, route: str, *, amount: int = 25, params: dict = None) -> List[dict]: """A helper method to fetch a certain amount of Reddit posts at a given route.""" # Reddit's JSON responses only provide 25 posts at most. @@ -253,9 +255,9 @@ class Reddit(Cog): max_lines=15 ) - @Cog.listener() - async def on_ready(self) -> None: + async def prepare_cog(self) -> None: """Initiate reddit post event loop.""" + self.bot.wait_until_ready() self.reddit_channel = await self.bot.fetch_channel(Channels.reddit) if self.reddit_channel is not None: diff --git a/bot/cogs/reminders.py b/bot/cogs/reminders.py index 6e91d2c06..dc5536b12 100644 --- a/bot/cogs/reminders.py +++ b/bot/cogs/reminders.py @@ -30,9 +30,11 @@ class Reminders(Scheduler, Cog): self.bot = bot super().__init__() - @Cog.listener() - async def on_ready(self) -> None: + bot.loop.create_task(self.prepare_cog()) + + async def prepare_cog(self) -> None: """Get all current reminders from the API and reschedule them.""" + self.bot.wait_until_ready() response = await self.bot.api_client.get( 'bot/reminders', params={'active': 'true'} diff --git a/bot/cogs/sync/cog.py b/bot/cogs/sync/cog.py index b75fb26cd..15e671ab3 100644 --- a/bot/cogs/sync/cog.py +++ b/bot/cogs/sync/cog.py @@ -29,9 +29,11 @@ class Sync(Cog): def __init__(self, bot: Bot) -> None: self.bot = bot - @Cog.listener() - async def on_ready(self) -> None: + bot.loop.create_task(self.prepare_cog()) + + async def prepare_cog(self) -> None: """Syncs the roles/users of the guild with the database.""" + self.bot.wait_until_ready() guild = self.bot.get_guild(self.SYNC_SERVER_ID) if guild is not None: for syncer in self.ON_READY_SYNCERS: -- cgit v1.2.3 From 815f2b7f97cf86196b903d65eb18e52e21fd1a60 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 1 Oct 2019 23:27:22 -0700 Subject: Rename the "cogs" extension & cog to "extensions" --- bot/__main__.py | 2 +- bot/cogs/cogs.py | 298 ------------------------------------------------- bot/cogs/extensions.py | 298 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 299 insertions(+), 299 deletions(-) delete mode 100644 bot/cogs/cogs.py create mode 100644 bot/cogs/extensions.py diff --git a/bot/__main__.py b/bot/__main__.py index f25693734..347d2ea71 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -43,7 +43,7 @@ bot.load_extension("bot.cogs.security") 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/cogs.py deleted file mode 100644 index 1f6ccd09c..000000000 --- a/bot/cogs/cogs.py +++ /dev/null @@ -1,298 +0,0 @@ -import logging -import os - -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.decorators import with_role -from bot.pagination import LinePaginator - -log = logging.getLogger(__name__) - -KEEP_LOADED = ["bot.cogs.cogs", "bot.cogs.modlog"] - - -class Cogs(Cog): - """Cog management commands.""" - - 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()}) - - @group(name='cogs', aliases=('c',), invoke_without_command=True) - @with_role(*MODERATION_ROLES, Roles.core_developer) - async def cogs_group(self, ctx: Context) -> None: - """Load, unload, reload, and list active cogs.""" - await ctx.invoke(self.bot.get_command("help"), "cogs") - - @cogs_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. - - 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 - 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" - - await ctx.send(embed=embed) - - @cogs_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. - - 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 - 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) - - @cogs_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. - - 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) - - @cogs_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. - - Gray indicates that the cog is unloaded. Green indicates that the cog is currently loaded. - """ - embed = Embed() - lines = [] - cogs = {} - - embed.colour = Colour.blurple() - embed.set_author( - name="Python Bot (Cogs)", - 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: - status = Emojis.status_online - else: - status = Emojis.status_offline - - lines.append(f"{status} {cog}") - - 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 setup(bot: Bot) -> None: - """Cogs cog load.""" - bot.add_cog(Cogs(bot)) - log.info("Cog loaded: Cogs") diff --git a/bot/cogs/extensions.py b/bot/cogs/extensions.py new file mode 100644 index 000000000..612a5aad2 --- /dev/null +++ b/bot/cogs/extensions.py @@ -0,0 +1,298 @@ +import logging +import os + +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.decorators import with_role +from bot.pagination import LinePaginator + +log = logging.getLogger(__name__) + +KEEP_LOADED = ["bot.cogs.extensions", "bot.cogs.modlog"] + + +class Extensions(Cog): + """Extension management commands.""" + + 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()}) + + @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. + + 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 + 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" + + 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. + + 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 + 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) + + @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. + + 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',)) + @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. + + Gray indicates that the cog is unloaded. Green indicates that the cog is currently loaded. + """ + embed = Embed() + lines = [] + cogs = {} + + embed.colour = Colour.blurple() + embed.set_author( + name="Python Bot (Cogs)", + 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: + status = Emojis.status_online + else: + status = Emojis.status_offline + + lines.append(f"{status} {cog}") + + 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 setup(bot: Bot) -> None: + """Load the Extensions cog.""" + bot.add_cog(Extensions(bot)) + log.info("Cog loaded: Extensions") -- cgit v1.2.3 From 4f75160a8a66861eb92a0da97c1bc4ffca86402e Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 13:18:32 -0700 Subject: 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 612a5aad2..10f4d38e3 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 @@ log = logging.getLogger(__name__) 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.""" -- cgit v1.2.3 From 6cdda6a6efcb6201d56d036c21a056621533380f Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 14:47:16 -0700 Subject: 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 10f4d38e3..468c350bb 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) -- cgit v1.2.3 From f7109cc9617c0484b6f7742c58961383ef83ddd6 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 14:51:48 -0700 Subject: 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 468c350bb..58ab45ca9 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -7,8 +7,8 @@ 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.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 @@ class Extensions(Cog): 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 @@ class Extensions(Cog): 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 @@ class Extensions(Cog): 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 @@ class Extensions(Cog): 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 @@ class Extensions(Cog): 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.""" -- cgit v1.2.3 From 19ad4392fe50c4c50676fdb509b7208692d48026 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 15:12:44 -0700 Subject: 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 58ab45ca9..83048bb76 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 @@ class Extensions(Cog): 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.""" -- cgit v1.2.3 From a01a969512b8eb11a337b9c5292bae1d678429a2 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 19:06:27 -0700 Subject: 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 83048bb76..e50ef5553 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -5,7 +5,7 @@ from enum import Enum 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 @@ from bot.utils.checks import with_role_check 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 @@ class Extensions(Cog): 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 @@ class Extensions(Cog): """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.""" -- cgit v1.2.3 From 4342f978f4b526a8c6850ccce7f3a3e33a04b1c3 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 19:18:24 -0700 Subject: 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 e50ef5553..c3d6fae27 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -14,7 +14,11 @@ from bot.utils.checks import with_role_check 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): -- cgit v1.2.3 From 0c0fd629192170988ab6bce81144a453e91f7a1d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 20:14:14 -0700 Subject: 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 c3d6fae27..e24e95e6d 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 @@ from bot.utils.checks import with_role_check 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 @@ class Extensions(Cog): 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() -- cgit v1.2.3 From c05d0dbf01f7357ee20a8b7dcc7ca07939ea28c4 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 20:19:27 -0700 Subject: 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 e24e95e6d..0e223b2a3 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -196,6 +196,9 @@ class Extensions(Cog): 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}" -- cgit v1.2.3 From 22c9aaa30c907ceda5e436fa532d8889db73afbc Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 20:20:42 -0700 Subject: 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 0e223b2a3..53952b1a7 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -171,11 +171,11 @@ class Extensions(Cog): """).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.') -- cgit v1.2.3 From 37040baf0f3c3cf9c7e4668a6c4a2b3736031dab Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 20:35:30 -0700 Subject: 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 53952b1a7..5e0bd29bf 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -81,19 +81,19 @@ class Extensions(Cog): 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 @@ class Extensions(Cog): 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: -- cgit v1.2.3 From 1fda5f7e1d7fc3bd7002bf047cd975dae5eb1c25 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 21:37:03 -0700 Subject: 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 5e0bd29bf..0d2cc726e 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 @@ EXTENSIONS = frozenset( 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 @@ class Extension(Converter): 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 @@ class Extensions(Cog): """ 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 @@ class Extensions(Cog): 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 @@ class Extensions(Cog): 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 @@ class Extensions(Cog): # 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 -- cgit v1.2.3 From 0f63028bfc1fea19209342cdd1acbbf57d586e18 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 21:44:18 -0700 Subject: 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 0f49a400c..6648805e9 100644 --- a/bot/cogs/alias.py +++ b/bot/cogs/alias.py @@ -5,6 +5,7 @@ from typing import Union 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 @@ class Alias (Cog): 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: -- cgit v1.2.3 From 82fb11c0e08f6913ce5273a49b269a80c5dd2be4 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 22:15:21 -0700 Subject: 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 0d2cc726e..f848b8a52 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -92,6 +92,10 @@ class Extensions(commands.Cog): 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: -- cgit v1.2.3 From cbccb1e594295bb24983641ae32717f2f002a09b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 3 Oct 2019 22:31:46 -0700 Subject: 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 f848b8a52..3cbaa810a 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -106,44 +106,29 @@ class Extensions(commands.Cog): @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) -- cgit v1.2.3 From 8a8ab1924496560b2f66ff56bd8c9a419d2adb84 Mon Sep 17 00:00:00 2001 From: Jens Date: Sat, 5 Oct 2019 10:20:24 +0200 Subject: Specify names of "prepare_cog" methods --- bot/cogs/antispam.py | 4 ++-- bot/cogs/defcon.py | 4 ++-- bot/cogs/doc.py | 6 +++--- bot/cogs/logging.py | 4 ++-- bot/cogs/moderation.py | 4 ++-- bot/cogs/off_topic_names.py | 4 ++-- bot/cogs/reddit.py | 4 ++-- bot/cogs/reminders.py | 4 ++-- bot/cogs/sync/cog.py | 4 ++-- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index 68b3cf91b..f51804ad3 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -107,14 +107,14 @@ class AntiSpam(Cog): self.message_deletion_queue = dict() self.queue_consumption_tasks = dict() - bot.loop.create_task(self.prepare_cog()) + bot.loop.create_task(self.alert_on_validation_error()) @property def mod_log(self) -> ModLog: """Allows for easy access of the ModLog cog.""" return self.bot.get_cog("ModLog") - async def prepare_cog(self) -> None: + async def alert_on_validation_error(self) -> None: """Unloads the cog and alerts admins if configuration validation failed.""" await self.bot.wait_until_ready() if self.validation_errors: diff --git a/bot/cogs/defcon.py b/bot/cogs/defcon.py index 93d84e6b5..abbf8c770 100644 --- a/bot/cogs/defcon.py +++ b/bot/cogs/defcon.py @@ -35,14 +35,14 @@ class Defcon(Cog): self.channel = None self.days = timedelta(days=0) - bot.loop.create_task(self.prepare_cog()) + bot.loop.create_task(self.sync_settings()) @property def mod_log(self) -> ModLog: """Get currently loaded ModLog cog instance.""" return self.bot.get_cog("ModLog") - async def prepare_cog(self) -> None: + async def sync_settings(self) -> None: """On cog load, try to synchronize DEFCON settings to the API.""" self.bot.wait_until_ready() self.channel = await self.bot.fetch_channel(Channels.defcon) diff --git a/bot/cogs/doc.py b/bot/cogs/doc.py index d503ea4c1..2b0869f04 100644 --- a/bot/cogs/doc.py +++ b/bot/cogs/doc.py @@ -126,10 +126,10 @@ class Doc(commands.Cog): self.bot = bot self.inventories = {} - bot.loop.create_task(self.prepare_cog()) + bot.loop.create_task(self.init_refresh_inventory()) - async def prepare_cog(self) -> None: - """Refresh documentation inventory.""" + async def init_refresh_inventory(self) -> None: + """Refresh documentation inventory on cog initialization.""" await self.bot.wait_until_ready() await self.refresh_inventory() diff --git a/bot/cogs/logging.py b/bot/cogs/logging.py index 25b7d77cc..959e185f9 100644 --- a/bot/cogs/logging.py +++ b/bot/cogs/logging.py @@ -15,9 +15,9 @@ class Logging(Cog): def __init__(self, bot: Bot): self.bot = bot - bot.loop.create_task(self.prepare_cog()) + bot.loop.create_task(self.startup_greeting()) - async def prepare_cog(self) -> None: + async def startup_greeting(self) -> None: """Announce our presence to the configured devlog channel.""" await self.bot.wait_until_ready() log.info("Bot connected!") diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 79502ee1c..8a5cb5853 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -64,14 +64,14 @@ class Moderation(Scheduler, Cog): self._muted_role = Object(constants.Roles.muted) super().__init__() - bot.loop.create_task(self.prepare_cog()) + bot.loop.create_task(self.schedule_infractions()) @property def mod_log(self) -> ModLog: """Get currently loaded ModLog cog instance.""" return self.bot.get_cog("ModLog") - async def prepare_cog(self) -> None: + async def schedule_infractions(self) -> None: """Schedule expiration for previous infractions.""" await self.bot.wait_until_ready() # Schedule expiration for previous infractions diff --git a/bot/cogs/off_topic_names.py b/bot/cogs/off_topic_names.py index eb966c737..ca943e73f 100644 --- a/bot/cogs/off_topic_names.py +++ b/bot/cogs/off_topic_names.py @@ -75,14 +75,14 @@ class OffTopicNames(Cog): self.bot = bot self.updater_task = None - bot.loop.create_task(self.prepare_cog()) + bot.loop.create_task(self.init_offtopic_updater()) def cog_unload(self) -> None: """Cancel any running updater tasks on cog unload.""" if self.updater_task is not None: self.updater_task.cancel() - async def prepare_cog(self) -> None: + async def init_offtopic_updater(self) -> None: """Start off-topic channel updating event loop if it hasn't already started.""" self.bot.wait_until_ready() if self.updater_task is None: diff --git a/bot/cogs/reddit.py b/bot/cogs/reddit.py index ba926e166..c7ed01aa1 100644 --- a/bot/cogs/reddit.py +++ b/bot/cogs/reddit.py @@ -33,7 +33,7 @@ class Reddit(Cog): self.new_posts_task = None self.top_weekly_posts_task = None - bot.loop.create_task(self.prepare_cog()) + bot.loop.create_task(self.init_reddit_polling()) async def fetch_posts(self, route: str, *, amount: int = 25, params: dict = None) -> List[dict]: """A helper method to fetch a certain amount of Reddit posts at a given route.""" @@ -255,7 +255,7 @@ class Reddit(Cog): max_lines=15 ) - async def prepare_cog(self) -> None: + async def init_reddit_polling(self) -> None: """Initiate reddit post event loop.""" self.bot.wait_until_ready() self.reddit_channel = await self.bot.fetch_channel(Channels.reddit) diff --git a/bot/cogs/reminders.py b/bot/cogs/reminders.py index dc5536b12..eb6e49ba9 100644 --- a/bot/cogs/reminders.py +++ b/bot/cogs/reminders.py @@ -30,9 +30,9 @@ class Reminders(Scheduler, Cog): self.bot = bot super().__init__() - bot.loop.create_task(self.prepare_cog()) + bot.loop.create_task(self.reschedule_reminders()) - async def prepare_cog(self) -> None: + async def reschedule_reminders(self) -> None: """Get all current reminders from the API and reschedule them.""" self.bot.wait_until_ready() response = await self.bot.api_client.get( diff --git a/bot/cogs/sync/cog.py b/bot/cogs/sync/cog.py index 15e671ab3..b61b089fc 100644 --- a/bot/cogs/sync/cog.py +++ b/bot/cogs/sync/cog.py @@ -29,9 +29,9 @@ class Sync(Cog): def __init__(self, bot: Bot) -> None: self.bot = bot - bot.loop.create_task(self.prepare_cog()) + bot.loop.create_task(self.sync_guild()) - async def prepare_cog(self) -> None: + async def sync_guild(self) -> None: """Syncs the roles/users of the guild with the database.""" self.bot.wait_until_ready() guild = self.bot.get_guild(self.SYNC_SERVER_ID) -- cgit v1.2.3 From ab5d9722569c027f13fce7daa420fe74b4acf311 Mon Sep 17 00:00:00 2001 From: Jens Date: Mon, 7 Oct 2019 10:17:06 +0200 Subject: Add missing awaits and call bot as attribut --- bot/cogs/antispam.py | 2 +- bot/cogs/defcon.py | 4 ++-- bot/cogs/doc.py | 2 +- bot/cogs/logging.py | 2 +- bot/cogs/moderation.py | 2 +- bot/cogs/off_topic_names.py | 4 ++-- bot/cogs/reddit.py | 4 ++-- bot/cogs/reminders.py | 4 ++-- bot/cogs/sync/cog.py | 4 ++-- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index f51804ad3..37516c519 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -107,7 +107,7 @@ class AntiSpam(Cog): self.message_deletion_queue = dict() self.queue_consumption_tasks = dict() - bot.loop.create_task(self.alert_on_validation_error()) + self.bot.loop.create_task(self.alert_on_validation_error()) @property def mod_log(self) -> ModLog: diff --git a/bot/cogs/defcon.py b/bot/cogs/defcon.py index abbf8c770..e82b6d2e1 100644 --- a/bot/cogs/defcon.py +++ b/bot/cogs/defcon.py @@ -35,7 +35,7 @@ class Defcon(Cog): self.channel = None self.days = timedelta(days=0) - bot.loop.create_task(self.sync_settings()) + self.bot.loop.create_task(self.sync_settings()) @property def mod_log(self) -> ModLog: @@ -44,7 +44,7 @@ class Defcon(Cog): async def sync_settings(self) -> None: """On cog load, try to synchronize DEFCON settings to the API.""" - self.bot.wait_until_ready() + await self.bot.wait_until_ready() self.channel = await self.bot.fetch_channel(Channels.defcon) try: response = await self.bot.api_client.get('bot/bot-settings/defcon') diff --git a/bot/cogs/doc.py b/bot/cogs/doc.py index 2b0869f04..e87192a86 100644 --- a/bot/cogs/doc.py +++ b/bot/cogs/doc.py @@ -126,7 +126,7 @@ class Doc(commands.Cog): self.bot = bot self.inventories = {} - bot.loop.create_task(self.init_refresh_inventory()) + self.bot.loop.create_task(self.init_refresh_inventory()) async def init_refresh_inventory(self) -> None: """Refresh documentation inventory on cog initialization.""" diff --git a/bot/cogs/logging.py b/bot/cogs/logging.py index 959e185f9..c92b619ff 100644 --- a/bot/cogs/logging.py +++ b/bot/cogs/logging.py @@ -15,7 +15,7 @@ class Logging(Cog): def __init__(self, bot: Bot): self.bot = bot - bot.loop.create_task(self.startup_greeting()) + self.bot.loop.create_task(self.startup_greeting()) async def startup_greeting(self) -> None: """Announce our presence to the configured devlog channel.""" diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 8a5cb5853..e2470c600 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -64,7 +64,7 @@ class Moderation(Scheduler, Cog): self._muted_role = Object(constants.Roles.muted) super().__init__() - bot.loop.create_task(self.schedule_infractions()) + self.bot.loop.create_task(self.schedule_infractions()) @property def mod_log(self) -> ModLog: diff --git a/bot/cogs/off_topic_names.py b/bot/cogs/off_topic_names.py index ca943e73f..2977e4ebb 100644 --- a/bot/cogs/off_topic_names.py +++ b/bot/cogs/off_topic_names.py @@ -75,7 +75,7 @@ class OffTopicNames(Cog): self.bot = bot self.updater_task = None - bot.loop.create_task(self.init_offtopic_updater()) + self.bot.loop.create_task(self.init_offtopic_updater()) def cog_unload(self) -> None: """Cancel any running updater tasks on cog unload.""" @@ -84,7 +84,7 @@ class OffTopicNames(Cog): async def init_offtopic_updater(self) -> None: """Start off-topic channel updating event loop if it hasn't already started.""" - self.bot.wait_until_ready() + await self.bot.wait_until_ready() if self.updater_task is None: coro = update_names(self.bot) self.updater_task = self.bot.loop.create_task(coro) diff --git a/bot/cogs/reddit.py b/bot/cogs/reddit.py index c7ed01aa1..d4a16a0a7 100644 --- a/bot/cogs/reddit.py +++ b/bot/cogs/reddit.py @@ -33,7 +33,7 @@ class Reddit(Cog): self.new_posts_task = None self.top_weekly_posts_task = None - bot.loop.create_task(self.init_reddit_polling()) + self.bot.loop.create_task(self.init_reddit_polling()) async def fetch_posts(self, route: str, *, amount: int = 25, params: dict = None) -> List[dict]: """A helper method to fetch a certain amount of Reddit posts at a given route.""" @@ -257,7 +257,7 @@ class Reddit(Cog): async def init_reddit_polling(self) -> None: """Initiate reddit post event loop.""" - self.bot.wait_until_ready() + await self.bot.wait_until_ready() self.reddit_channel = await self.bot.fetch_channel(Channels.reddit) if self.reddit_channel is not None: diff --git a/bot/cogs/reminders.py b/bot/cogs/reminders.py index eb6e49ba9..b54622306 100644 --- a/bot/cogs/reminders.py +++ b/bot/cogs/reminders.py @@ -30,11 +30,11 @@ class Reminders(Scheduler, Cog): self.bot = bot super().__init__() - bot.loop.create_task(self.reschedule_reminders()) + self.bot.loop.create_task(self.reschedule_reminders()) async def reschedule_reminders(self) -> None: """Get all current reminders from the API and reschedule them.""" - self.bot.wait_until_ready() + await self.bot.wait_until_ready() response = await self.bot.api_client.get( 'bot/reminders', params={'active': 'true'} diff --git a/bot/cogs/sync/cog.py b/bot/cogs/sync/cog.py index b61b089fc..aaa581f96 100644 --- a/bot/cogs/sync/cog.py +++ b/bot/cogs/sync/cog.py @@ -29,11 +29,11 @@ class Sync(Cog): def __init__(self, bot: Bot) -> None: self.bot = bot - bot.loop.create_task(self.sync_guild()) + self.bot.loop.create_task(self.sync_guild()) async def sync_guild(self) -> None: """Syncs the roles/users of the guild with the database.""" - self.bot.wait_until_ready() + await self.bot.wait_until_ready() guild = self.bot.get_guild(self.SYNC_SERVER_ID) if guild is not None: for syncer in self.ON_READY_SYNCERS: -- cgit v1.2.3 From 2ece22e0c8b58290e7d90d71849d01272d138fe8 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 7 Oct 2019 09:35:48 -0700 Subject: 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 3cbaa810a..a385e50d5 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -89,8 +89,8 @@ class Extensions(commands.Cog): 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 @@ class Extensions(commands.Cog): """ 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 = {} -- cgit v1.2.3 From 77216353a87bcf2dbf67cfe028f9f38ba7a2406e Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 7 Oct 2019 10:23:13 -0700 Subject: 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 a385e50d5..5f9b4aef4 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 @@ class Extensions(commands.Cog): 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 @@ class Extensions(commands.Cog): 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 @@ class Extensions(commands.Cog): 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 @@ class Extensions(commands.Cog): 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:]) -- cgit v1.2.3 From da7b23cddc22a27c5b1091bbf25a6ae714b07a8c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 7 Oct 2019 10:30:50 -0700 Subject: 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 5f9b4aef4..3c59ad8c2 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -71,8 +71,8 @@ class Extensions(commands.Cog): """ 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 @@ class Extensions(commands.Cog): """ 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 @@ class Extensions(commands.Cog): 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 -- cgit v1.2.3 From 319cf13c1946715cff5fbadfdaa301e86849547c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 9 Oct 2019 16:42:48 -0700 Subject: 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 3c59ad8c2..bb66e0b8e 100644 --- a/bot/cogs/extensions.py +++ b/bot/cogs/extensions.py @@ -73,6 +73,10 @@ class Extensions(commands.Cog): 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 @@ class Extensions(commands.Cog): 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: -- cgit v1.2.3