diff options
Diffstat (limited to '')
| -rw-r--r-- | bot/__main__.py | 52 | ||||
| -rw-r--r-- | bot/cogs/moderation/__init__.py | 19 | ||||
| -rw-r--r-- | bot/cogs/sync/__init__.py | 7 | ||||
| -rw-r--r-- | bot/cogs/watchchannels/__init__.py | 9 | ||||
| -rw-r--r-- | bot/exts/__init__.py (renamed from bot/cogs/__init__.py) | 0 | ||||
| -rw-r--r-- | bot/exts/backend/__init__.py (renamed from tests/bot/cogs/__init__.py) | 0 | ||||
| -rw-r--r-- | bot/exts/backend/alias.py (renamed from bot/cogs/alias.py) | 0 | ||||
| -rw-r--r-- | bot/exts/backend/config_verifier.py (renamed from bot/cogs/config_verifier.py) | 0 | ||||
| -rw-r--r-- | bot/exts/backend/error_handler.py (renamed from bot/cogs/error_handler.py) | 0 | ||||
| -rw-r--r-- | bot/exts/backend/logging.py (renamed from bot/cogs/logging.py) | 0 | ||||
| -rw-r--r-- | bot/exts/backend/sync/__init__.py | 8 | ||||
| -rw-r--r-- | bot/exts/backend/sync/_cog.py (renamed from bot/cogs/sync/cog.py) | 6 | ||||
| -rw-r--r-- | bot/exts/backend/sync/_syncers.py (renamed from bot/cogs/sync/syncers.py) | 0 | ||||
| -rw-r--r-- | bot/exts/filters/__init__.py (renamed from tests/bot/cogs/moderation/__init__.py) | 0 | ||||
| -rw-r--r-- | bot/exts/filters/antimalware.py (renamed from bot/cogs/antimalware.py) | 0 | ||||
| -rw-r--r-- | bot/exts/filters/antispam.py (renamed from bot/cogs/antispam.py) | 2 | ||||
| -rw-r--r-- | bot/exts/filters/filter_lists.py (renamed from bot/cogs/filter_lists.py) | 0 | ||||
| -rw-r--r-- | bot/exts/filters/filtering.py (renamed from bot/cogs/filtering.py) | 2 | ||||
| -rw-r--r-- | bot/exts/filters/security.py (renamed from bot/cogs/security.py) | 0 | ||||
| -rw-r--r-- | bot/exts/filters/token_remover.py (renamed from bot/cogs/token_remover.py) | 2 | ||||
| -rw-r--r-- | bot/exts/filters/webhook_remover.py (renamed from bot/cogs/webhook_remover.py) | 2 | ||||
| -rw-r--r-- | bot/exts/fun/__init__.py (renamed from tests/bot/cogs/sync/__init__.py) | 0 | ||||
| -rw-r--r-- | bot/exts/fun/duck_pond.py (renamed from bot/cogs/duck_pond.py) | 0 | ||||
| -rw-r--r-- | bot/exts/fun/off_topic_names.py (renamed from bot/cogs/off_topic_names.py) | 0 | ||||
| -rw-r--r-- | bot/exts/help_channels.py (renamed from bot/cogs/help_channels.py) | 0 | ||||
| -rw-r--r-- | bot/exts/info/__init__.py | 0 | ||||
| -rw-r--r-- | bot/exts/info/doc.py (renamed from bot/cogs/doc.py) | 0 | ||||
| -rw-r--r-- | bot/exts/info/help.py (renamed from bot/cogs/help.py) | 0 | ||||
| -rw-r--r-- | bot/exts/info/information.py (renamed from bot/cogs/information.py) | 0 | ||||
| -rw-r--r-- | bot/exts/info/python_news.py (renamed from bot/cogs/python_news.py) | 0 | ||||
| -rw-r--r-- | bot/exts/info/reddit.py (renamed from bot/cogs/reddit.py) | 0 | ||||
| -rw-r--r-- | bot/exts/info/site.py (renamed from bot/cogs/site.py) | 0 | ||||
| -rw-r--r-- | bot/exts/info/source.py (renamed from bot/cogs/source.py) | 0 | ||||
| -rw-r--r-- | bot/exts/info/stats.py (renamed from bot/cogs/stats.py) | 0 | ||||
| -rw-r--r-- | bot/exts/info/tags.py (renamed from bot/cogs/tags.py) | 0 | ||||
| -rw-r--r-- | bot/exts/moderation/__init__.py | 0 | ||||
| -rw-r--r-- | bot/exts/moderation/defcon.py (renamed from bot/cogs/defcon.py) | 2 | ||||
| -rw-r--r-- | bot/exts/moderation/dm_relay.py (renamed from bot/cogs/dm_relay.py) | 0 | ||||
| -rw-r--r-- | bot/exts/moderation/incidents.py (renamed from bot/cogs/moderation/incidents.py) | 5 | ||||
| -rw-r--r-- | bot/exts/moderation/infraction/__init__.py | 0 | ||||
| -rw-r--r-- | bot/exts/moderation/infraction/_scheduler.py (renamed from bot/cogs/moderation/scheduler.py) | 24 | ||||
| -rw-r--r-- | bot/exts/moderation/infraction/_utils.py (renamed from bot/cogs/moderation/utils.py) | 0 | ||||
| -rw-r--r-- | bot/exts/moderation/infraction/infractions.py (renamed from bot/cogs/moderation/infractions.py) | 31 | ||||
| -rw-r--r-- | bot/exts/moderation/infraction/management.py (renamed from bot/cogs/moderation/management.py) | 15 | ||||
| -rw-r--r-- | bot/exts/moderation/infraction/superstarify.py (renamed from bot/cogs/moderation/superstarify.py) | 29 | ||||
| -rw-r--r-- | bot/exts/moderation/modlog.py (renamed from bot/cogs/moderation/modlog.py) | 5 | ||||
| -rw-r--r-- | bot/exts/moderation/silence.py (renamed from bot/cogs/moderation/silence.py) | 5 | ||||
| -rw-r--r-- | bot/exts/moderation/slowmode.py (renamed from bot/cogs/moderation/slowmode.py) | 0 | ||||
| -rw-r--r-- | bot/exts/moderation/verification.py (renamed from bot/cogs/verification.py) | 2 | ||||
| -rw-r--r-- | bot/exts/moderation/watchchannels/__init__.py | 0 | ||||
| -rw-r--r-- | bot/exts/moderation/watchchannels/_watchchannel.py (renamed from bot/cogs/watchchannels/watchchannel.py) | 6 | ||||
| -rw-r--r-- | bot/exts/moderation/watchchannels/bigbrother.py (renamed from bot/cogs/watchchannels/bigbrother.py) | 9 | ||||
| -rw-r--r-- | bot/exts/moderation/watchchannels/talentpool.py (renamed from bot/cogs/watchchannels/talentpool.py) | 7 | ||||
| -rw-r--r-- | bot/exts/utils/__init__.py | 0 | ||||
| -rw-r--r-- | bot/exts/utils/bot.py (renamed from bot/cogs/bot.py) | 4 | ||||
| -rw-r--r-- | bot/exts/utils/clean.py (renamed from bot/cogs/clean.py) | 2 | ||||
| -rw-r--r-- | bot/exts/utils/eval.py (renamed from bot/cogs/eval.py) | 0 | ||||
| -rw-r--r-- | bot/exts/utils/extensions.py (renamed from bot/cogs/extensions.py) | 67 | ||||
| -rw-r--r-- | bot/exts/utils/jams.py (renamed from bot/cogs/jams.py) | 0 | ||||
| -rw-r--r-- | bot/exts/utils/reminders.py (renamed from bot/cogs/reminders.py) | 0 | ||||
| -rw-r--r-- | bot/exts/utils/snekbox.py (renamed from bot/cogs/snekbox.py) | 0 | ||||
| -rw-r--r-- | bot/exts/utils/utils.py (renamed from bot/cogs/utils.py) | 0 | ||||
| -rw-r--r-- | bot/utils/extensions.py | 34 | ||||
| -rw-r--r-- | tests/bot/exts/__init__.py | 0 | ||||
| -rw-r--r-- | tests/bot/exts/backend/__init__.py | 0 | ||||
| -rw-r--r-- | tests/bot/exts/backend/sync/__init__.py | 0 | ||||
| -rw-r--r-- | tests/bot/exts/backend/sync/test_base.py (renamed from tests/bot/cogs/sync/test_base.py) | 2 | ||||
| -rw-r--r-- | tests/bot/exts/backend/sync/test_cog.py (renamed from tests/bot/cogs/sync/test_cog.py) | 17 | ||||
| -rw-r--r-- | tests/bot/exts/backend/sync/test_roles.py (renamed from tests/bot/cogs/sync/test_roles.py) | 2 | ||||
| -rw-r--r-- | tests/bot/exts/backend/sync/test_users.py (renamed from tests/bot/cogs/sync/test_users.py) | 2 | ||||
| -rw-r--r-- | tests/bot/exts/backend/test_logging.py (renamed from tests/bot/cogs/test_logging.py) | 6 | ||||
| -rw-r--r-- | tests/bot/exts/filters/__init__.py | 0 | ||||
| -rw-r--r-- | tests/bot/exts/filters/test_antimalware.py (renamed from tests/bot/cogs/test_antimalware.py) | 2 | ||||
| -rw-r--r-- | tests/bot/exts/filters/test_antispam.py (renamed from tests/bot/cogs/test_antispam.py) | 2 | ||||
| -rw-r--r-- | tests/bot/exts/filters/test_security.py (renamed from tests/bot/cogs/test_security.py) | 2 | ||||
| -rw-r--r-- | tests/bot/exts/filters/test_token_remover.py (renamed from tests/bot/cogs/test_token_remover.py) | 22 | ||||
| -rw-r--r-- | tests/bot/exts/fun/__init__.py | 0 | ||||
| -rw-r--r-- | tests/bot/exts/fun/test_duck_pond.py (renamed from tests/bot/cogs/test_duck_pond.py) | 10 | ||||
| -rw-r--r-- | tests/bot/exts/info/__init__.py | 0 | ||||
| -rw-r--r-- | tests/bot/exts/info/test_information.py (renamed from tests/bot/cogs/test_information.py) | 20 | ||||
| -rw-r--r-- | tests/bot/exts/moderation/__init__.py | 0 | ||||
| -rw-r--r-- | tests/bot/exts/moderation/infraction/__init__.py | 0 | ||||
| -rw-r--r-- | tests/bot/exts/moderation/infraction/test_infractions.py (renamed from tests/bot/cogs/moderation/test_infractions.py) | 8 | ||||
| -rw-r--r-- | tests/bot/exts/moderation/test_incidents.py (renamed from tests/bot/cogs/moderation/test_incidents.py) | 66 | ||||
| -rw-r--r-- | tests/bot/exts/moderation/test_modlog.py (renamed from tests/bot/cogs/moderation/test_modlog.py) | 2 | ||||
| -rw-r--r-- | tests/bot/exts/moderation/test_silence.py (renamed from tests/bot/cogs/moderation/test_silence.py) | 12 | ||||
| -rw-r--r-- | tests/bot/exts/moderation/test_slowmode.py (renamed from tests/bot/cogs/test_slowmode.py) | 6 | ||||
| -rw-r--r-- | tests/bot/exts/test_cogs.py (renamed from tests/bot/cogs/test_cogs.py) | 7 | ||||
| -rw-r--r-- | tests/bot/exts/utils/__init__.py | 0 | ||||
| -rw-r--r-- | tests/bot/exts/utils/test_jams.py (renamed from tests/bot/cogs/test_jams.py) | 2 | ||||
| -rw-r--r-- | tests/bot/exts/utils/test_snekbox.py (renamed from tests/bot/cogs/test_snekbox.py) | 12 | 
91 files changed, 301 insertions, 257 deletions
| diff --git a/bot/__main__.py b/bot/__main__.py index fe2cf90e6..8770ac31b 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -9,7 +9,9 @@ from sentry_sdk.integrations.redis import RedisIntegration  from bot import constants, patches  from bot.bot import Bot +from bot.utils.extensions import EXTENSIONS +# Set up Sentry.  sentry_logging = LoggingIntegration(      level=logging.DEBUG,      event_level=logging.WARNING @@ -24,6 +26,7 @@ sentry_sdk.init(      ]  ) +# Instantiate the bot.  allowed_roles = [discord.Object(id_) for id_ in constants.MODERATION_ROLES]  bot = Bot(      command_prefix=when_mentioned_or(constants.Bot.prefix), @@ -33,50 +36,13 @@ bot = Bot(      allowed_mentions=discord.AllowedMentions(everyone=False, roles=allowed_roles)  ) -# Internal/debug -bot.load_extension("bot.cogs.config_verifier") -bot.load_extension("bot.cogs.error_handler") -bot.load_extension("bot.cogs.filtering") -bot.load_extension("bot.cogs.logging") -bot.load_extension("bot.cogs.security") +# Load extensions. +extensions = set(EXTENSIONS)  # Create a mutable copy. +if not constants.HelpChannels.enable: +    extensions.remove("bot.exts.help_channels") -# Commands, etc -bot.load_extension("bot.cogs.antimalware") -bot.load_extension("bot.cogs.antispam") -bot.load_extension("bot.cogs.bot") -bot.load_extension("bot.cogs.clean") -bot.load_extension("bot.cogs.doc") -bot.load_extension("bot.cogs.extensions") -bot.load_extension("bot.cogs.help") -bot.load_extension("bot.cogs.verification") - -# Feature cogs -bot.load_extension("bot.cogs.alias") -bot.load_extension("bot.cogs.defcon") -bot.load_extension("bot.cogs.dm_relay") -bot.load_extension("bot.cogs.duck_pond") -bot.load_extension("bot.cogs.eval") -bot.load_extension("bot.cogs.filter_lists") -bot.load_extension("bot.cogs.information") -bot.load_extension("bot.cogs.jams") -bot.load_extension("bot.cogs.moderation") -bot.load_extension("bot.cogs.off_topic_names") -bot.load_extension("bot.cogs.python_news") -bot.load_extension("bot.cogs.reddit") -bot.load_extension("bot.cogs.reminders") -bot.load_extension("bot.cogs.site") -bot.load_extension("bot.cogs.snekbox") -bot.load_extension("bot.cogs.source") -bot.load_extension("bot.cogs.stats") -bot.load_extension("bot.cogs.sync") -bot.load_extension("bot.cogs.tags") -bot.load_extension("bot.cogs.token_remover") -bot.load_extension("bot.cogs.utils") -bot.load_extension("bot.cogs.watchchannels") -bot.load_extension("bot.cogs.webhook_remover") - -if constants.HelpChannels.enable: -    bot.load_extension("bot.cogs.help_channels") +for extension in extensions: +    bot.load_extension(extension)  # Apply `message_edited_at` patch if discord.py did not yet release a bug fix.  if not hasattr(discord.message.Message, '_handle_edited_timestamp'): diff --git a/bot/cogs/moderation/__init__.py b/bot/cogs/moderation/__init__.py deleted file mode 100644 index 995187ef0..000000000 --- a/bot/cogs/moderation/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -from bot.bot import Bot -from .incidents import Incidents -from .infractions import Infractions -from .management import ModManagement -from .modlog import ModLog -from .silence import Silence -from .slowmode import Slowmode -from .superstarify import Superstarify - - -def setup(bot: Bot) -> None: -    """Load the Incidents, Infractions, ModManagement, ModLog, Silence, Slowmode and Superstarify cogs.""" -    bot.add_cog(Incidents(bot)) -    bot.add_cog(Infractions(bot)) -    bot.add_cog(ModLog(bot)) -    bot.add_cog(ModManagement(bot)) -    bot.add_cog(Silence(bot)) -    bot.add_cog(Slowmode(bot)) -    bot.add_cog(Superstarify(bot)) diff --git a/bot/cogs/sync/__init__.py b/bot/cogs/sync/__init__.py deleted file mode 100644 index fe7df4e9b..000000000 --- a/bot/cogs/sync/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from bot.bot import Bot -from .cog import Sync - - -def setup(bot: Bot) -> None: -    """Load the Sync cog.""" -    bot.add_cog(Sync(bot)) diff --git a/bot/cogs/watchchannels/__init__.py b/bot/cogs/watchchannels/__init__.py deleted file mode 100644 index 69d118df6..000000000 --- a/bot/cogs/watchchannels/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from bot.bot import Bot -from .bigbrother import BigBrother -from .talentpool import TalentPool - - -def setup(bot: Bot) -> None: -    """Load the BigBrother and TalentPool cogs.""" -    bot.add_cog(BigBrother(bot)) -    bot.add_cog(TalentPool(bot)) diff --git a/bot/cogs/__init__.py b/bot/exts/__init__.py index e69de29bb..e69de29bb 100644 --- a/bot/cogs/__init__.py +++ b/bot/exts/__init__.py diff --git a/tests/bot/cogs/__init__.py b/bot/exts/backend/__init__.py index e69de29bb..e69de29bb 100644 --- a/tests/bot/cogs/__init__.py +++ b/bot/exts/backend/__init__.py diff --git a/bot/cogs/alias.py b/bot/exts/backend/alias.py index c6ba8d6f3..c6ba8d6f3 100644 --- a/bot/cogs/alias.py +++ b/bot/exts/backend/alias.py diff --git a/bot/cogs/config_verifier.py b/bot/exts/backend/config_verifier.py index d72c6c22e..d72c6c22e 100644 --- a/bot/cogs/config_verifier.py +++ b/bot/exts/backend/config_verifier.py diff --git a/bot/cogs/error_handler.py b/bot/exts/backend/error_handler.py index f9d4de638..f9d4de638 100644 --- a/bot/cogs/error_handler.py +++ b/bot/exts/backend/error_handler.py diff --git a/bot/cogs/logging.py b/bot/exts/backend/logging.py index 94fa2b139..94fa2b139 100644 --- a/bot/cogs/logging.py +++ b/bot/exts/backend/logging.py diff --git a/bot/exts/backend/sync/__init__.py b/bot/exts/backend/sync/__init__.py new file mode 100644 index 000000000..829098f79 --- /dev/null +++ b/bot/exts/backend/sync/__init__.py @@ -0,0 +1,8 @@ +from bot.bot import Bot + + +def setup(bot: Bot) -> None: +    """Load the Sync cog.""" +    # Defer import to reduce side effects from importing the sync package. +    from bot.exts.backend.sync._cog import Sync +    bot.add_cog(Sync(bot)) diff --git a/bot/cogs/sync/cog.py b/bot/exts/backend/sync/_cog.py index 5ace957e7..6e85e2b7d 100644 --- a/bot/cogs/sync/cog.py +++ b/bot/exts/backend/sync/_cog.py @@ -8,7 +8,7 @@ from discord.ext.commands import Cog, Context  from bot import constants  from bot.api import ResponseCodeError  from bot.bot import Bot -from bot.cogs.sync import syncers +from bot.exts.backend.sync import _syncers  log = logging.getLogger(__name__) @@ -18,8 +18,8 @@ class Sync(Cog):      def __init__(self, bot: Bot) -> None:          self.bot = bot -        self.role_syncer = syncers.RoleSyncer(self.bot) -        self.user_syncer = syncers.UserSyncer(self.bot) +        self.role_syncer = _syncers.RoleSyncer(self.bot) +        self.user_syncer = _syncers.UserSyncer(self.bot)          self.bot.loop.create_task(self.sync_guild()) diff --git a/bot/cogs/sync/syncers.py b/bot/exts/backend/sync/_syncers.py index 156c32a15..156c32a15 100644 --- a/bot/cogs/sync/syncers.py +++ b/bot/exts/backend/sync/_syncers.py diff --git a/tests/bot/cogs/moderation/__init__.py b/bot/exts/filters/__init__.py index e69de29bb..e69de29bb 100644 --- a/tests/bot/cogs/moderation/__init__.py +++ b/bot/exts/filters/__init__.py diff --git a/bot/cogs/antimalware.py b/bot/exts/filters/antimalware.py index 7894ec48f..7894ec48f 100644 --- a/bot/cogs/antimalware.py +++ b/bot/exts/filters/antimalware.py diff --git a/bot/cogs/antispam.py b/bot/exts/filters/antispam.py index 3ad487d8c..2e7e32d9a 100644 --- a/bot/cogs/antispam.py +++ b/bot/exts/filters/antispam.py @@ -11,7 +11,6 @@ from discord.ext.commands import Cog  from bot import rules  from bot.bot import Bot -from bot.cogs.moderation import ModLog  from bot.constants import (      AntiSpam as AntiSpamConfig, Channels,      Colours, DEBUG_MODE, Event, Filter, @@ -19,6 +18,7 @@ from bot.constants import (      STAFF_ROLES,  )  from bot.converters import Duration +from bot.exts.moderation.modlog import ModLog  from bot.utils.messages import send_attachments diff --git a/bot/cogs/filter_lists.py b/bot/exts/filters/filter_lists.py index c15adc461..c15adc461 100644 --- a/bot/cogs/filter_lists.py +++ b/bot/exts/filters/filter_lists.py diff --git a/bot/cogs/filtering.py b/bot/exts/filters/filtering.py index 99b659bff..2751ed7f6 100644 --- a/bot/cogs/filtering.py +++ b/bot/exts/filters/filtering.py @@ -13,11 +13,11 @@ from discord.utils import escape_markdown  from bot.api import ResponseCodeError  from bot.bot import Bot -from bot.cogs.moderation import ModLog  from bot.constants import (      Channels, Colours,      Filter, Icons, URLs  ) +from bot.exts.moderation.modlog import ModLog  from bot.utils.redis_cache import RedisCache  from bot.utils.regex import INVITE_RE  from bot.utils.scheduling import Scheduler diff --git a/bot/cogs/security.py b/bot/exts/filters/security.py index c680c5e27..c680c5e27 100644 --- a/bot/cogs/security.py +++ b/bot/exts/filters/security.py diff --git a/bot/cogs/token_remover.py b/bot/exts/filters/token_remover.py index ef979f222..0eda3dc6a 100644 --- a/bot/cogs/token_remover.py +++ b/bot/exts/filters/token_remover.py @@ -9,8 +9,8 @@ from discord.ext.commands import Cog  from bot import utils  from bot.bot import Bot -from bot.cogs.moderation import ModLog  from bot.constants import Channels, Colours, Event, Icons +from bot.exts.moderation.modlog import ModLog  log = logging.getLogger(__name__) diff --git a/bot/cogs/webhook_remover.py b/bot/exts/filters/webhook_remover.py index 5812da87c..ca126ebf5 100644 --- a/bot/cogs/webhook_remover.py +++ b/bot/exts/filters/webhook_remover.py @@ -5,8 +5,8 @@ from discord import Colour, Message, NotFound  from discord.ext.commands import Cog  from bot.bot import Bot -from bot.cogs.moderation.modlog import ModLog  from bot.constants import Channels, Colours, Event, Icons +from bot.exts.moderation.modlog import ModLog  WEBHOOK_URL_RE = re.compile(r"((?:https?://)?discord(?:app)?\.com/api/webhooks/\d+/)\S+/?", re.IGNORECASE) diff --git a/tests/bot/cogs/sync/__init__.py b/bot/exts/fun/__init__.py index e69de29bb..e69de29bb 100644 --- a/tests/bot/cogs/sync/__init__.py +++ b/bot/exts/fun/__init__.py diff --git a/bot/cogs/duck_pond.py b/bot/exts/fun/duck_pond.py index 7021069fa..7021069fa 100644 --- a/bot/cogs/duck_pond.py +++ b/bot/exts/fun/duck_pond.py diff --git a/bot/cogs/off_topic_names.py b/bot/exts/fun/off_topic_names.py index ce95450e0..ce95450e0 100644 --- a/bot/cogs/off_topic_names.py +++ b/bot/exts/fun/off_topic_names.py diff --git a/bot/cogs/help_channels.py b/bot/exts/help_channels.py index 0f9cac89e..0f9cac89e 100644 --- a/bot/cogs/help_channels.py +++ b/bot/exts/help_channels.py diff --git a/bot/exts/info/__init__.py b/bot/exts/info/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/bot/exts/info/__init__.py diff --git a/bot/cogs/doc.py b/bot/exts/info/doc.py index 30c793c75..30c793c75 100644 --- a/bot/cogs/doc.py +++ b/bot/exts/info/doc.py diff --git a/bot/cogs/help.py b/bot/exts/info/help.py index 99d503f5c..99d503f5c 100644 --- a/bot/cogs/help.py +++ b/bot/exts/info/help.py diff --git a/bot/cogs/information.py b/bot/exts/info/information.py index 55ecb2836..55ecb2836 100644 --- a/bot/cogs/information.py +++ b/bot/exts/info/information.py diff --git a/bot/cogs/python_news.py b/bot/exts/info/python_news.py index 0ab5738a4..0ab5738a4 100644 --- a/bot/cogs/python_news.py +++ b/bot/exts/info/python_news.py diff --git a/bot/cogs/reddit.py b/bot/exts/info/reddit.py index 5d9e2c20b..5d9e2c20b 100644 --- a/bot/cogs/reddit.py +++ b/bot/exts/info/reddit.py diff --git a/bot/cogs/site.py b/bot/exts/info/site.py index 2d3a3d9f3..2d3a3d9f3 100644 --- a/bot/cogs/site.py +++ b/bot/exts/info/site.py diff --git a/bot/cogs/source.py b/bot/exts/info/source.py index 205e0ba81..205e0ba81 100644 --- a/bot/cogs/source.py +++ b/bot/exts/info/source.py diff --git a/bot/cogs/stats.py b/bot/exts/info/stats.py index d42f55466..d42f55466 100644 --- a/bot/cogs/stats.py +++ b/bot/exts/info/stats.py diff --git a/bot/cogs/tags.py b/bot/exts/info/tags.py index d01647312..d01647312 100644 --- a/bot/cogs/tags.py +++ b/bot/exts/info/tags.py diff --git a/bot/exts/moderation/__init__.py b/bot/exts/moderation/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/bot/exts/moderation/__init__.py diff --git a/bot/cogs/defcon.py b/bot/exts/moderation/defcon.py index 9087ac454..6e4008777 100644 --- a/bot/cogs/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -9,9 +9,9 @@ from discord import Colour, Embed, Member  from discord.ext.commands import Cog, Context, group  from bot.bot import Bot -from bot.cogs.moderation import ModLog  from bot.constants import Channels, Colours, Emojis, Event, Icons, MODERATION_ROLES, Roles  from bot.decorators import with_role +from bot.exts.moderation.modlog import ModLog  log = logging.getLogger(__name__) diff --git a/bot/cogs/dm_relay.py b/bot/exts/moderation/dm_relay.py index 0d8f340b4..0d8f340b4 100644 --- a/bot/cogs/dm_relay.py +++ b/bot/exts/moderation/dm_relay.py diff --git a/bot/cogs/moderation/incidents.py b/bot/exts/moderation/incidents.py index 3605ab1d2..e49913552 100644 --- a/bot/cogs/moderation/incidents.py +++ b/bot/exts/moderation/incidents.py @@ -405,3 +405,8 @@ class Incidents(Cog):          """Pass `message` to `add_signals` if and only if it satisfies `is_incident`."""          if is_incident(message):              await add_signals(message) + + +def setup(bot: Bot) -> None: +    """Load the Incidents cog.""" +    bot.add_cog(Incidents(bot)) diff --git a/bot/exts/moderation/infraction/__init__.py b/bot/exts/moderation/infraction/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/bot/exts/moderation/infraction/__init__.py diff --git a/bot/cogs/moderation/scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 051f6c52c..cf48ef2ac 100644 --- a/bot/cogs/moderation/scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -13,11 +13,11 @@ from bot import constants  from bot.api import ResponseCodeError  from bot.bot import Bot  from bot.constants import Colours, STAFF_CHANNELS +from bot.exts.moderation.infraction import _utils +from bot.exts.moderation.infraction._utils import UserSnowflake +from bot.exts.moderation.modlog import ModLog  from bot.utils import time  from bot.utils.scheduling import Scheduler -from . import utils -from .modlog import ModLog -from .utils import UserSnowflake  log = logging.getLogger(__name__) @@ -56,7 +56,7 @@ class InfractionScheduler:      async def reapply_infraction(          self, -        infraction: utils.Infraction, +        infraction: _utils.Infraction,          apply_coro: t.Optional[t.Awaitable]      ) -> None:          """Reapply an infraction if it's still active or deactivate it if less than 60 sec left.""" @@ -80,13 +80,13 @@ class InfractionScheduler:      async def apply_infraction(          self,          ctx: Context, -        infraction: utils.Infraction, +        infraction: _utils.Infraction,          user: UserSnowflake,          action_coro: t.Optional[t.Awaitable] = None      ) -> None:          """Apply an infraction to the user, log the infraction, and optionally notify the user."""          infr_type = infraction["type"] -        icon = utils.INFRACTION_ICONS[infr_type][0] +        icon = _utils.INFRACTION_ICONS[infr_type][0]          reason = infraction["reason"]          expiry = time.format_infraction_with_duration(infraction["expires_at"])          id_ = infraction['id'] @@ -126,7 +126,7 @@ class InfractionScheduler:                  log.error(f"Failed to DM {user.id}: could not fetch user (status {e.status})")              else:                  # Accordingly display whether the user was successfully notified via DM. -                if await utils.notify_infraction(user, infr_type, expiry, reason, icon): +                if await _utils.notify_infraction(user, infr_type, expiry, reason, icon):                      dm_result = ":incoming_envelope: "                      dm_log_text = "\nDM: Sent" @@ -318,7 +318,7 @@ class InfractionScheduler:          # Send a log message to the mod log.          await self.mod_log.send_log_message( -            icon_url=utils.INFRACTION_ICONS[infr_type][1], +            icon_url=_utils.INFRACTION_ICONS[infr_type][1],              colour=Colours.soft_green,              title=f"Infraction {log_title}: {infr_type}",              thumbnail=user.avatar_url_as(static_format="png"), @@ -329,7 +329,7 @@ class InfractionScheduler:      async def deactivate_infraction(          self, -        infraction: utils.Infraction, +        infraction: _utils.Infraction,          send_log: bool = True      ) -> t.Dict[str, str]:          """ @@ -434,7 +434,7 @@ class InfractionScheduler:              log.trace(f"Sending deactivation mod log for infraction #{id_}.")              await self.mod_log.send_log_message( -                icon_url=utils.INFRACTION_ICONS[type_][1], +                icon_url=_utils.INFRACTION_ICONS[type_][1],                  colour=Colours.soft_green,                  title=f"Infraction {log_title}: {type_}",                  thumbnail=avatar, @@ -446,7 +446,7 @@ class InfractionScheduler:          return log_text      @abstractmethod -    async def _pardon_action(self, infraction: utils.Infraction) -> t.Optional[t.Dict[str, str]]: +    async def _pardon_action(self, infraction: _utils.Infraction) -> t.Optional[t.Dict[str, str]]:          """          Execute deactivation steps specific to the infraction's type and return a log dict. @@ -454,7 +454,7 @@ class InfractionScheduler:          """          raise NotImplementedError -    def schedule_expiration(self, infraction: utils.Infraction) -> None: +    def schedule_expiration(self, infraction: _utils.Infraction) -> None:          """          Marks an infraction expired after the delay from time of scheduling to time of expiration. diff --git a/bot/cogs/moderation/utils.py b/bot/exts/moderation/infraction/_utils.py index f21272102..f21272102 100644 --- a/bot/cogs/moderation/utils.py +++ b/bot/exts/moderation/infraction/_utils.py diff --git a/bot/cogs/moderation/infractions.py b/bot/exts/moderation/infraction/infractions.py index 8df642428..84ea47371 100644 --- a/bot/cogs/moderation/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -12,10 +12,10 @@ from bot.bot import Bot  from bot.constants import Event  from bot.converters import Expiry, FetchedMember  from bot.decorators import respect_role_hierarchy +from bot.exts.moderation.infraction import _utils +from bot.exts.moderation.infraction._scheduler import InfractionScheduler +from bot.exts.moderation.infraction._utils import UserSnowflake  from bot.utils.checks import with_role_check -from . import utils -from .scheduler import InfractionScheduler -from .utils import UserSnowflake  log = logging.getLogger(__name__) @@ -55,7 +55,7 @@ class Infractions(InfractionScheduler, commands.Cog):      @command()      async def warn(self, ctx: Context, user: Member, *, reason: t.Optional[str] = None) -> None:          """Warn a user for the given reason.""" -        infraction = await utils.post_infraction(ctx, user, "warning", reason, active=False) +        infraction = await _utils.post_infraction(ctx, user, "warning", reason, active=False)          if infraction is None:              return @@ -125,7 +125,7 @@ class Infractions(InfractionScheduler, commands.Cog):      @command(hidden=True)      async def note(self, ctx: Context, user: FetchedMember, *, reason: t.Optional[str] = None) -> None:          """Create a private note for a user with the given reason without notifying the user.""" -        infraction = await utils.post_infraction(ctx, user, "note", reason, hidden=True, active=False) +        infraction = await _utils.post_infraction(ctx, user, "note", reason, hidden=True, active=False)          if infraction is None:              return @@ -213,10 +213,10 @@ class Infractions(InfractionScheduler, commands.Cog):      async def apply_mute(self, ctx: Context, user: Member, reason: t.Optional[str], **kwargs) -> None:          """Apply a mute infraction with kwargs passed to `post_infraction`.""" -        if await utils.get_active_infraction(ctx, user, "mute"): +        if await _utils.get_active_infraction(ctx, user, "mute"):              return -        infraction = await utils.post_infraction(ctx, user, "mute", reason, active=True, **kwargs) +        infraction = await _utils.post_infraction(ctx, user, "mute", reason, active=True, **kwargs)          if infraction is None:              return @@ -233,7 +233,7 @@ class Infractions(InfractionScheduler, commands.Cog):      @respect_role_hierarchy()      async def apply_kick(self, ctx: Context, user: Member, reason: t.Optional[str], **kwargs) -> None:          """Apply a kick infraction with kwargs passed to `post_infraction`.""" -        infraction = await utils.post_infraction(ctx, user, "kick", reason, active=False, **kwargs) +        infraction = await _utils.post_infraction(ctx, user, "kick", reason, active=False, **kwargs)          if infraction is None:              return @@ -254,7 +254,7 @@ class Infractions(InfractionScheduler, commands.Cog):          """          # In the case of a permanent ban, we don't need get_active_infractions to tell us if one is active          is_temporary = kwargs.get("expires_at") is not None -        active_infraction = await utils.get_active_infraction(ctx, user, "ban", is_temporary) +        active_infraction = await _utils.get_active_infraction(ctx, user, "ban", is_temporary)          if active_infraction:              if is_temporary: @@ -269,7 +269,7 @@ class Infractions(InfractionScheduler, commands.Cog):              log.trace("Old tempban is being replaced by new permaban.")              await self.pardon_infraction(ctx, "ban", user, is_temporary) -        infraction = await utils.post_infraction(ctx, user, "ban", reason, active=True, **kwargs) +        infraction = await _utils.post_infraction(ctx, user, "ban", reason, active=True, **kwargs)          if infraction is None:              return @@ -309,11 +309,11 @@ class Infractions(InfractionScheduler, commands.Cog):              await user.remove_roles(self._muted_role, reason=reason)              # DM the user about the expiration. -            notified = await utils.notify_pardon( +            notified = await _utils.notify_pardon(                  user=user,                  title="You have been unmuted",                  content="You may now send messages in the server.", -                icon_url=utils.INFRACTION_ICONS["mute"][1] +                icon_url=_utils.INFRACTION_ICONS["mute"][1]              )              log_text["Member"] = f"{user.mention}(`{user.id}`)" @@ -339,7 +339,7 @@ class Infractions(InfractionScheduler, commands.Cog):          return log_text -    async def _pardon_action(self, infraction: utils.Infraction) -> t.Optional[t.Dict[str, str]]: +    async def _pardon_action(self, infraction: _utils.Infraction) -> t.Optional[t.Dict[str, str]]:          """          Execute deactivation steps specific to the infraction's type and return a log dict. @@ -368,3 +368,8 @@ class Infractions(InfractionScheduler, commands.Cog):              if discord.User in error.converters or discord.Member in error.converters:                  await ctx.send(str(error.errors[0]))                  error.handled = True + + +def setup(bot: Bot) -> None: +    """Load the Infractions cog.""" +    bot.add_cog(Infractions(bot)) diff --git a/bot/cogs/moderation/management.py b/bot/exts/moderation/infraction/management.py index 672bb0e9c..5875abd26 100644 --- a/bot/cogs/moderation/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -10,12 +10,12 @@ from discord.ext.commands import Context  from bot import constants  from bot.bot import Bot  from bot.converters import Expiry, InfractionSearchQuery, allowed_strings, proxy_user +from bot.exts.moderation.infraction import _utils +from bot.exts.moderation.infraction.infractions import Infractions +from bot.exts.moderation.modlog import ModLog  from bot.pagination import LinePaginator  from bot.utils import time  from bot.utils.checks import in_whitelist_check, with_role_check -from . import utils -from .infractions import Infractions -from .modlog import ModLog  log = logging.getLogger(__name__) @@ -220,7 +220,7 @@ class ModManagement(commands.Cog):          self,          ctx: Context,          embed: discord.Embed, -        infractions: t.Iterable[utils.Infraction] +        infractions: t.Iterable[_utils.Infraction]      ) -> None:          """Send a paginated embed of infractions for the specified user."""          if not infractions: @@ -241,7 +241,7 @@ class ModManagement(commands.Cog):              max_size=1000          ) -    def infraction_to_string(self, infraction: utils.Infraction) -> str: +    def infraction_to_string(self, infraction: _utils.Infraction) -> str:          """Convert the infraction object to a string representation."""          actor_id = infraction["actor"]          guild = self.bot.get_guild(constants.Guild.id) @@ -303,3 +303,8 @@ class ModManagement(commands.Cog):              if discord.User in error.converters:                  await ctx.send(str(error.errors[0]))                  error.handled = True + + +def setup(bot: Bot) -> None: +    """Load the ModManagement cog.""" +    bot.add_cog(ModManagement(bot)) diff --git a/bot/cogs/moderation/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 867de815a..a4e78c4d3 100644 --- a/bot/cogs/moderation/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -11,10 +11,10 @@ from discord.ext.commands import Cog, Context, command  from bot import constants  from bot.bot import Bot  from bot.converters import Expiry +from bot.exts.moderation.infraction import _utils +from bot.exts.moderation.infraction._scheduler import InfractionScheduler  from bot.utils.checks import with_role_check  from bot.utils.time import format_infraction -from . import utils -from .scheduler import InfractionScheduler  log = logging.getLogger(__name__)  NICKNAME_POLICY_URL = "https://pythondiscord.com/pages/rules/#nickname-policy" @@ -67,7 +67,7 @@ class Superstarify(InfractionScheduler, Cog):              reason=f"Superstarified member tried to escape the prison: {infraction['id']}"          ) -        notified = await utils.notify_infraction( +        notified = await _utils.notify_infraction(              user=after,              infr_type="Superstarify",              expires_at=format_infraction(infraction["expires_at"]), @@ -76,7 +76,7 @@ class Superstarify(InfractionScheduler, Cog):                  f"from **{before.display_name}** to **{after.display_name}**, but as you "                  "are currently in superstar-prison, you do not have permission to do so."              ), -            icon_url=utils.INFRACTION_ICONS["superstar"][0] +            icon_url=_utils.INFRACTION_ICONS["superstar"][0]          )          if not notified: @@ -130,12 +130,12 @@ class Superstarify(InfractionScheduler, Cog):          An optional reason can be provided. If no reason is given, the original name will be shown          in a generated reason.          """ -        if await utils.get_active_infraction(ctx, member, "superstar"): +        if await _utils.get_active_infraction(ctx, member, "superstar"):              return          # Post the infraction to the API          reason = reason or f"old nick: {member.display_name}" -        infraction = await utils.post_infraction(ctx, member, "superstar", reason, duration, active=True) +        infraction = await _utils.post_infraction(ctx, member, "superstar", reason, duration, active=True)          id_ = infraction["id"]          old_nick = member.display_name @@ -149,11 +149,11 @@ class Superstarify(InfractionScheduler, Cog):          self.schedule_expiration(infraction)          # Send a DM to the user to notify them of their new infraction. -        await utils.notify_infraction( +        await _utils.notify_infraction(              user=member,              infr_type="Superstarify",              expires_at=expiry_str, -            icon_url=utils.INFRACTION_ICONS["superstar"][0], +            icon_url=_utils.INFRACTION_ICONS["superstar"][0],              reason=f"Your nickname didn't comply with our [nickname policy]({NICKNAME_POLICY_URL})."          ) @@ -176,7 +176,7 @@ class Superstarify(InfractionScheduler, Cog):          # Log to the mod log channel.          log.trace(f"Sending apply mod log for superstar #{id_}.")          await self.mod_log.send_log_message( -            icon_url=utils.INFRACTION_ICONS["superstar"][0], +            icon_url=_utils.INFRACTION_ICONS["superstar"][0],              colour=Colour.gold(),              title="Member achieved superstardom",              thumbnail=member.avatar_url_as(static_format="png"), @@ -196,7 +196,7 @@ class Superstarify(InfractionScheduler, Cog):          """Remove the superstarify infraction and allow the user to change their nickname."""          await self.pardon_infraction(ctx, "superstar", member) -    async def _pardon_action(self, infraction: utils.Infraction) -> t.Optional[t.Dict[str, str]]: +    async def _pardon_action(self, infraction: _utils.Infraction) -> t.Optional[t.Dict[str, str]]:          """Pardon a superstar infraction and return a log dict."""          if infraction["type"] != "superstar":              return @@ -213,11 +213,11 @@ class Superstarify(InfractionScheduler, Cog):              return {}          # DM the user about the expiration. -        notified = await utils.notify_pardon( +        notified = await _utils.notify_pardon(              user=user,              title="You are no longer superstarified",              content="You may now change your nickname on the server.", -            icon_url=utils.INFRACTION_ICONS["superstar"][1] +            icon_url=_utils.INFRACTION_ICONS["superstar"][1]          )          return { @@ -237,3 +237,8 @@ class Superstarify(InfractionScheduler, Cog):      def cog_check(self, ctx: Context) -> bool:          """Only allow moderators to invoke the commands in this cog."""          return with_role_check(ctx, *constants.MODERATION_ROLES) + + +def setup(bot: Bot) -> None: +    """Load the Superstarify cog.""" +    bot.add_cog(Superstarify(bot)) diff --git a/bot/cogs/moderation/modlog.py b/bot/exts/moderation/modlog.py index 5f30d3744..b0d9b5b2b 100644 --- a/bot/cogs/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -834,3 +834,8 @@ class ModLog(Cog, name="ModLog"):              thumbnail=member.avatar_url_as(static_format="png"),              channel_id=Channels.voice_log          ) + + +def setup(bot: Bot) -> None: +    """Load the ModLog cog.""" +    bot.add_cog(ModLog(bot)) diff --git a/bot/cogs/moderation/silence.py b/bot/exts/moderation/silence.py index f8a6592bc..4af87c724 100644 --- a/bot/cogs/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -163,3 +163,8 @@ class Silence(commands.Cog):      def cog_check(self, ctx: Context) -> bool:          """Only allow moderators to invoke the commands in this cog."""          return with_role_check(ctx, *MODERATION_ROLES) + + +def setup(bot: Bot) -> None: +    """Load the Silence cog.""" +    bot.add_cog(Silence(bot)) diff --git a/bot/cogs/moderation/slowmode.py b/bot/exts/moderation/slowmode.py index 1d055afac..1d055afac 100644 --- a/bot/cogs/moderation/slowmode.py +++ b/bot/exts/moderation/slowmode.py diff --git a/bot/cogs/verification.py b/bot/exts/moderation/verification.py index 9ae92a228..53fa0730b 100644 --- a/bot/cogs/verification.py +++ b/bot/exts/moderation/verification.py @@ -11,8 +11,8 @@ from discord.utils import snowflake_time  from bot import constants  from bot.bot import Bot -from bot.cogs.moderation import ModLog  from bot.decorators import in_whitelist, with_role, without_role +from bot.exts.moderation.modlog import ModLog  from bot.utils.checks import InWhitelistCheckFailure, without_role_check  from bot.utils.redis_cache import RedisCache diff --git a/bot/exts/moderation/watchchannels/__init__.py b/bot/exts/moderation/watchchannels/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/bot/exts/moderation/watchchannels/__init__.py diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/exts/moderation/watchchannels/_watchchannel.py index a58b604c0..7118dee02 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/exts/moderation/watchchannels/_watchchannel.py @@ -14,10 +14,10 @@ from discord.ext.commands import Cog, Context  from bot.api import ResponseCodeError  from bot.bot import Bot -from bot.cogs.moderation import ModLog -from bot.cogs.token_remover import TokenRemover -from bot.cogs.webhook_remover import WEBHOOK_URL_RE  from bot.constants import BigBrother as BigBrotherConfig, Guild as GuildConfig, Icons +from bot.exts.filters.token_remover import TokenRemover +from bot.exts.filters.webhook_remover import WEBHOOK_URL_RE +from bot.exts.moderation.modlog import ModLog  from bot.pagination import LinePaginator  from bot.utils import CogABCMeta, messages  from bot.utils.time import time_since diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/exts/moderation/watchchannels/bigbrother.py index 11ab8917a..d7127b5c4 100644 --- a/bot/cogs/watchchannels/bigbrother.py +++ b/bot/exts/moderation/watchchannels/bigbrother.py @@ -5,11 +5,11 @@ from collections import ChainMap  from discord.ext.commands import Cog, Context, group  from bot.bot import Bot -from bot.cogs.moderation.utils import post_infraction  from bot.constants import Channels, MODERATION_ROLES, Webhooks  from bot.converters import FetchedMember  from bot.decorators import with_role -from .watchchannel import WatchChannel +from bot.exts.moderation.infraction._utils import post_infraction +from bot.exts.moderation.watchchannels._watchchannel import WatchChannel  log = logging.getLogger(__name__) @@ -163,3 +163,8 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"):              message = ":x: The specified user is currently not being watched."          await ctx.send(message) + + +def setup(bot: Bot) -> None: +    """Load the BigBrother cog.""" +    bot.add_cog(BigBrother(bot)) diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/exts/moderation/watchchannels/talentpool.py index 76d6fe9bd..3724e94e6 100644 --- a/bot/cogs/watchchannels/talentpool.py +++ b/bot/exts/moderation/watchchannels/talentpool.py @@ -11,9 +11,9 @@ from bot.bot import Bot  from bot.constants import Channels, Guild, MODERATION_ROLES, STAFF_ROLES, Webhooks  from bot.converters import FetchedMember  from bot.decorators import with_role +from bot.exts.moderation.watchchannels._watchchannel import WatchChannel  from bot.pagination import LinePaginator  from bot.utils import time -from .watchchannel import WatchChannel  log = logging.getLogger(__name__) @@ -278,3 +278,8 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):              )          return lines.strip() + + +def setup(bot: Bot) -> None: +    """Load the TalentPool cog.""" +    bot.add_cog(TalentPool(bot)) diff --git a/bot/exts/utils/__init__.py b/bot/exts/utils/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/bot/exts/utils/__init__.py diff --git a/bot/cogs/bot.py b/bot/exts/utils/bot.py index ddd1cef8d..66f340a99 100644 --- a/bot/cogs/bot.py +++ b/bot/exts/utils/bot.py @@ -8,10 +8,10 @@ from discord import Embed, Message, RawMessageUpdateEvent, TextChannel  from discord.ext.commands import Cog, Context, command, group  from bot.bot import Bot -from bot.cogs.token_remover import TokenRemover -from bot.cogs.webhook_remover import WEBHOOK_URL_RE  from bot.constants import Categories, Channels, DEBUG_MODE, Guild, MODERATION_ROLES, Roles, URLs  from bot.decorators import with_role +from bot.exts.filters.token_remover import TokenRemover +from bot.exts.filters.webhook_remover import WEBHOOK_URL_RE  from bot.utils.messages import wait_for_deletion  log = logging.getLogger(__name__) diff --git a/bot/cogs/clean.py b/bot/exts/utils/clean.py index f436e531a..d9a7aafe1 100644 --- a/bot/cogs/clean.py +++ b/bot/exts/utils/clean.py @@ -8,11 +8,11 @@ from discord.ext import commands  from discord.ext.commands import Cog, Context, group  from bot.bot import Bot -from bot.cogs.moderation import ModLog  from bot.constants import (      Channels, CleanMessages, Colours, Event, Icons, MODERATION_ROLES, NEGATIVE_REPLIES  )  from bot.decorators import with_role +from bot.exts.moderation.modlog import ModLog  log = logging.getLogger(__name__) diff --git a/bot/cogs/eval.py b/bot/exts/utils/eval.py index 23e5998d8..23e5998d8 100644 --- a/bot/cogs/eval.py +++ b/bot/exts/utils/eval.py diff --git a/bot/cogs/extensions.py b/bot/exts/utils/extensions.py index 396e406b0..123f356e8 100644 --- a/bot/cogs/extensions.py +++ b/bot/exts/utils/extensions.py @@ -2,25 +2,23 @@ import functools  import logging  import typing as t  from enum import Enum -from pkgutil import iter_modules  from discord import Colour, Embed  from discord.ext import commands  from discord.ext.commands import Context, group +from bot import exts  from bot.bot import Bot  from bot.constants import Emojis, MODERATION_ROLES, Roles, URLs  from bot.pagination import LinePaginator  from bot.utils.checks import with_role_check +from bot.utils.extensions import EXTENSIONS, unqualify  log = logging.getLogger(__name__) -UNLOAD_BLACKLIST = {"bot.cogs.extensions", "bot.cogs.modlog"} -EXTENSIONS = frozenset( -    ext.name -    for ext in iter_modules(("bot/cogs",), "bot.cogs.") -    if ext.name[-1] != "_" -) + +UNLOAD_BLACKLIST = {f"{exts.__name__}.utils.extensions", f"{exts.__name__}.moderation.modlog"} +BASE_PATH_LEN = len(exts.__name__.split("."))  class Action(Enum): @@ -47,11 +45,25 @@ class Extension(commands.Converter):          argument = argument.lower() -        if "." not in argument: -            argument = f"bot.cogs.{argument}" -          if argument in EXTENSIONS:              return argument +        elif (qualified_arg := f"{exts.__name__}.{argument}") in EXTENSIONS: +            return qualified_arg + +        matches = [] +        for ext in EXTENSIONS: +            if argument == unqualify(ext): +                matches.append(ext) + +        if len(matches) > 1: +            matches.sort() +            names = "\n".join(matches) +            raise commands.BadArgument( +                f":x: `{argument}` is an ambiguous extension name. " +                f"Please use one of the following fully-qualified names.```\n{names}```" +            ) +        elif matches: +            return matches[0]          else:              raise commands.BadArgument(f":x: Could not find the extension `{argument}`.") @@ -139,27 +151,44 @@ class Extensions(commands.Cog):          Grey indicates that the extension is unloaded.          Green indicates that the extension is currently loaded.          """ -        embed = Embed() -        lines = [] - -        embed.colour = Colour.blurple() +        embed = Embed(colour=Colour.blurple())          embed.set_author(              name="Extensions List",              url=URLs.github_bot_repo,              icon_url=URLs.bot_avatar          ) -        for ext in sorted(list(EXTENSIONS)): +        lines = [] +        categories = self.group_extension_statuses() +        for category, extensions in sorted(categories.items()): +            # Treat each category as a single line by concatenating everything. +            # This ensures the paginator will not cut off a page in the middle of a category. +            category = category.replace("_", " ").title() +            extensions = "\n".join(sorted(extensions)) +            lines.append(f"**{category}**\n{extensions}\n") + +        log.debug(f"{ctx.author} requested a list of all cogs. Returning a paginated list.") +        await LinePaginator.paginate(lines, ctx, embed, scale_to_size=700, empty=False) + +    def group_extension_statuses(self) -> t.Mapping[str, str]: +        """Return a mapping of extension names and statuses to their categories.""" +        categories = {} + +        for ext in EXTENSIONS:              if ext in self.bot.extensions:                  status = Emojis.status_online              else:                  status = Emojis.status_offline -            ext = ext.rsplit(".", 1)[1] -            lines.append(f"{status}  {ext}") +            path = ext.split(".") +            if len(path) > BASE_PATH_LEN + 1: +                category = " - ".join(path[BASE_PATH_LEN:-1]) +            else: +                category = "uncategorised" -        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) +            categories.setdefault(category, []).append(f"{status}  {path[-1]}") + +        return categories      def batch_manage(self, action: Action, *extensions: str) -> str:          """ diff --git a/bot/cogs/jams.py b/bot/exts/utils/jams.py index b3102db2f..b3102db2f 100644 --- a/bot/cogs/jams.py +++ b/bot/exts/utils/jams.py diff --git a/bot/cogs/reminders.py b/bot/exts/utils/reminders.py index 08bce2153..08bce2153 100644 --- a/bot/cogs/reminders.py +++ b/bot/exts/utils/reminders.py diff --git a/bot/cogs/snekbox.py b/bot/exts/utils/snekbox.py index 03bf454ac..03bf454ac 100644 --- a/bot/cogs/snekbox.py +++ b/bot/exts/utils/snekbox.py diff --git a/bot/cogs/utils.py b/bot/exts/utils/utils.py index d96abbd5a..d96abbd5a 100644 --- a/bot/cogs/utils.py +++ b/bot/exts/utils/utils.py diff --git a/bot/utils/extensions.py b/bot/utils/extensions.py new file mode 100644 index 000000000..50350ea8d --- /dev/null +++ b/bot/utils/extensions.py @@ -0,0 +1,34 @@ +import importlib +import inspect +import pkgutil +from typing import Iterator, NoReturn + +from bot import exts + + +def unqualify(name: str) -> str: +    """Return an unqualified name given a qualified module/package `name`.""" +    return name.rsplit(".", maxsplit=1)[-1] + + +def walk_extensions() -> Iterator[str]: +    """Yield extension names from the bot.exts subpackage.""" + +    def on_error(name: str) -> NoReturn: +        raise ImportError(name=name)  # pragma: no cover + +    for module in pkgutil.walk_packages(exts.__path__, f"{exts.__name__}.", onerror=on_error): +        if unqualify(module.name).startswith("_"): +            # Ignore module/package names starting with an underscore. +            continue + +        if module.ispkg: +            imported = importlib.import_module(module.name) +            if not inspect.isfunction(getattr(imported, "setup", None)): +                # If it lacks a setup function, it's not an extension. +                continue + +        yield module.name + + +EXTENSIONS = frozenset(walk_extensions()) diff --git a/tests/bot/exts/__init__.py b/tests/bot/exts/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/bot/exts/__init__.py diff --git a/tests/bot/exts/backend/__init__.py b/tests/bot/exts/backend/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/bot/exts/backend/__init__.py diff --git a/tests/bot/exts/backend/sync/__init__.py b/tests/bot/exts/backend/sync/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/bot/exts/backend/sync/__init__.py diff --git a/tests/bot/cogs/sync/test_base.py b/tests/bot/exts/backend/sync/test_base.py index 70aea2bab..886c243cf 100644 --- a/tests/bot/cogs/sync/test_base.py +++ b/tests/bot/exts/backend/sync/test_base.py @@ -6,7 +6,7 @@ import discord  from bot import constants  from bot.api import ResponseCodeError -from bot.cogs.sync.syncers import Syncer, _Diff +from bot.exts.backend.sync._syncers import Syncer, _Diff  from tests import helpers diff --git a/tests/bot/cogs/sync/test_cog.py b/tests/bot/exts/backend/sync/test_cog.py index 120bc991d..1b89564f2 100644 --- a/tests/bot/cogs/sync/test_cog.py +++ b/tests/bot/exts/backend/sync/test_cog.py @@ -5,8 +5,9 @@ import discord  from bot import constants  from bot.api import ResponseCodeError -from bot.cogs import sync -from bot.cogs.sync.syncers import Syncer +from bot.exts.backend import sync +from bot.exts.backend.sync._cog import Sync +from bot.exts.backend.sync._syncers import Syncer  from tests import helpers  from tests.base import CommandTestCase @@ -29,19 +30,19 @@ class SyncCogTestCase(unittest.IsolatedAsyncioTestCase):          self.bot = helpers.MockBot()          self.role_syncer_patcher = mock.patch( -            "bot.cogs.sync.syncers.RoleSyncer", +            "bot.exts.backend.sync._syncers.RoleSyncer",              autospec=Syncer,              spec_set=True          )          self.user_syncer_patcher = mock.patch( -            "bot.cogs.sync.syncers.UserSyncer", +            "bot.exts.backend.sync._syncers.UserSyncer",              autospec=Syncer,              spec_set=True          )          self.RoleSyncer = self.role_syncer_patcher.start()          self.UserSyncer = self.user_syncer_patcher.start() -        self.cog = sync.Sync(self.bot) +        self.cog = Sync(self.bot)      def tearDown(self):          self.role_syncer_patcher.stop() @@ -59,7 +60,7 @@ class SyncCogTestCase(unittest.IsolatedAsyncioTestCase):  class SyncCogTests(SyncCogTestCase):      """Tests for the Sync cog.""" -    @mock.patch.object(sync.Sync, "sync_guild", new_callable=mock.MagicMock) +    @mock.patch.object(Sync, "sync_guild", new_callable=mock.MagicMock)      def test_sync_cog_init(self, sync_guild):          """Should instantiate syncers and run a sync for the guild."""          # Reset because a Sync cog was already instantiated in setUp. @@ -70,7 +71,7 @@ class SyncCogTests(SyncCogTestCase):          mock_sync_guild_coro = mock.MagicMock()          sync_guild.return_value = mock_sync_guild_coro -        sync.Sync(self.bot) +        Sync(self.bot)          self.RoleSyncer.assert_called_once_with(self.bot)          self.UserSyncer.assert_called_once_with(self.bot) @@ -131,7 +132,7 @@ class SyncCogListenerTests(SyncCogTestCase):          super().setUp()          self.cog.patch_user = mock.AsyncMock(spec_set=self.cog.patch_user) -        self.guild_id_patcher = mock.patch("bot.cogs.sync.cog.constants.Guild.id", 5) +        self.guild_id_patcher = mock.patch("bot.exts.backend.sync._cog.constants.Guild.id", 5)          self.guild_id = self.guild_id_patcher.start()          self.guild = helpers.MockGuild(id=self.guild_id) diff --git a/tests/bot/cogs/sync/test_roles.py b/tests/bot/exts/backend/sync/test_roles.py index 79eee98f4..7b9f40cad 100644 --- a/tests/bot/cogs/sync/test_roles.py +++ b/tests/bot/exts/backend/sync/test_roles.py @@ -3,7 +3,7 @@ from unittest import mock  import discord -from bot.cogs.sync.syncers import RoleSyncer, _Diff, _Role +from bot.exts.backend.sync._syncers import RoleSyncer, _Diff, _Role  from tests import helpers diff --git a/tests/bot/cogs/sync/test_users.py b/tests/bot/exts/backend/sync/test_users.py index 002a947ad..c0a1da35c 100644 --- a/tests/bot/cogs/sync/test_users.py +++ b/tests/bot/exts/backend/sync/test_users.py @@ -1,7 +1,7 @@  import unittest  from unittest import mock -from bot.cogs.sync.syncers import UserSyncer, _Diff, _User +from bot.exts.backend.sync._syncers import UserSyncer, _Diff, _User  from tests import helpers diff --git a/tests/bot/cogs/test_logging.py b/tests/bot/exts/backend/test_logging.py index 8a18fdcd6..466f207d9 100644 --- a/tests/bot/cogs/test_logging.py +++ b/tests/bot/exts/backend/test_logging.py @@ -2,7 +2,7 @@ import unittest  from unittest.mock import patch  from bot import constants -from bot.cogs.logging import Logging +from bot.exts.backend.logging import Logging  from tests.helpers import MockBot, MockTextChannel @@ -14,7 +14,7 @@ class LoggingTests(unittest.IsolatedAsyncioTestCase):          self.cog = Logging(self.bot)          self.dev_log = MockTextChannel(id=1234, name="dev-log") -    @patch("bot.cogs.logging.DEBUG_MODE", False) +    @patch("bot.exts.backend.logging.DEBUG_MODE", False)      async def test_debug_mode_false(self):          """Should send connected message to dev-log."""          self.bot.get_channel.return_value = self.dev_log @@ -24,7 +24,7 @@ class LoggingTests(unittest.IsolatedAsyncioTestCase):          self.bot.get_channel.assert_called_once_with(constants.Channels.dev_log)          self.dev_log.send.assert_awaited_once() -    @patch("bot.cogs.logging.DEBUG_MODE", True) +    @patch("bot.exts.backend.logging.DEBUG_MODE", True)      async def test_debug_mode_true(self):          """Should not send anything to dev-log."""          await self.cog.startup_greeting() diff --git a/tests/bot/exts/filters/__init__.py b/tests/bot/exts/filters/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/bot/exts/filters/__init__.py diff --git a/tests/bot/cogs/test_antimalware.py b/tests/bot/exts/filters/test_antimalware.py index f50c0492d..3393c6cdc 100644 --- a/tests/bot/cogs/test_antimalware.py +++ b/tests/bot/exts/filters/test_antimalware.py @@ -3,8 +3,8 @@ from unittest.mock import AsyncMock, Mock  from discord import NotFound -from bot.cogs import antimalware  from bot.constants import Channels, STAFF_ROLES +from bot.exts.filters import antimalware  from tests.helpers import MockAttachment, MockBot, MockMessage, MockRole diff --git a/tests/bot/cogs/test_antispam.py b/tests/bot/exts/filters/test_antispam.py index ce5472c71..6a0e4fded 100644 --- a/tests/bot/cogs/test_antispam.py +++ b/tests/bot/exts/filters/test_antispam.py @@ -1,6 +1,6 @@  import unittest -from bot.cogs import antispam +from bot.exts.filters import antispam  class AntispamConfigurationValidationTests(unittest.TestCase): diff --git a/tests/bot/cogs/test_security.py b/tests/bot/exts/filters/test_security.py index 9d1a62f7e..c0c3baa42 100644 --- a/tests/bot/cogs/test_security.py +++ b/tests/bot/exts/filters/test_security.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock  from discord.ext.commands import NoPrivateMessage -from bot.cogs import security +from bot.exts.filters import security  from tests.helpers import MockBot, MockContext diff --git a/tests/bot/cogs/test_token_remover.py b/tests/bot/exts/filters/test_token_remover.py index 3349caa73..a0ff8a877 100644 --- a/tests/bot/cogs/test_token_remover.py +++ b/tests/bot/exts/filters/test_token_remover.py @@ -6,9 +6,9 @@ from unittest.mock import MagicMock  from discord import Colour, NotFound  from bot import constants -from bot.cogs import token_remover -from bot.cogs.moderation import ModLog -from bot.cogs.token_remover import Token, TokenRemover +from bot.exts.filters import token_remover +from bot.exts.filters.token_remover import Token, TokenRemover +from bot.exts.moderation.modlog import ModLog  from tests.helpers import MockBot, MockMessage, autospec @@ -132,7 +132,7 @@ class TokenRemoverTests(unittest.IsolatedAsyncioTestCase):              await cog.on_message(msg)              find_token_in_message.assert_not_called() -    @autospec("bot.cogs.token_remover", "TOKEN_RE") +    @autospec("bot.exts.filters.token_remover", "TOKEN_RE")      def test_find_token_no_matches(self, token_re):          """None should be returned if the regex matches no tokens in a message."""          token_re.finditer.return_value = () @@ -143,8 +143,8 @@ class TokenRemoverTests(unittest.IsolatedAsyncioTestCase):          token_re.finditer.assert_called_once_with(self.msg.content)      @autospec(TokenRemover, "is_valid_user_id", "is_valid_timestamp") -    @autospec("bot.cogs.token_remover", "Token") -    @autospec("bot.cogs.token_remover", "TOKEN_RE") +    @autospec("bot.exts.filters.token_remover", "Token") +    @autospec("bot.exts.filters.token_remover", "TOKEN_RE")      def test_find_token_valid_match(self, token_re, token_cls, is_valid_id, is_valid_timestamp):          """The first match with a valid user ID and timestamp should be returned as a `Token`."""          matches = [ @@ -167,8 +167,8 @@ class TokenRemoverTests(unittest.IsolatedAsyncioTestCase):          token_re.finditer.assert_called_once_with(self.msg.content)      @autospec(TokenRemover, "is_valid_user_id", "is_valid_timestamp") -    @autospec("bot.cogs.token_remover", "Token") -    @autospec("bot.cogs.token_remover", "TOKEN_RE") +    @autospec("bot.exts.filters.token_remover", "Token") +    @autospec("bot.exts.filters.token_remover", "TOKEN_RE")      def test_find_token_invalid_matches(self, token_re, token_cls, is_valid_id, is_valid_timestamp):          """None should be returned if no matches have valid user IDs or timestamps."""          token_re.finditer.return_value = [mock.create_autospec(Match, spec_set=True, instance=True)] @@ -230,7 +230,7 @@ class TokenRemoverTests(unittest.IsolatedAsyncioTestCase):          results = [match[0] for match in results]          self.assertCountEqual((token_1, token_2), results) -    @autospec("bot.cogs.token_remover", "LOG_MESSAGE") +    @autospec("bot.exts.filters.token_remover", "LOG_MESSAGE")      def test_format_log_message(self, log_message):          """Should correctly format the log message with info from the message and token."""          token = Token("NDY3MjIzMjMwNjUwNzc3NjQx", "XsySD_", "s45jqDV_Iisn-symw0yDRrk_jf4") @@ -249,7 +249,7 @@ class TokenRemoverTests(unittest.IsolatedAsyncioTestCase):          )      @mock.patch.object(TokenRemover, "mod_log", new_callable=mock.PropertyMock) -    @autospec("bot.cogs.token_remover", "log") +    @autospec("bot.exts.filters.token_remover", "log")      @autospec(TokenRemover, "format_log_message")      async def test_take_action(self, format_log_message, logger, mod_log_property):          """Should delete the message and send a mod log.""" @@ -299,7 +299,7 @@ class TokenRemoverTests(unittest.IsolatedAsyncioTestCase):  class TokenRemoverExtensionTests(unittest.TestCase):      """Tests for the token_remover extension.""" -    @autospec("bot.cogs.token_remover", "TokenRemover") +    @autospec("bot.exts.filters.token_remover", "TokenRemover")      def test_extension_setup(self, cog):          """The TokenRemover cog should be added."""          bot = MockBot() diff --git a/tests/bot/exts/fun/__init__.py b/tests/bot/exts/fun/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/bot/exts/fun/__init__.py diff --git a/tests/bot/cogs/test_duck_pond.py b/tests/bot/exts/fun/test_duck_pond.py index cfe10aebf..704b08066 100644 --- a/tests/bot/cogs/test_duck_pond.py +++ b/tests/bot/exts/fun/test_duck_pond.py @@ -7,11 +7,11 @@ from unittest.mock import AsyncMock, MagicMock, patch  import discord  from bot import constants -from bot.cogs import duck_pond +from bot.exts.fun import duck_pond  from tests import base  from tests import helpers -MODULE_PATH = "bot.cogs.duck_pond" +MODULE_PATH = "bot.exts.fun.duck_pond"  class DuckPondTests(base.LoggingTestsMixin, unittest.IsolatedAsyncioTestCase): @@ -63,7 +63,7 @@ class DuckPondTests(base.LoggingTestsMixin, unittest.IsolatedAsyncioTestCase):          self.bot.fetch_webhook.side_effect = discord.HTTPException(response=MagicMock(), message="Not found.")          self.cog.webhook_id = 1 -        log = logging.getLogger('bot.cogs.duck_pond') +        log = logging.getLogger(MODULE_PATH)          with self.assertLogs(logger=log, level=logging.ERROR) as log_watcher:              asyncio.run(self.cog.fetch_webhook()) @@ -282,7 +282,7 @@ class DuckPondTests(base.LoggingTestsMixin, unittest.IsolatedAsyncioTestCase):          side_effects = (discord.errors.Forbidden(MagicMock(), ""), discord.errors.NotFound(MagicMock(), ""))          self.cog.webhook = helpers.MockAsyncWebhook() -        log = logging.getLogger("bot.cogs.duck_pond") +        log = logging.getLogger(MODULE_PATH)          for side_effect in side_effects:  # pragma: no cover              send_attachments.side_effect = side_effect @@ -300,7 +300,7 @@ class DuckPondTests(base.LoggingTestsMixin, unittest.IsolatedAsyncioTestCase):          message = helpers.MockMessage(clean_content="message", attachments=["attachment"])          self.cog.webhook = helpers.MockAsyncWebhook() -        log = logging.getLogger("bot.cogs.duck_pond") +        log = logging.getLogger(MODULE_PATH)          side_effect = discord.HTTPException(MagicMock(), "")          send_attachments.side_effect = side_effect diff --git a/tests/bot/exts/info/__init__.py b/tests/bot/exts/info/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/bot/exts/info/__init__.py diff --git a/tests/bot/cogs/test_information.py b/tests/bot/exts/info/test_information.py index 77b0ddf17..ba8d5d608 100644 --- a/tests/bot/cogs/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -6,11 +6,11 @@ import unittest.mock  import discord  from bot import constants -from bot.cogs import information +from bot.exts.info import information  from bot.utils.checks import InWhitelistCheckFailure  from tests import helpers -COG_PATH = "bot.cogs.information.Information" +COG_PATH = "bot.exts.info.information.Information"  class InformationCogTests(unittest.TestCase): @@ -97,7 +97,7 @@ class InformationCogTests(unittest.TestCase):          self.assertEqual(admin_embed.title, "Admins info")          self.assertEqual(admin_embed.colour, discord.Colour.red()) -    @unittest.mock.patch('bot.cogs.information.time_since') +    @unittest.mock.patch('bot.exts.info.information.time_since')      def test_server_info_command(self, time_since_patch):          time_since_patch.return_value = '2 days ago' @@ -339,8 +339,8 @@ class UserInfractionHelperMethodTests(unittest.TestCase):          self._method_subtests(self.cog.user_nomination_counts, test_values, header) [email protected]("bot.cogs.information.time_since", new=unittest.mock.MagicMock(return_value="1 year ago")) [email protected]("bot.cogs.information.constants.MODERATION_CHANNELS", new=[50]) [email protected]("bot.exts.info.information.time_since", new=unittest.mock.MagicMock(return_value="1 year ago")) [email protected]("bot.exts.info.information.constants.MODERATION_CHANNELS", new=[50])  class UserEmbedTests(unittest.TestCase):      """Tests for the creation of the `!user` embed.""" @@ -515,7 +515,7 @@ class UserEmbedTests(unittest.TestCase):          self.assertEqual(embed.thumbnail.url, "avatar url") [email protected]("bot.cogs.information.constants") [email protected]("bot.exts.info.information.constants")  class UserCommandTests(unittest.TestCase):      """Tests for the `!user` command.""" @@ -554,7 +554,7 @@ class UserCommandTests(unittest.TestCase):          with self.assertRaises(InWhitelistCheckFailure, msg=msg):              asyncio.run(self.cog.user_info.callback(self.cog, ctx)) -    @unittest.mock.patch("bot.cogs.information.Information.create_user_embed", new_callable=unittest.mock.AsyncMock) +    @unittest.mock.patch("bot.exts.info.information.Information.create_user_embed")      def test_regular_user_may_use_command_in_bot_commands_channel(self, create_embed, constants):          """A regular user should be allowed to use `!user` targeting themselves in bot-commands."""          constants.STAFF_ROLES = [self.moderator_role.id] @@ -567,7 +567,7 @@ class UserCommandTests(unittest.TestCase):          create_embed.assert_called_once_with(ctx, self.author)          ctx.send.assert_called_once() -    @unittest.mock.patch("bot.cogs.information.Information.create_user_embed", new_callable=unittest.mock.AsyncMock) +    @unittest.mock.patch("bot.exts.info.information.Information.create_user_embed")      def test_regular_user_can_explicitly_target_themselves(self, create_embed, constants):          """A user should target itself with `!user` when a `user` argument was not provided."""          constants.STAFF_ROLES = [self.moderator_role.id] @@ -580,7 +580,7 @@ class UserCommandTests(unittest.TestCase):          create_embed.assert_called_once_with(ctx, self.author)          ctx.send.assert_called_once() -    @unittest.mock.patch("bot.cogs.information.Information.create_user_embed", new_callable=unittest.mock.AsyncMock) +    @unittest.mock.patch("bot.exts.info.information.Information.create_user_embed")      def test_staff_members_can_bypass_channel_restriction(self, create_embed, constants):          """Staff members should be able to bypass the bot-commands channel restriction."""          constants.STAFF_ROLES = [self.moderator_role.id] @@ -593,7 +593,7 @@ class UserCommandTests(unittest.TestCase):          create_embed.assert_called_once_with(ctx, self.moderator)          ctx.send.assert_called_once() -    @unittest.mock.patch("bot.cogs.information.Information.create_user_embed", new_callable=unittest.mock.AsyncMock) +    @unittest.mock.patch("bot.exts.info.information.Information.create_user_embed")      def test_moderators_can_target_another_member(self, create_embed, constants):          """A moderator should be able to use `!user` targeting another user."""          constants.MODERATION_ROLES = [self.moderator_role.id] diff --git a/tests/bot/exts/moderation/__init__.py b/tests/bot/exts/moderation/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/bot/exts/moderation/__init__.py diff --git a/tests/bot/exts/moderation/infraction/__init__.py b/tests/bot/exts/moderation/infraction/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/bot/exts/moderation/infraction/__init__.py diff --git a/tests/bot/cogs/moderation/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index da4e92ccc..be1b649e1 100644 --- a/tests/bot/cogs/moderation/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -2,7 +2,7 @@ import textwrap  import unittest  from unittest.mock import AsyncMock, Mock, patch -from bot.cogs.moderation.infractions import Infractions +from bot.exts.moderation.infraction.infractions import Infractions  from tests.helpers import MockBot, MockContext, MockGuild, MockMember, MockRole @@ -17,8 +17,8 @@ class TruncationTests(unittest.IsolatedAsyncioTestCase):          self.guild = MockGuild(id=4567)          self.ctx = MockContext(bot=self.bot, author=self.user, guild=self.guild) -    @patch("bot.cogs.moderation.utils.get_active_infraction") -    @patch("bot.cogs.moderation.utils.post_infraction") +    @patch("bot.exts.moderation.infraction._utils.get_active_infraction") +    @patch("bot.exts.moderation.infraction._utils.post_infraction")      async def test_apply_ban_reason_truncation(self, post_infraction_mock, get_active_mock):          """Should truncate reason for `ctx.guild.ban`."""          get_active_mock.return_value = None @@ -39,7 +39,7 @@ class TruncationTests(unittest.IsolatedAsyncioTestCase):              self.ctx, {"foo": "bar"}, self.target, self.ctx.guild.ban.return_value          ) -    @patch("bot.cogs.moderation.utils.post_infraction") +    @patch("bot.exts.moderation.infraction._utils.post_infraction")      async def test_apply_kick_reason_truncation(self, post_infraction_mock):          """Should truncate reason for `Member.kick`."""          post_infraction_mock.return_value = {"foo": "bar"} diff --git a/tests/bot/cogs/moderation/test_incidents.py b/tests/bot/exts/moderation/test_incidents.py index 435a1cd51..cbf7f7bcf 100644 --- a/tests/bot/cogs/moderation/test_incidents.py +++ b/tests/bot/exts/moderation/test_incidents.py @@ -8,8 +8,8 @@ from unittest.mock import AsyncMock, MagicMock, call, patch  import aiohttp  import discord -from bot.cogs.moderation import Incidents, incidents  from bot.constants import Colours +from bot.exts.moderation import incidents  from tests.helpers import (      MockAsyncWebhook,      MockAttachment, @@ -130,7 +130,7 @@ class TestMakeEmbed(unittest.IsolatedAsyncioTestCase):          incident = MockMessage(content="this is an incident", attachments=[attachment])          # Patch `download_file` to return our `file` -        with patch("bot.cogs.moderation.incidents.download_file", AsyncMock(return_value=file)): +        with patch("bot.exts.moderation.incidents.download_file", AsyncMock(return_value=file)):              embed, returned_file = await incidents.make_embed(incident, incidents.Signal.ACTIONED, MockMember())          self.assertIs(file, returned_file) @@ -142,7 +142,7 @@ class TestMakeEmbed(unittest.IsolatedAsyncioTestCase):          incident = MockMessage(content="this is an incident", attachments=[attachment])          # Patch `download_file` to return None as if the download failed -        with patch("bot.cogs.moderation.incidents.download_file", AsyncMock(return_value=None)): +        with patch("bot.exts.moderation.incidents.download_file", AsyncMock(return_value=None)):              embed, returned_file = await incidents.make_embed(incident, incidents.Signal.ACTIONED, MockMember())          self.assertIsNone(returned_file) @@ -215,7 +215,7 @@ class TestOwnReactions(unittest.TestCase):          self.assertSetEqual(incidents.own_reactions(message), {"A", "B"}) -@patch("bot.cogs.moderation.incidents.ALL_SIGNALS", {"A", "B"}) +@patch("bot.exts.moderation.incidents.ALL_SIGNALS", {"A", "B"})  class TestHasSignals(unittest.TestCase):      """      Assertions for the `has_signals` function. @@ -229,7 +229,7 @@ class TestHasSignals(unittest.TestCase):          message = MockMessage()          own_reactions = MagicMock(return_value={"A", "B"}) -        with patch("bot.cogs.moderation.incidents.own_reactions", own_reactions): +        with patch("bot.exts.moderation.incidents.own_reactions", own_reactions):              self.assertTrue(incidents.has_signals(message))      def test_has_signals_false(self): @@ -237,11 +237,11 @@ class TestHasSignals(unittest.TestCase):          message = MockMessage()          own_reactions = MagicMock(return_value={"A", "C"}) -        with patch("bot.cogs.moderation.incidents.own_reactions", own_reactions): +        with patch("bot.exts.moderation.incidents.own_reactions", own_reactions):              self.assertFalse(incidents.has_signals(message)) -@patch("bot.cogs.moderation.incidents.Signal", MockSignal) +@patch("bot.exts.moderation.incidents.Signal", MockSignal)  class TestAddSignals(unittest.IsolatedAsyncioTestCase):      """      Assertions for the `add_signals` coroutine. @@ -255,19 +255,19 @@ class TestAddSignals(unittest.IsolatedAsyncioTestCase):          """Prepare a mock incident message for tests to use."""          self.incident = MockMessage() -    @patch("bot.cogs.moderation.incidents.own_reactions", MagicMock(return_value=set())) +    @patch("bot.exts.moderation.incidents.own_reactions", MagicMock(return_value=set()))      async def test_add_signals_missing(self):          """All emoji are added when none are present."""          await incidents.add_signals(self.incident)          self.incident.add_reaction.assert_has_calls([call("A"), call("B")]) -    @patch("bot.cogs.moderation.incidents.own_reactions", MagicMock(return_value={"A"})) +    @patch("bot.exts.moderation.incidents.own_reactions", MagicMock(return_value={"A"}))      async def test_add_signals_partial(self):          """Only missing emoji are added when some are present."""          await incidents.add_signals(self.incident)          self.incident.add_reaction.assert_has_calls([call("B")]) -    @patch("bot.cogs.moderation.incidents.own_reactions", MagicMock(return_value={"A", "B"})) +    @patch("bot.exts.moderation.incidents.own_reactions", MagicMock(return_value={"A", "B"}))      async def test_add_signals_present(self):          """No emoji are added when all are present."""          await incidents.add_signals(self.incident) @@ -290,7 +290,7 @@ class TestIncidents(unittest.IsolatedAsyncioTestCase):          Note that this will not schedule `crawl_incidents` in the background, as everything          is being mocked. The `crawl_task` attribute will end up being None.          """ -        self.cog_instance = Incidents(MockBot()) +        self.cog_instance = incidents.Incidents(MockBot())  @patch("asyncio.sleep", AsyncMock())  # Prevent the coro from sleeping to speed up the test @@ -326,25 +326,25 @@ class TestCrawlIncidents(TestIncidents):          await self.cog_instance.crawl_incidents()          self.cog_instance.bot.wait_until_guild_available.assert_awaited() -    @patch("bot.cogs.moderation.incidents.add_signals", AsyncMock()) -    @patch("bot.cogs.moderation.incidents.is_incident", MagicMock(return_value=False))  # Message doesn't qualify -    @patch("bot.cogs.moderation.incidents.has_signals", MagicMock(return_value=False)) +    @patch("bot.exts.moderation.incidents.add_signals", AsyncMock()) +    @patch("bot.exts.moderation.incidents.is_incident", MagicMock(return_value=False))  # Message doesn't qualify +    @patch("bot.exts.moderation.incidents.has_signals", MagicMock(return_value=False))      async def test_crawl_incidents_noop_if_is_not_incident(self):          """Signals are not added for a non-incident message."""          await self.cog_instance.crawl_incidents()          incidents.add_signals.assert_not_awaited() -    @patch("bot.cogs.moderation.incidents.add_signals", AsyncMock()) -    @patch("bot.cogs.moderation.incidents.is_incident", MagicMock(return_value=True))  # Message qualifies -    @patch("bot.cogs.moderation.incidents.has_signals", MagicMock(return_value=True))  # But already has signals +    @patch("bot.exts.moderation.incidents.add_signals", AsyncMock()) +    @patch("bot.exts.moderation.incidents.is_incident", MagicMock(return_value=True))  # Message qualifies +    @patch("bot.exts.moderation.incidents.has_signals", MagicMock(return_value=True))  # But already has signals      async def test_crawl_incidents_noop_if_message_already_has_signals(self):          """Signals are not added for messages which already have them."""          await self.cog_instance.crawl_incidents()          incidents.add_signals.assert_not_awaited() -    @patch("bot.cogs.moderation.incidents.add_signals", AsyncMock()) -    @patch("bot.cogs.moderation.incidents.is_incident", MagicMock(return_value=True))  # Message qualifies -    @patch("bot.cogs.moderation.incidents.has_signals", MagicMock(return_value=False))  # And doesn't have signals +    @patch("bot.exts.moderation.incidents.add_signals", AsyncMock()) +    @patch("bot.exts.moderation.incidents.is_incident", MagicMock(return_value=True))  # Message qualifies +    @patch("bot.exts.moderation.incidents.has_signals", MagicMock(return_value=False))  # And doesn't have signals      async def test_crawl_incidents_add_signals_called(self):          """Message has signals added as it does not have them yet and qualifies as an incident."""          await self.cog_instance.crawl_incidents() @@ -384,7 +384,7 @@ class TestArchive(TestIncidents):          )          built_embed = MagicMock(discord.Embed, id=123)  # We patch `make_embed` to return this -        with patch("bot.cogs.moderation.incidents.make_embed", AsyncMock(return_value=(built_embed, None))): +        with patch("bot.exts.moderation.incidents.make_embed", AsyncMock(return_value=(built_embed, None))):              archive_return = await self.cog_instance.archive(incident, MagicMock(value="A"), MockMember())          # Now we check that the webhook was given the correct args, and that `archive` returned True @@ -451,8 +451,8 @@ class TestMakeConfirmationTask(TestIncidents):          self.assertFalse(created_check(payload=MagicMock(message_id=0))) -@patch("bot.cogs.moderation.incidents.ALLOWED_ROLES", {1, 2}) -@patch("bot.cogs.moderation.incidents.Incidents.make_confirmation_task", AsyncMock())  # Generic awaitable +@patch("bot.exts.moderation.incidents.ALLOWED_ROLES", {1, 2}) +@patch("bot.exts.moderation.incidents.Incidents.make_confirmation_task", AsyncMock())  # Generic awaitable  class TestProcessEvent(TestIncidents):      """Tests for the `Incidents.process_event` coroutine.""" @@ -479,7 +479,7 @@ class TestProcessEvent(TestIncidents):      async def test_process_event_no_archive_on_investigating(self):          """Message is not archived on `Signal.INVESTIGATING`.""" -        with patch("bot.cogs.moderation.incidents.Incidents.archive", AsyncMock()) as mocked_archive: +        with patch("bot.exts.moderation.incidents.Incidents.archive", AsyncMock()) as mocked_archive:              await self.cog_instance.process_event(                  reaction=incidents.Signal.INVESTIGATING.value,                  incident=MockMessage(), @@ -497,7 +497,7 @@ class TestProcessEvent(TestIncidents):          """          incident = MockMessage() -        with patch("bot.cogs.moderation.incidents.Incidents.archive", AsyncMock(return_value=False)): +        with patch("bot.exts.moderation.incidents.Incidents.archive", AsyncMock(return_value=False)):              await self.cog_instance.process_event(                  reaction=incidents.Signal.ACTIONED.value,                  incident=incident, @@ -510,7 +510,7 @@ class TestProcessEvent(TestIncidents):          """Task given by `Incidents.make_confirmation_task` is awaited before method exits."""          mock_task = AsyncMock() -        with patch("bot.cogs.moderation.incidents.Incidents.make_confirmation_task", mock_task): +        with patch("bot.exts.moderation.incidents.Incidents.make_confirmation_task", mock_task):              await self.cog_instance.process_event(                  reaction=incidents.Signal.ACTIONED.value,                  incident=MockMessage(), @@ -530,7 +530,7 @@ class TestProcessEvent(TestIncidents):          mock_task = AsyncMock(side_effect=asyncio.TimeoutError())          try: -            with patch("bot.cogs.moderation.incidents.Incidents.make_confirmation_task", mock_task): +            with patch("bot.exts.moderation.incidents.Incidents.make_confirmation_task", mock_task):                  await self.cog_instance.process_event(                      reaction=incidents.Signal.ACTIONED.value,                      incident=MockMessage(), @@ -712,7 +712,7 @@ class TestOnRawReactionAdd(TestIncidents):          self.cog_instance.process_event = AsyncMock()          self.cog_instance.resolve_message = AsyncMock(return_value=MockMessage()) -        with patch("bot.cogs.moderation.incidents.is_incident", MagicMock(return_value=False)): +        with patch("bot.exts.moderation.incidents.is_incident", MagicMock(return_value=False)):              await self.cog_instance.on_raw_reaction_add(self.payload)          self.cog_instance.process_event.assert_not_called() @@ -733,7 +733,7 @@ class TestOnRawReactionAdd(TestIncidents):          self.cog_instance.process_event = AsyncMock()          self.cog_instance.resolve_message = AsyncMock(return_value=incident) -        with patch("bot.cogs.moderation.incidents.is_incident", MagicMock(return_value=True)): +        with patch("bot.exts.moderation.incidents.is_incident", MagicMock(return_value=True)):              await self.cog_instance.on_raw_reaction_add(self.payload)          self.cog_instance.process_event.assert_called_with( @@ -751,20 +751,20 @@ class TestOnMessage(TestIncidents):      function is tested in `TestIsIncident` - here we do not worry about it.      """ -    @patch("bot.cogs.moderation.incidents.is_incident", MagicMock(return_value=True)) +    @patch("bot.exts.moderation.incidents.is_incident", MagicMock(return_value=True))      async def test_on_message_incident(self):          """Messages qualifying as incidents are passed to `add_signals`."""          incident = MockMessage() -        with patch("bot.cogs.moderation.incidents.add_signals", AsyncMock()) as mock_add_signals: +        with patch("bot.exts.moderation.incidents.add_signals", AsyncMock()) as mock_add_signals:              await self.cog_instance.on_message(incident)          mock_add_signals.assert_called_once_with(incident) -    @patch("bot.cogs.moderation.incidents.is_incident", MagicMock(return_value=False)) +    @patch("bot.exts.moderation.incidents.is_incident", MagicMock(return_value=False))      async def test_on_message_non_incident(self):          """Messages not qualifying as incidents are ignored.""" -        with patch("bot.cogs.moderation.incidents.add_signals", AsyncMock()) as mock_add_signals: +        with patch("bot.exts.moderation.incidents.add_signals", AsyncMock()) as mock_add_signals:              await self.cog_instance.on_message(MockMessage())          mock_add_signals.assert_not_called() diff --git a/tests/bot/cogs/moderation/test_modlog.py b/tests/bot/exts/moderation/test_modlog.py index f2809f40a..f8f142484 100644 --- a/tests/bot/cogs/moderation/test_modlog.py +++ b/tests/bot/exts/moderation/test_modlog.py @@ -2,7 +2,7 @@ import unittest  import discord -from bot.cogs.moderation.modlog import ModLog +from bot.exts.moderation.modlog import ModLog  from tests.helpers import MockBot, MockTextChannel diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index ab3d0742a..8c4fb764a 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -4,8 +4,8 @@ from unittest.mock import MagicMock, Mock  from discord import PermissionOverwrite -from bot.cogs.moderation.silence import Silence, SilenceNotifier  from bot.constants import Channels, Emojis, Guild, Roles +from bot.exts.moderation.silence import Silence, SilenceNotifier  from tests.helpers import MockBot, MockContext, MockTextChannel @@ -99,7 +99,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase):          self.bot.get_channel.called_once_with(Channels.mod_alerts)          self.bot.get_channel.called_once_with(Channels.mod_log) -    @mock.patch("bot.cogs.moderation.silence.SilenceNotifier") +    @mock.patch("bot.exts.moderation.silence.SilenceNotifier")      async def test_instance_vars_got_notifier(self, notifier):          """Notifier was started with channel."""          mod_log = MockTextChannel() @@ -238,7 +238,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase):          del mock_permissions_dict['send_messages']          self.assertDictEqual(mock_permissions_dict, new_permissions) -    @mock.patch("bot.cogs.moderation.silence.asyncio") +    @mock.patch("bot.exts.moderation.silence.asyncio")      @mock.patch.object(Silence, "_mod_alerts_channel", create=True)      def test_cog_unload_starts_task(self, alert_channel, asyncio_mock):          """Task for sending an alert was created with present `muted_channels`.""" @@ -247,14 +247,14 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase):              alert_channel.send.assert_called_once_with(f"<@&{Roles.moderators}> channels left silenced on cog unload: ")              asyncio_mock.create_task.assert_called_once_with(alert_channel.send()) -    @mock.patch("bot.cogs.moderation.silence.asyncio") +    @mock.patch("bot.exts.moderation.silence.asyncio")      def test_cog_unload_skips_task_start(self, asyncio_mock):          """No task created with no channels."""          self.cog.cog_unload()          asyncio_mock.create_task.assert_not_called() -    @mock.patch("bot.cogs.moderation.silence.with_role_check") -    @mock.patch("bot.cogs.moderation.silence.MODERATION_ROLES", new=(1, 2, 3)) +    @mock.patch("bot.exts.moderation.silence.with_role_check") +    @mock.patch("bot.exts.moderation.silence.MODERATION_ROLES", new=(1, 2, 3))      def test_cog_check(self, role_check):          """Role check is called with `MODERATION_ROLES`"""          self.cog.cog_check(self.ctx) diff --git a/tests/bot/cogs/test_slowmode.py b/tests/bot/exts/moderation/test_slowmode.py index f442814c8..e90394ab9 100644 --- a/tests/bot/cogs/test_slowmode.py +++ b/tests/bot/exts/moderation/test_slowmode.py @@ -3,8 +3,8 @@ from unittest import mock  from dateutil.relativedelta import relativedelta -from bot.cogs.moderation.slowmode import Slowmode  from bot.constants import Emojis +from bot.exts.moderation.slowmode import Slowmode  from tests.helpers import MockBot, MockContext, MockTextChannel @@ -103,8 +103,8 @@ class SlowmodeTests(unittest.IsolatedAsyncioTestCase):              f'{Emojis.check_mark} The slowmode delay for #meta has been reset to 0 seconds.'          ) -    @mock.patch("bot.cogs.moderation.slowmode.with_role_check") -    @mock.patch("bot.cogs.moderation.slowmode.MODERATION_ROLES", new=(1, 2, 3)) +    @mock.patch("bot.exts.moderation.slowmode.with_role_check") +    @mock.patch("bot.exts.moderation.slowmode.MODERATION_ROLES", new=(1, 2, 3))      def test_cog_check(self, role_check):          """Role check is called with `MODERATION_ROLES`"""          self.cog.cog_check(self.ctx) diff --git a/tests/bot/cogs/test_cogs.py b/tests/bot/exts/test_cogs.py index 30a04422a..f8e120262 100644 --- a/tests/bot/cogs/test_cogs.py +++ b/tests/bot/exts/test_cogs.py @@ -10,7 +10,7 @@ from unittest import mock  from discord.ext import commands -from bot import cogs +from bot import exts  class CommandNameTests(unittest.TestCase): @@ -29,13 +29,14 @@ class CommandNameTests(unittest.TestCase):      @staticmethod      def walk_modules() -> t.Iterator[ModuleType]: -        """Yield imported modules from the bot.cogs subpackage.""" +        """Yield imported modules from the bot.exts subpackage."""          def on_error(name: str) -> t.NoReturn:              raise ImportError(name=name)  # pragma: no cover          # The mock prevents asyncio.get_event_loop() from being called.          with mock.patch("discord.ext.tasks.loop"): -            for module in pkgutil.walk_packages(cogs.__path__, "bot.cogs.", onerror=on_error): +            prefix = f"{exts.__name__}." +            for module in pkgutil.walk_packages(exts.__path__, prefix, onerror=on_error):                  if not module.ispkg:                      yield importlib.import_module(module.name) diff --git a/tests/bot/exts/utils/__init__.py b/tests/bot/exts/utils/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/bot/exts/utils/__init__.py diff --git a/tests/bot/cogs/test_jams.py b/tests/bot/exts/utils/test_jams.py index b4ad8535f..45e7b5b51 100644 --- a/tests/bot/cogs/test_jams.py +++ b/tests/bot/exts/utils/test_jams.py @@ -3,8 +3,8 @@ from unittest.mock import AsyncMock, MagicMock, create_autospec  from discord import CategoryChannel -from bot.cogs import jams  from bot.constants import Roles +from bot.exts.utils import jams  from tests.helpers import MockBot, MockContext, MockGuild, MockMember, MockRole, MockTextChannel diff --git a/tests/bot/cogs/test_snekbox.py b/tests/bot/exts/utils/test_snekbox.py index f22952931..c272a4756 100644 --- a/tests/bot/cogs/test_snekbox.py +++ b/tests/bot/exts/utils/test_snekbox.py @@ -5,8 +5,8 @@ from unittest.mock import AsyncMock, MagicMock, Mock, call, create_autospec, pat  from discord.ext import commands  from bot import constants -from bot.cogs import snekbox -from bot.cogs.snekbox import Snekbox +from bot.exts.utils import snekbox +from bot.exts.utils.snekbox import Snekbox  from tests.helpers import MockBot, MockContext, MockMessage, MockReaction, MockUser @@ -38,7 +38,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):          result = await self.cog.upload_output("-" * (snekbox.MAX_PASTE_LEN + 1))          self.assertEqual(result, "too long to upload") -    @patch("bot.cogs.snekbox.send_to_paste_service") +    @patch("bot.exts.utils.snekbox.send_to_paste_service")      async def test_upload_output(self, mock_paste_util):          """Upload the eval output to the URLs.paste_service.format(key="documents") endpoint."""          await self.cog.upload_output("Test output.") @@ -69,14 +69,14 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):                  actual = self.cog.get_results_message({'stdout': stdout, 'returncode': returncode})                  self.assertEqual(actual, expected) -    @patch('bot.cogs.snekbox.Signals', side_effect=ValueError) +    @patch('bot.exts.utils.snekbox.Signals', side_effect=ValueError)      def test_get_results_message_invalid_signal(self, mock_signals: Mock):          self.assertEqual(              self.cog.get_results_message({'stdout': '', 'returncode': 127}),              ('Your eval job has completed with return code 127', '')          ) -    @patch('bot.cogs.snekbox.Signals') +    @patch('bot.exts.utils.snekbox.Signals')      def test_get_results_message_valid_signal(self, mock_signals: Mock):          mock_signals.return_value.name = 'SIGTEST'          self.assertEqual( @@ -266,7 +266,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):          self.cog.get_results_message.assert_called_once_with({'stdout': 'ERROR', 'returncode': 127})          self.cog.format_output.assert_not_called() -    @patch("bot.cogs.snekbox.partial") +    @patch("bot.exts.utils.snekbox.partial")      async def test_continue_eval_does_continue(self, partial_mock):          """Test that the continue_eval function does continue if required conditions are met."""          ctx = MockContext(message=MockMessage(add_reaction=AsyncMock(), clear_reactions=AsyncMock())) | 
