aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar wookie184 <[email protected]>2023-04-12 15:21:53 +0100
committerGravatar GitHub <[email protected]>2023-04-12 15:21:53 +0100
commitcd420dade00b253bc7d458a2480748a55668591a (patch)
treedb171434ffe1e3872e3f6a83c56f9ea6a8ac8806
parentFix deleted message logging in mod alert (diff)
parentMerge pull request #2523 from python-discord/flake8-to-ruff (diff)
Merge branch 'main' into fix_alert_message_logging
-rw-r--r--.git-blame-ignore-revs2
-rw-r--r--.github/workflows/lint-test.yml20
-rw-r--r--.pre-commit-config.yaml19
-rw-r--r--bot/__main__.py2
-rw-r--r--bot/bot.py2
-rw-r--r--bot/constants.py63
-rw-r--r--bot/converters.py81
-rw-r--r--bot/decorators.py25
-rw-r--r--bot/errors.py10
-rw-r--r--bot/exts/backend/branding/_cog.py10
-rw-r--r--bot/exts/backend/branding/_repository.py36
-rw-r--r--bot/exts/backend/error_handler.py6
-rw-r--r--bot/exts/backend/sync/_cog.py50
-rw-r--r--bot/exts/backend/sync/_syncers.py16
-rw-r--r--bot/exts/events/__init__.py0
-rw-r--r--bot/exts/events/code_jams/__init__.py8
-rw-r--r--bot/exts/events/code_jams/_channels.py113
-rw-r--r--bot/exts/events/code_jams/_cog.py239
-rw-r--r--bot/exts/filtering/_filter_context.py2
-rw-r--r--bot/exts/filtering/_filter_lists/__init__.py4
-rw-r--r--bot/exts/filtering/_filter_lists/antispam.py4
-rw-r--r--bot/exts/filtering/_filter_lists/extension.py2
-rw-r--r--bot/exts/filtering/_filter_lists/filter_list.py20
-rw-r--r--bot/exts/filtering/_filter_lists/token.py2
-rw-r--r--bot/exts/filtering/_filters/antispam/mentions.py2
-rw-r--r--bot/exts/filtering/_filters/unique/discord_token.py16
-rw-r--r--bot/exts/filtering/_filters/unique/rich_embed.py10
-rw-r--r--bot/exts/filtering/_settings.py10
-rw-r--r--bot/exts/filtering/_settings_types/actions/infraction_and_notification.py5
-rw-r--r--bot/exts/filtering/_settings_types/actions/ping.py3
-rw-r--r--bot/exts/filtering/_settings_types/actions/remove_context.py3
-rw-r--r--bot/exts/filtering/_settings_types/actions/send_alert.py4
-rw-r--r--bot/exts/filtering/_settings_types/settings_entry.py5
-rw-r--r--bot/exts/filtering/_settings_types/validations/bypass_roles.py2
-rw-r--r--bot/exts/filtering/_settings_types/validations/channel_scope.py8
-rw-r--r--bot/exts/filtering/_ui/filter.py7
-rw-r--r--bot/exts/filtering/_ui/filter_list.py19
-rw-r--r--bot/exts/filtering/_ui/search.py4
-rw-r--r--bot/exts/filtering/_ui/ui.py21
-rw-r--r--bot/exts/filtering/_utils.py53
-rw-r--r--bot/exts/filtering/filtering.py97
-rw-r--r--bot/exts/fun/duck_pond.py6
-rw-r--r--bot/exts/fun/off_topic_names.py41
-rw-r--r--bot/exts/help_channels/_channel.py4
-rw-r--r--bot/exts/help_channels/_cog.py4
-rw-r--r--bot/exts/help_channels/_message.py2
-rw-r--r--bot/exts/info/code_snippets.py101
-rw-r--r--bot/exts/info/codeblock/_cog.py3
-rw-r--r--bot/exts/info/codeblock/_instructions.py27
-rw-r--r--bot/exts/info/codeblock/_parsing.py16
-rw-r--r--bot/exts/info/doc/_batch_parser.py16
-rw-r--r--bot/exts/info/doc/_cog.py25
-rw-r--r--bot/exts/info/doc/_html.py20
-rw-r--r--bot/exts/info/doc/_inventory_parser.py16
-rw-r--r--bot/exts/info/doc/_parsing.py16
-rw-r--r--bot/exts/info/doc/_redis_cache.py4
-rw-r--r--bot/exts/info/help.py45
-rw-r--r--bot/exts/info/information.py68
-rw-r--r--bot/exts/info/pep.py37
-rw-r--r--bot/exts/info/python_news.py15
-rw-r--r--bot/exts/info/resources.py5
-rw-r--r--bot/exts/info/source.py7
-rw-r--r--bot/exts/info/stats.py4
-rw-r--r--bot/exts/info/subscribe.py5
-rw-r--r--bot/exts/info/tags.py43
-rw-r--r--bot/exts/moderation/clean.py51
-rw-r--r--bot/exts/moderation/defcon.py19
-rw-r--r--bot/exts/moderation/incidents.py23
-rw-r--r--bot/exts/moderation/infraction/_scheduler.py28
-rw-r--r--bot/exts/moderation/infraction/_utils.py54
-rw-r--r--bot/exts/moderation/infraction/infractions.py86
-rw-r--r--bot/exts/moderation/infraction/management.py77
-rw-r--r--bot/exts/moderation/infraction/superstarify.py17
-rw-r--r--bot/exts/moderation/metabase.py8
-rw-r--r--bot/exts/moderation/modlog.py41
-rw-r--r--bot/exts/moderation/modpings.py18
-rw-r--r--bot/exts/moderation/silence.py27
-rw-r--r--bot/exts/moderation/slowmode.py28
-rw-r--r--bot/exts/moderation/stream.py8
-rw-r--r--bot/exts/moderation/verification.py12
-rw-r--r--bot/exts/moderation/voice_gate.py4
-rw-r--r--bot/exts/moderation/watchchannels/_watchchannel.py28
-rw-r--r--bot/exts/moderation/watchchannels/bigbrother.py24
-rw-r--r--bot/exts/recruitment/talentpool/_cog.py35
-rw-r--r--bot/exts/recruitment/talentpool/_review.py31
-rw-r--r--bot/exts/utils/bot.py11
-rw-r--r--bot/exts/utils/extensions.py8
-rw-r--r--bot/exts/utils/internal.py25
-rw-r--r--bot/exts/utils/ping.py2
-rw-r--r--bot/exts/utils/reminders.py84
-rw-r--r--bot/exts/utils/snekbox/_cog.py19
-rw-r--r--bot/exts/utils/snekbox/_eval.py6
-rw-r--r--bot/exts/utils/thread_bumper.py12
-rw-r--r--bot/exts/utils/utils.py7
-rw-r--r--bot/log.py4
-rw-r--r--bot/pagination.py51
-rw-r--r--bot/rules/mentions.py70
-rw-r--r--bot/utils/__init__.py14
-rw-r--r--bot/utils/caching.py3
-rw-r--r--bot/utils/channel.py10
-rw-r--r--bot/utils/checks.py25
-rw-r--r--bot/utils/function.py8
-rw-r--r--bot/utils/helpers.py5
-rw-r--r--bot/utils/lock.py8
-rw-r--r--bot/utils/members.py2
-rw-r--r--bot/utils/message_cache.py10
-rw-r--r--bot/utils/messages.py46
-rw-r--r--bot/utils/services.py6
-rw-r--r--bot/utils/time.py48
-rw-r--r--bot/utils/webhooks.py11
-rw-r--r--botstrap.py2
-rw-r--r--poetry.lock483
-rw-r--r--pyproject.toml38
-rw-r--r--tests/_autospec.py2
-rw-r--r--tests/base.py3
-rw-r--r--tests/bot/exts/events/__init__.py0
-rw-r--r--tests/bot/exts/events/test_code_jams.py170
-rw-r--r--tests/bot/exts/filtering/test_discord_token_filter.py4
-rw-r--r--tests/bot/exts/filtering/test_extension_filter.py2
-rw-r--r--tests/bot/exts/info/test_information.py44
-rw-r--r--tests/bot/exts/moderation/infraction/test_infractions.py5
-rw-r--r--tests/bot/exts/moderation/infraction/test_utils.py14
-rw-r--r--tests/bot/exts/moderation/test_incidents.py4
-rw-r--r--tests/bot/exts/moderation/test_silence.py83
-rw-r--r--tests/bot/exts/moderation/test_slowmode.py20
-rw-r--r--tests/bot/exts/recruitment/talentpool/test_review.py10
-rw-r--r--tests/bot/exts/test_cogs.py2
-rw-r--r--tests/bot/exts/utils/snekbox/test_io.py2
-rw-r--r--tests/bot/exts/utils/snekbox/test_snekbox.py170
-rw-r--r--tests/bot/resources/test_resources.py2
-rw-r--r--tests/bot/test_constants.py8
-rw-r--r--tests/bot/test_converters.py162
-rw-r--r--tests/bot/test_decorators.py8
-rw-r--r--tests/bot/test_pagination.py18
-rw-r--r--tests/bot/utils/test_message_cache.py5
-rw-r--r--tests/bot/utils/test_time.py81
-rw-r--r--tests/helpers.py187
-rw-r--r--tests/test_base.py21
-rw-r--r--tests/test_helpers.py29
-rw-r--r--tox.ini19
140 files changed, 1699 insertions, 2548 deletions
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
new file mode 100644
index 000000000..f866e3ea8
--- /dev/null
+++ b/.git-blame-ignore-revs
@@ -0,0 +1,2 @@
+# Migrate code style to ruff
+8dca42846d2956122d45795763095559a6a51b64
diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml
index 63dc8a581..3ce2ac7a9 100644
--- a/.github/workflows/lint-test.yml
+++ b/.github/workflows/lint-test.yml
@@ -45,24 +45,12 @@ jobs:
pip-licenses --allow-only="$ALLOWED_LICENSE" \
--package $(poetry export -f requirements.txt --without-hashes | sed "s/==.*//g" | tr "\n" " ")
- # We will not run `flake8` here, as we will use a separate flake8
- # action. As pre-commit does not support user installs, we set
- # PIP_USER=0 to not do a user install.
- name: Run pre-commit hooks
- run: export PIP_USER=0; SKIP=flake8 pre-commit run --all-files
+ run: SKIP=ruff pre-commit run --all-files
- # Run flake8 and have it format the linting errors in the format of
- # the GitHub Workflow command to register error annotations. This
- # means that our flake8 output is automatically added as an error
- # annotation to both the run result and in the "Files" tab of a
- # pull request.
- #
- # Format used:
- # ::error file={filename},line={line},col={col}::{message}
- - name: Run flake8
- run: "flake8 \
- --format='::error file=%(path)s,line=%(row)d,col=%(col)d::\
- [flake8] %(code)s: %(text)s'"
+ # Run `ruff` using github formatting to enable automatic inline annotations.
+ - name: Run ruff
+ run: "ruff check --format=github ."
- name: Run tests and generate coverage report
run: pytest -n auto --cov --disable-warnings -q
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 47fd80f97..511de28c7 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -9,10 +9,6 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
- - repo: https://github.com/pre-commit/pygrep-hooks
- rev: v1.5.1
- hooks:
- - id: python-check-blanket-noqa
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
@@ -20,10 +16,11 @@ repos:
name: isort (python)
- repo: local
hooks:
- - id: flake8
- name: Flake8
- description: This hook runs flake8 within our project's environment.
- entry: poetry run flake8
- language: system
- types: [python]
- require_serial: true
+ - id: ruff
+ name: ruff
+ description: Run ruff linting
+ entry: poetry run ruff check --force-exclude
+ language: system
+ 'types_or': [python, pyi]
+ require_serial: true
+ args: [--fix, --exit-non-zero-on-fix]
diff --git a/bot/__main__.py b/bot/__main__.py
index 8b640a370..ddb6ed608 100644
--- a/bot/__main__.py
+++ b/bot/__main__.py
@@ -78,7 +78,7 @@ try:
asyncio.run(main())
except StartupError as e:
message = "Unknown Startup Error Occurred."
- if isinstance(e.exception, (aiohttp.ClientConnectorError, aiohttp.ServerDisconnectedError)):
+ if isinstance(e.exception, aiohttp.ClientConnectorError | aiohttp.ServerDisconnectedError):
message = "Could not connect to site API. Is it running?"
elif isinstance(e.exception, OSError):
message = "Could not connect to Redis. Is it running?"
diff --git a/bot/bot.py b/bot/bot.py
index f56aec38e..2321232de 100644
--- a/bot/bot.py
+++ b/bot/bot.py
@@ -8,7 +8,7 @@ from sentry_sdk import push_scope
from bot import constants, exts
from bot.log import get_logger
-log = get_logger('bot')
+log = get_logger("bot")
class StartupError(Exception):
diff --git a/bot/constants.py b/bot/constants.py
index eefae11e2..1f9a55614 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -1,13 +1,9 @@
"""
-Loads bot configuration from environment variables
-and `.env` files. By default, this simply loads the
-default configuration defined thanks to the `default`
-keyword argument in each instance of the `Field` class
-If two files called `.env` and `.env.server` are found
-in the project directory, the values will be loaded
-from both of them, thus overlooking the predefined defaults.
-Any settings left out in the custom user configuration
-will default to the values passed to the `default` kwarg.
+Loads bot configuration from environment variables and `.env` files.
+
+By default, the values defined in the classes are used, these can be overridden by an env var with the same name.
+
+`.env` and `.env.server` files are used to populate env vars, if present.
"""
import os
from enum import Enum
@@ -16,10 +12,14 @@ from pydantic import BaseModel, BaseSettings, root_validator
class EnvConfig(BaseSettings):
+ """Our default configuration for models that should load from .env files."""
+
class Config:
+ """Specify what .env files to load, and how to load them."""
+
env_file = ".env.server", ".env",
- env_file_encoding = 'utf-8'
- env_nested_delimiter = '__'
+ env_file_encoding = "utf-8"
+ env_nested_delimiter = "__"
class _Miscellaneous(EnvConfig):
@@ -227,8 +227,9 @@ Guild = _Guild()
class Event(Enum):
"""
- Event names. This does not include every event (for example, raw
- events aren't here), but only events used in ModLog for now.
+ Discord.py event names.
+
+ This does not include every event (for example, raw events aren't here), only events used in ModLog for now.
"""
guild_channel_create = "guild_channel_create"
@@ -252,6 +253,8 @@ class Event(Enum):
class ThreadArchiveTimes(Enum):
+ """The time periods threads can have the archive time set to."""
+
HOUR = 60
DAY = 1440
THREE_DAY = 4320
@@ -259,6 +262,8 @@ class ThreadArchiveTimes(Enum):
class Webhook(BaseModel):
+ """A base class for all webhooks."""
+
id: int
channel: int
@@ -317,7 +322,8 @@ class _Colours(EnvConfig):
yellow = 0xffd241
@root_validator(pre=True)
- def parse_hex_values(cls, values):
+ def parse_hex_values(cls, values: dict) -> dict: # noqa: N805
+ """Convert hex strings to ints."""
for key, value in values.items():
values[key] = int(value, 16)
return values
@@ -326,17 +332,6 @@ class _Colours(EnvConfig):
Colours = _Colours()
-class _Free(EnvConfig):
- EnvConfig.Config.env_prefix = "free_"
-
- activity_timeout = 600
- cooldown_per = 60.0
- cooldown_rate = 1
-
-
-Free = _Free()
-
-
class _HelpChannels(EnvConfig):
EnvConfig.Config.env_prefix = "help_channels_"
@@ -388,7 +383,7 @@ class _PythonNews(EnvConfig):
channel: int = Webhooks.python_news.channel
webhook: int = Webhooks.python_news.id
- mail_lists = ['python-ideas', 'python-announce-list', 'pypi-announce', 'python-dev']
+ mail_lists = ["python-ideas", "python-announce-list", "pypi-announce", "python-dev"]
PythonNews = _PythonNews()
@@ -533,9 +528,9 @@ class _Emojis(EnvConfig):
verified_bot = "<:verified_bot:811645219220750347>"
bot = "<:bot:812712599464443914>"
- defcon_shutdown = "<:defcondisabled:470326273952972810>" # noqa: E704
- defcon_unshutdown = "<:defconenabled:470326274213150730>" # noqa: E704
- defcon_update = "<:defconsettingsupdated:470326274082996224>" # noqa: E704
+ defcon_shutdown = "<:defcondisabled:470326273952972810>"
+ defcon_unshutdown = "<:defconenabled:470326274213150730>"
+ defcon_update = "<:defconsettingsupdated:470326274082996224>"
failmail = "<:failmail:633660039931887616>"
failed_file = "<:failed_file:1073298441968562226>"
@@ -572,10 +567,10 @@ class _Icons(EnvConfig):
crown_green = "https://cdn.discordapp.com/emojis/469964154719961088.png"
crown_red = "https://cdn.discordapp.com/emojis/469964154879344640.png"
- defcon_denied = "https://cdn.discordapp.com/emojis/472475292078964738.png" # noqa: E704
- defcon_shutdown = "https://cdn.discordapp.com/emojis/470326273952972810.png" # noqa: E704
- defcon_unshutdown = "https://cdn.discordapp.com/emojis/470326274213150730.png" # noqa: E704
- defcon_update = "https://cdn.discordapp.com/emojis/472472638342561793.png" # noqa: E704
+ defcon_denied = "https://cdn.discordapp.com/emojis/472475292078964738.png"
+ defcon_shutdown = "https://cdn.discordapp.com/emojis/470326273952972810.png"
+ defcon_unshutdown = "https://cdn.discordapp.com/emojis/470326274213150730.png"
+ defcon_update = "https://cdn.discordapp.com/emojis/472472638342561793.png"
filtering = "https://cdn.discordapp.com/emojis/472472638594482195.png"
@@ -605,7 +600,7 @@ class _Icons(EnvConfig):
superstarify = "https://cdn.discordapp.com/emojis/636288153044516874.png"
unsuperstarify = "https://cdn.discordapp.com/emojis/636288201258172446.png"
- token_removed = "https://cdn.discordapp.com/emojis/470326273298792469.png"
+ token_removed = "https://cdn.discordapp.com/emojis/470326273298792469.png" # noqa: S105
user_ban = "https://cdn.discordapp.com/emojis/469952898026045441.png"
user_timeout = "https://cdn.discordapp.com/emojis/472472640100106250.png"
diff --git a/bot/converters.py b/bot/converters.py
index 21623b597..89fda6cbb 100644
--- a/bot/converters.py
+++ b/bot/converters.py
@@ -2,7 +2,7 @@ from __future__ import annotations
import re
import typing as t
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from ssl import CertificateError
import dateutil.parser
@@ -17,7 +17,7 @@ from pydis_core.utils.regex import DISCORD_INVITE
from bot import exts, instance as bot_instance
from bot.constants import URLs
-from bot.errors import InvalidInfraction
+from bot.errors import InvalidInfractionError
from bot.exts.info.doc import _inventory_parser
from bot.exts.info.tags import TagIdentifier
from bot.log import get_logger
@@ -85,7 +85,8 @@ class Extension(Converter):
if argument in bot_instance.all_extensions:
return argument
- elif (qualified_arg := f"{exts.__name__}.{argument}") in bot_instance.all_extensions:
+
+ if (qualified_arg := f"{exts.__name__}.{argument}") in bot_instance.all_extensions:
return qualified_arg
matches = []
@@ -100,10 +101,10 @@ class Extension(Converter):
f":x: `{argument}` is an ambiguous extension name. "
f"Please use one of the following fully-qualified names.```\n{names}```"
)
- elif matches:
+
+ if matches:
return matches[0]
- else:
- raise BadArgument(f":x: Could not find the extension `{argument}`.")
+ raise BadArgument(f":x: Could not find the extension `{argument}`.")
class PackageName(Converter):
@@ -143,7 +144,7 @@ class ValidURL(Converter):
f"HTTP GET on `{url}` returned status `{resp.status}`, expected 200"
)
except CertificateError:
- if url.startswith('https'):
+ if url.startswith("https"):
raise BadArgument(
f"Got a `CertificateError` for URL `{url}`. Does it support HTTPS?"
)
@@ -166,7 +167,7 @@ class Inventory(Converter):
"""
@staticmethod
- async def convert(ctx: Context, url: str) -> t.Tuple[str, _inventory_parser.InventoryDict]:
+ async def convert(ctx: Context, url: str) -> tuple[str, _inventory_parser.InventoryDict]:
"""Convert url to Intersphinx inventory URL."""
await ctx.typing()
try:
@@ -213,7 +214,7 @@ class Snowflake(IDConverter):
if time < DISCORD_EPOCH_DT:
raise BadArgument(f"{error}: timestamp is before the Discord epoch.")
- elif (datetime.now(timezone.utc) - time).days < -1:
+ if (datetime.now(UTC) - time).days < -1:
raise BadArgument(f"{error}: timestamp is too far into the future.")
return snowflake
@@ -286,7 +287,7 @@ class Duration(DurationDelta):
The converter supports the same symbols for each unit of time as its parent class.
"""
delta = await super().convert(ctx, duration)
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
try:
return now + delta
@@ -304,7 +305,7 @@ class Age(DurationDelta):
The converter supports the same symbols for each unit of time as its parent class.
"""
delta = await super().convert(ctx, duration)
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
try:
return now - delta
@@ -316,7 +317,7 @@ class OffTopicName(Converter):
"""A converter that ensures an added off-topic name is valid."""
ALLOWED_CHARACTERS = r"ABCDEFGHIJKLMNOPQRSTUVWXYZ!?'`-<>\/"
- TRANSLATED_CHARACTERS = "𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹ǃ?’’-<>⧹⧸"
+ TRANSLATED_CHARACTERS = "𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹ǃ?’’-<>⧹⧸" # noqa: RUF001
@classmethod
def translate_name(cls, name: str, *, from_unicode: bool = True) -> str:
@@ -340,7 +341,7 @@ class OffTopicName(Converter):
if not (2 <= len(argument) <= 96):
raise BadArgument("Channel name must be between 2 and 96 chars long")
- elif not all(c.isalnum() or c in self.ALLOWED_CHARACTERS for c in argument):
+ if not all(c.isalnum() or c in self.ALLOWED_CHARACTERS for c in argument):
raise BadArgument(
"Channel name must only consist of "
"alphanumeric characters, minus signs or apostrophes."
@@ -386,9 +387,9 @@ class ISODateTime(Converter):
raise BadArgument(f"`{datetime_string}` is not a valid ISO-8601 datetime string")
if dt.tzinfo:
- dt = dt.astimezone(timezone.utc)
+ dt = dt.astimezone(UTC)
else: # Without a timezone, assume it represents UTC.
- dt = dt.replace(tzinfo=timezone.utc)
+ dt = dt.replace(tzinfo=UTC)
return dt
@@ -426,8 +427,8 @@ def _is_an_unambiguous_user_argument(argument: str) -> bool:
has_id_or_mention = bool(IDConverter()._get_id_match(argument) or RE_USER_MENTION.match(argument))
# Check to see if the author passed a username (a discriminator exists)
- argument = argument.removeprefix('@')
- has_username = len(argument) > 5 and argument[-5] == '#'
+ argument = argument.removeprefix("@")
+ has_username = len(argument) > 5 and argument[-5] == "#"
return has_id_or_mention or has_username
@@ -448,8 +449,7 @@ class UnambiguousUser(UserConverter):
"""Convert the `argument` to a `discord.User`."""
if _is_an_unambiguous_user_argument(argument):
return await super().convert(ctx, argument)
- else:
- raise BadArgument(AMBIGUOUS_ARGUMENT_MSG.format(argument=argument))
+ raise BadArgument(AMBIGUOUS_ARGUMENT_MSG.format(argument=argument))
class UnambiguousMember(MemberConverter):
@@ -464,8 +464,7 @@ class UnambiguousMember(MemberConverter):
"""Convert the `argument` to a `discord.Member`."""
if _is_an_unambiguous_user_argument(argument):
return await super().convert(ctx, argument)
- else:
- raise BadArgument(AMBIGUOUS_ARGUMENT_MSG.format(argument=argument))
+ raise BadArgument(AMBIGUOUS_ARGUMENT_MSG.format(argument=argument))
class Infraction(Converter):
@@ -476,7 +475,7 @@ class Infraction(Converter):
obtain the most recent infraction by the actor.
"""
- async def convert(self, ctx: Context, arg: str) -> t.Optional[dict]:
+ async def convert(self, ctx: Context, arg: str) -> dict | None:
"""Attempts to convert `arg` into an infraction `dict`."""
if arg in ("l", "last", "recent"):
params = {
@@ -490,32 +489,30 @@ class Infraction(Converter):
raise BadArgument(
"Couldn't find most recent infraction; you have never given an infraction."
)
- else:
- return infractions[0]
+ return infractions[0]
- else:
- try:
- return await ctx.bot.api_client.get(f"bot/infractions/{arg}/expanded")
- except ResponseCodeError as e:
- if e.status == 404:
- raise InvalidInfraction(
- converter=Infraction,
- original=e,
- infraction_arg=arg
- )
- raise e
+ try:
+ return await ctx.bot.api_client.get(f"bot/infractions/{arg}/expanded")
+ except ResponseCodeError as e:
+ if e.status == 404:
+ raise InvalidInfractionError(
+ converter=Infraction,
+ original=e,
+ infraction_arg=arg
+ )
+ raise e
if t.TYPE_CHECKING:
ValidDiscordServerInvite = dict # noqa: F811
- ValidFilterListType = str # noqa: F811
+ ValidFilterListType = str
Extension = str # noqa: F811
PackageName = str # noqa: F811
ValidURL = str # noqa: F811
- Inventory = t.Tuple[str, _inventory_parser.InventoryDict] # noqa: F811
+ Inventory = tuple[str, _inventory_parser.InventoryDict] # noqa: F811
Snowflake = int # noqa: F811
SourceConverter = SourceType # noqa: F811
- DurationDelta = relativedelta # noqa: F811
+ DurationDelta = relativedelta
Duration = datetime # noqa: F811
Age = datetime # noqa: F811
OffTopicName = str # noqa: F811
@@ -525,7 +522,7 @@ if t.TYPE_CHECKING:
UnambiguousMember = discord.Member # noqa: F811
Infraction = t.Optional[dict] # noqa: F811
-Expiry = t.Union[Duration, ISODateTime]
-DurationOrExpiry = t.Union[DurationDelta, ISODateTime]
-MemberOrUser = t.Union[discord.Member, discord.User]
-UnambiguousMemberOrUser = t.Union[UnambiguousMember, UnambiguousUser]
+Expiry = Duration | ISODateTime
+DurationOrExpiry = DurationDelta | ISODateTime
+MemberOrUser = discord.Member | discord.User
+UnambiguousMemberOrUser = UnambiguousMember | UnambiguousUser
diff --git a/bot/decorators.py b/bot/decorators.py
index 2ddc7ee96..a68f85d5b 100644
--- a/bot/decorators.py
+++ b/bot/decorators.py
@@ -24,7 +24,7 @@ def in_whitelist(
channels: t.Container[int] = (),
categories: t.Container[int] = (),
roles: t.Container[int] = (),
- redirect: t.Optional[int] = Channels.bot_commands,
+ redirect: int | None = Channels.bot_commands,
fail_silently: bool = False,
) -> t.Callable:
"""
@@ -57,7 +57,7 @@ def not_in_blacklist(
categories: t.Container[int] = (),
roles: t.Container[int] = (),
override_roles: t.Container[int] = (),
- redirect: t.Optional[int] = Channels.bot_commands,
+ redirect: int | None = Channels.bot_commands,
fail_silently: bool = False,
) -> t.Callable:
"""
@@ -90,7 +90,7 @@ def not_in_blacklist(
return commands.check(predicate)
-def has_no_roles(*roles: t.Union[str, int]) -> t.Callable:
+def has_no_roles(*roles: str | int) -> t.Callable:
"""
Returns True if the user does not have any of the roles specified.
@@ -111,9 +111,9 @@ def has_no_roles(*roles: t.Union[str, int]) -> t.Callable:
def redirect_output(
destination_channel: int,
- bypass_roles: t.Optional[t.Container[int]] = None,
- channels: t.Optional[t.Container[int]] = None,
- categories: t.Optional[t.Container[int]] = None,
+ bypass_roles: t.Container[int] | None = None,
+ channels: t.Container[int] | None = None,
+ categories: t.Container[int] | None = None,
ping_user: bool = True
) -> t.Callable:
"""
@@ -138,12 +138,12 @@ def redirect_output(
await func(self, ctx, *args, **kwargs)
return
- elif channels and ctx.channel.id not in channels:
+ if channels and ctx.channel.id not in channels:
log.trace(f"{ctx.author} used {ctx.command} in a channel that can bypass output redirection")
await func(self, ctx, *args, **kwargs)
return
- elif categories and ctx.channel.category.id not in categories:
+ if categories and ctx.channel.category.id not in categories:
log.trace(f"{ctx.author} used {ctx.command} in a category that can bypass output redirection")
await func(self, ctx, *args, **kwargs)
return
@@ -213,9 +213,10 @@ def respect_role_hierarchy(member_arg: function.Argument) -> t.Callable:
f":x: {actor.mention}, you may not {cmd} "
"someone with an equal or higher top role."
)
- else:
- log.trace(f"{func.__name__}: {target.top_role=} < {actor.top_role=}; calling func")
- return await func(*args, **kwargs)
+ return None
+
+ log.trace(f"{func.__name__}: {target.top_role=} < {actor.top_role=}; calling func")
+ return await func(*args, **kwargs)
return wrapper
return decorator
@@ -265,7 +266,7 @@ def ensure_future_timestamp(timestamp_arg: function.Argument) -> t.Callable:
is_future = True
if not is_future:
await ctx.send(":x: Provided timestamp is in the past.")
- return
+ return None
return await func(*args, **kwargs)
return wrapper
diff --git a/bot/errors.py b/bot/errors.py
index 078b645f1..7949dc1b4 100644
--- a/bot/errors.py
+++ b/bot/errors.py
@@ -1,6 +1,7 @@
from __future__ import annotations
-from typing import Hashable, TYPE_CHECKING, Union
+from collections.abc import Hashable
+from typing import TYPE_CHECKING
from discord.ext.commands import ConversionError, Converter
@@ -43,7 +44,7 @@ class InvalidInfractedUserError(Exception):
super().__init__(reason)
-class InvalidInfraction(ConversionError):
+class InvalidInfractionError(ConversionError):
"""
Raised by the Infraction converter when trying to fetch an invalid infraction id.
@@ -51,16 +52,15 @@ class InvalidInfraction(ConversionError):
`infraction_arg` -- the value that we attempted to convert into an Infraction
"""
- def __init__(self, converter: Converter, original: Exception, infraction_arg: Union[int, str]):
+ def __init__(self, converter: Converter, original: Exception, infraction_arg: int | str):
self.infraction_arg = infraction_arg
super().__init__(converter, original)
-class BrandingMisconfiguration(RuntimeError):
+class BrandingMisconfigurationError(RuntimeError):
"""Raised by the Branding cog when a misconfigured event is encountered."""
- pass
class NonExistentRoleError(ValueError):
diff --git a/bot/exts/backend/branding/_cog.py b/bot/exts/backend/branding/_cog.py
index 94429c172..5d194ec3e 100644
--- a/bot/exts/backend/branding/_cog.py
+++ b/bot/exts/backend/branding/_cog.py
@@ -269,7 +269,7 @@ class Branding(commands.Cog):
log.debug(f"Sending event information event to channel: {channel_id} ({is_notification=}).")
await self.bot.wait_until_guild_available()
- channel: t.Optional[discord.TextChannel] = self.bot.get_channel(channel_id)
+ channel: discord.TextChannel | None = self.bot.get_channel(channel_id)
if channel is None:
log.warning(f"Cannot send event information: channel {channel_id} not found!")
@@ -291,7 +291,7 @@ class Branding(commands.Cog):
await channel.send(content=content, embed=embed)
- async def enter_event(self, event: Event) -> t.Tuple[bool, bool]:
+ async def enter_event(self, event: Event) -> tuple[bool, bool]:
"""
Apply `event` assets and update information cache.
@@ -331,7 +331,7 @@ class Branding(commands.Cog):
return banner_success, icon_success
- async def synchronise(self) -> t.Tuple[bool, bool]:
+ async def synchronise(self) -> tuple[bool, bool]:
"""
Fetch the current event and delegate to `enter_event`.
@@ -353,7 +353,7 @@ class Branding(commands.Cog):
return await self.enter_event(current_event)
- async def populate_cache_events(self, events: t.List[Event]) -> None:
+ async def populate_cache_events(self, events: list[Event]) -> None:
"""
Clear `cache_events` and re-populate with names and durations of `events`.
@@ -402,7 +402,7 @@ class Branding(commands.Cog):
"""
log.debug("Checking whether daemon should start.")
- should_begin: t.Optional[bool] = await self.cache_information.get("daemon_active") # None if never set!
+ should_begin: bool | None = await self.cache_information.get("daemon_active") # None if never set!
if should_begin:
self.daemon_loop.start()
diff --git a/bot/exts/backend/branding/_repository.py b/bot/exts/backend/branding/_repository.py
index e14f0a1ef..db2061faa 100644
--- a/bot/exts/backend/branding/_repository.py
+++ b/bot/exts/backend/branding/_repository.py
@@ -1,11 +1,11 @@
import typing as t
-from datetime import date, datetime
+from datetime import UTC, date, datetime
import frontmatter
from bot.bot import Bot
from bot.constants import Keys
-from bot.errors import BrandingMisconfiguration
+from bot.errors import BrandingMisconfigurationError
from bot.log import get_logger
# Base URL for requests into the branding repository.
@@ -39,9 +39,9 @@ class RemoteObject:
name: str # Filename.
path: str # Path from repo root.
type: str # Either 'file' or 'dir'.
- download_url: t.Optional[str] # If type is 'dir', this is None!
+ download_url: str | None # If type is 'dir', this is None!
- def __init__(self, dictionary: t.Dict[str, t.Any]) -> None:
+ def __init__(self, dictionary: dict[str, t.Any]) -> None:
"""Initialize by grabbing annotated attributes from `dictionary`."""
missing_keys = self.__annotations__.keys() - dictionary.keys()
if missing_keys:
@@ -54,8 +54,8 @@ class MetaFile(t.NamedTuple):
"""Attributes defined in a 'meta.md' file."""
is_fallback: bool
- start_date: t.Optional[date]
- end_date: t.Optional[date]
+ start_date: date | None
+ end_date: date | None
description: str # Markdown event description.
@@ -93,7 +93,7 @@ class BrandingRepository:
def __init__(self, bot: Bot) -> None:
self.bot = bot
- async def fetch_directory(self, path: str, types: t.Container[str] = ("file", "dir")) -> t.Dict[str, RemoteObject]:
+ async def fetch_directory(self, path: str, types: t.Container[str] = ("file", "dir")) -> dict[str, RemoteObject]:
"""
Fetch directory found at `path` in the branding repository.
@@ -137,7 +137,7 @@ class BrandingRepository:
attrs, description = frontmatter.parse(raw_file, encoding="UTF-8")
if not description:
- raise BrandingMisconfiguration("No description found in 'meta.md'!")
+ raise BrandingMisconfigurationError("No description found in 'meta.md'!")
if attrs.get("fallback", False):
return MetaFile(is_fallback=True, start_date=None, end_date=None, description=description)
@@ -146,12 +146,12 @@ class BrandingRepository:
end_date_raw = attrs.get("end_date")
if None in (start_date_raw, end_date_raw):
- raise BrandingMisconfiguration("Non-fallback event doesn't have start and end dates defined!")
+ raise BrandingMisconfigurationError("Non-fallback event doesn't have start and end dates defined!")
# We extend the configured month & day with an arbitrary leap year, allowing a datetime object to exist.
# This may raise errors if misconfigured. We let the caller handle such cases.
- start_date = datetime.strptime(f"{start_date_raw} {ARBITRARY_YEAR}", DATE_FMT).date()
- end_date = datetime.strptime(f"{end_date_raw} {ARBITRARY_YEAR}", DATE_FMT).date()
+ start_date = datetime.strptime(f"{start_date_raw} {ARBITRARY_YEAR}", DATE_FMT).replace(tzinfo=UTC).date()
+ end_date = datetime.strptime(f"{end_date_raw} {ARBITRARY_YEAR}", DATE_FMT).replace(tzinfo=UTC).date()
return MetaFile(is_fallback=False, start_date=start_date, end_date=end_date, description=description)
@@ -166,15 +166,15 @@ class BrandingRepository:
missing_assets = {"meta.md", "server_icons", "banners"} - contents.keys()
if missing_assets:
- raise BrandingMisconfiguration(f"Directory is missing following assets: {missing_assets}")
+ raise BrandingMisconfigurationError(f"Directory is missing following assets: {missing_assets}")
server_icons = await self.fetch_directory(contents["server_icons"].path, types=("file",))
banners = await self.fetch_directory(contents["banners"].path, types=("file",))
if len(server_icons) == 0:
- raise BrandingMisconfiguration("Found no server icons!")
+ raise BrandingMisconfigurationError("Found no server icons!")
if len(banners) == 0:
- raise BrandingMisconfiguration("Found no server banners!")
+ raise BrandingMisconfigurationError("Found no server banners!")
meta_bytes = await self.fetch_file(contents["meta.md"].download_url)
@@ -182,7 +182,7 @@ class BrandingRepository:
return Event(directory.path, meta_file, list(banners.values()), list(server_icons.values()))
- async def get_events(self) -> t.List[Event]:
+ async def get_events(self) -> list[Event]:
"""
Discover available events in the branding repository.
@@ -196,7 +196,7 @@ class BrandingRepository:
log.exception("Failed to fetch 'events' directory.")
return []
- instances: t.List[Event] = []
+ instances: list[Event] = []
for event_directory in event_directories.values():
log.trace(f"Attempting to construct event from directory: '{event_directory.path}'.")
@@ -209,7 +209,7 @@ class BrandingRepository:
return instances
- async def get_current_event(self) -> t.Tuple[t.Optional[Event], t.List[Event]]:
+ async def get_current_event(self) -> tuple[Event | None, list[Event]]:
"""
Get the currently active event, or the fallback event.
@@ -218,7 +218,7 @@ class BrandingRepository:
The current event may be None in the case that no event is active, and no fallback event is found.
"""
- utc_now = datetime.utcnow()
+ utc_now = datetime.now(tz=UTC)
log.debug(f"Finding active event for: {utc_now}.")
# Construct an object in the arbitrary year for the purpose of comparison.
diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py
index 8883f7566..0c8938918 100644
--- a/bot/exts/backend/error_handler.py
+++ b/bot/exts/backend/error_handler.py
@@ -75,7 +75,7 @@ class ErrorHandler(Cog):
elif isinstance(e, errors.CheckFailure):
log.debug(debug_message)
await self.handle_check_failure(ctx, e)
- elif isinstance(e, (errors.CommandOnCooldown, errors.MaxConcurrencyReached)):
+ elif isinstance(e, errors.CommandOnCooldown | errors.MaxConcurrencyReached):
log.debug(debug_message)
await ctx.send(e)
elif isinstance(e, errors.CommandInvokeError):
@@ -154,7 +154,7 @@ class ErrorHandler(Cog):
if command.startswith("shh"):
await ctx.invoke(silence_command, duration_or_channel=channel, duration=duration, kick=kick)
return True
- elif command.startswith("unshh"):
+ if command.startswith("unshh"):
await ctx.invoke(self.bot.get_command("unsilence"), channel=channel)
return True
return False
@@ -319,7 +319,7 @@ class ErrorHandler(Cog):
await ctx.send(
"Sorry, it looks like I don't have the permissions or roles I need to do that."
)
- elif isinstance(e, (ContextCheckFailure, errors.NoPrivateMessage)):
+ elif isinstance(e, ContextCheckFailure | errors.NoPrivateMessage):
ctx.bot.stats.incr("errors.wrong_channel_or_dm_error")
await ctx.send(e)
diff --git a/bot/exts/backend/sync/_cog.py b/bot/exts/backend/sync/_cog.py
index 8c7dbb54e..83ebb70d2 100644
--- a/bot/exts/backend/sync/_cog.py
+++ b/bot/exts/backend/sync/_cog.py
@@ -1,5 +1,5 @@
import asyncio
-from typing import Any, Dict
+from typing import Any
from discord import Member, Role, User
from discord.ext import commands
@@ -48,7 +48,7 @@ class Sync(Cog):
for syncer in (_syncers.RoleSyncer, _syncers.UserSyncer):
await syncer.sync(guild)
- async def patch_user(self, user_id: int, json: Dict[str, Any], ignore_404: bool = False) -> None:
+ async def patch_user(self, user_id: int, json: dict[str, Any], ignore_404: bool = False) -> None:
"""Send a PATCH request to partially update a user in the database."""
try:
await self.bot.api_client.patch(f"bot/users/{user_id}", json=json)
@@ -65,13 +65,13 @@ class Sync(Cog):
return
await self.bot.api_client.post(
- 'bot/roles',
+ "bot/roles",
json={
- 'colour': role.colour.value,
- 'id': role.id,
- 'name': role.name,
- 'permissions': role.permissions.value,
- 'position': role.position,
+ "colour": role.colour.value,
+ "id": role.id,
+ "name": role.name,
+ "permissions": role.permissions.value,
+ "position": role.position,
}
)
@@ -81,7 +81,7 @@ class Sync(Cog):
if role.guild.id != constants.Guild.id:
return
- await self.bot.api_client.delete(f'bot/roles/{role.id}')
+ await self.bot.api_client.delete(f"bot/roles/{role.id}")
@Cog.listener()
async def on_guild_role_update(self, before: Role, after: Role) -> None:
@@ -98,13 +98,13 @@ class Sync(Cog):
if was_updated:
await self.bot.api_client.put(
- f'bot/roles/{after.id}',
+ f"bot/roles/{after.id}",
json={
- 'colour': after.colour.value,
- 'id': after.id,
- 'name': after.name,
- 'permissions': after.permissions.value,
- 'position': after.position,
+ "colour": after.colour.value,
+ "id": after.id,
+ "name": after.name,
+ "permissions": after.permissions.value,
+ "position": after.position,
}
)
@@ -121,11 +121,11 @@ class Sync(Cog):
return
packed = {
- 'discriminator': int(member.discriminator),
- 'id': member.id,
- 'in_guild': True,
- 'name': member.name,
- 'roles': sorted(role.id for role in member.roles)
+ "discriminator": int(member.discriminator),
+ "id": member.id,
+ "in_guild": True,
+ "name": member.name,
+ "roles": sorted(role.id for role in member.roles)
}
got_error = False
@@ -133,7 +133,7 @@ class Sync(Cog):
try:
# First try an update of the user to set the `in_guild` field and other
# fields that may have changed since the last time we've seen them.
- await self.bot.api_client.put(f'bot/users/{member.id}', json=packed)
+ await self.bot.api_client.put(f"bot/users/{member.id}", json=packed)
except ResponseCodeError as e:
# If we didn't get 404, something else broke - propagate it up.
@@ -144,7 +144,7 @@ class Sync(Cog):
if got_error:
# If we got `404`, the user is new. Create them.
- await self.bot.api_client.post('bot/users', json=packed)
+ await self.bot.api_client.post("bot/users", json=packed)
@Cog.listener()
async def on_member_remove(self, member: Member) -> None:
@@ -176,18 +176,18 @@ class Sync(Cog):
# A 404 likely means the user is in another guild.
await self.patch_user(after.id, json=updated_information, ignore_404=True)
- @commands.group(name='sync')
+ @commands.group(name="sync")
@commands.has_permissions(administrator=True)
async def sync_group(self, ctx: Context) -> None:
"""Run synchronizations between the bot and site manually."""
- @sync_group.command(name='roles')
+ @sync_group.command(name="roles")
@commands.has_permissions(administrator=True)
async def sync_roles_command(self, ctx: Context) -> None:
"""Manually synchronise the guild's roles with the roles on the site."""
await _syncers.RoleSyncer.sync(ctx.guild, ctx)
- @sync_group.command(name='users')
+ @sync_group.command(name="users")
@commands.has_permissions(administrator=True)
async def sync_users_command(self, ctx: Context) -> None:
"""Manually synchronise the guild's users with the users on the site."""
diff --git a/bot/exts/backend/sync/_syncers.py b/bot/exts/backend/sync/_syncers.py
index f68674f8d..cd7f5040d 100644
--- a/bot/exts/backend/sync/_syncers.py
+++ b/bot/exts/backend/sync/_syncers.py
@@ -17,8 +17,8 @@ CHUNK_SIZE = 1000
# These objects are declared as namedtuples because tuples are hashable,
# something that we make use of when diffing site roles against guild roles.
-_Role = namedtuple('Role', ('id', 'name', 'colour', 'permissions', 'position'))
-_Diff = namedtuple('Diff', ('created', 'updated', 'deleted'))
+_Role = namedtuple("Role", ("id", "name", "colour", "permissions", "position"))
+_Diff = namedtuple("Diff", ("created", "updated", "deleted"))
# Implementation of static abstract methods are not enforced if the subclass is never instantiated.
@@ -46,7 +46,7 @@ class Syncer(abc.ABC):
raise NotImplementedError # pragma: no cover
@classmethod
- async def sync(cls, guild: Guild, ctx: t.Optional[Context] = None) -> None:
+ async def sync(cls, guild: Guild, ctx: Context | None = None) -> None:
"""
Synchronise the database with the cache of `guild`.
@@ -89,7 +89,7 @@ class RoleSyncer(Syncer):
async def _get_diff(guild: Guild) -> _Diff:
"""Return the difference of roles between the cache of `guild` and the database."""
log.trace("Getting the diff for roles.")
- roles = await bot.instance.api_client.get('bot/roles')
+ roles = await bot.instance.api_client.get("bot/roles")
# Pack DB roles and guild roles into one common, hashable format.
# They're hashable so that they're easily comparable with sets later.
@@ -123,15 +123,15 @@ class RoleSyncer(Syncer):
"""Synchronise the database with the role cache of `guild`."""
log.trace("Syncing created roles...")
for role in diff.created:
- await bot.instance.api_client.post('bot/roles', json=role._asdict())
+ await bot.instance.api_client.post("bot/roles", json=role._asdict())
log.trace("Syncing updated roles...")
for role in diff.updated:
- await bot.instance.api_client.put(f'bot/roles/{role.id}', json=role._asdict())
+ await bot.instance.api_client.put(f"bot/roles/{role.id}", json=role._asdict())
log.trace("Syncing deleted roles...")
for role in diff.deleted:
- await bot.instance.api_client.delete(f'bot/roles/{role.id}')
+ await bot.instance.api_client.delete(f"bot/roles/{role.id}")
class UserSyncer(Syncer):
@@ -152,7 +152,7 @@ class UserSyncer(Syncer):
# Store user fields which are to be updated.
updated_fields = {}
- def maybe_update(db_field: str, guild_value: t.Union[str, int]) -> None:
+ def maybe_update(db_field: str, guild_value: str | int) -> None:
# Equalize DB user and guild user attributes.
if db_user[db_field] != guild_value: # noqa: B023
updated_fields[db_field] = guild_value # noqa: B023
diff --git a/bot/exts/events/__init__.py b/bot/exts/events/__init__.py
deleted file mode 100644
index e69de29bb..000000000
--- a/bot/exts/events/__init__.py
+++ /dev/null
diff --git a/bot/exts/events/code_jams/__init__.py b/bot/exts/events/code_jams/__init__.py
deleted file mode 100644
index 2f858d1f9..000000000
--- a/bot/exts/events/code_jams/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from bot.bot import Bot
-
-
-async def setup(bot: Bot) -> None:
- """Load the CodeJams cog."""
- from bot.exts.events.code_jams._cog import CodeJams
-
- await bot.add_cog(CodeJams(bot))
diff --git a/bot/exts/events/code_jams/_channels.py b/bot/exts/events/code_jams/_channels.py
deleted file mode 100644
index e8cf5f7bf..000000000
--- a/bot/exts/events/code_jams/_channels.py
+++ /dev/null
@@ -1,113 +0,0 @@
-import typing as t
-
-import discord
-
-from bot.constants import Categories, Channels, Roles
-from bot.log import get_logger
-
-log = get_logger(__name__)
-
-MAX_CHANNELS = 50
-CATEGORY_NAME = "Code Jam"
-
-
-async def _get_category(guild: discord.Guild) -> discord.CategoryChannel:
- """
- Return a code jam category.
-
- If all categories are full or none exist, create a new category.
- """
- for category in guild.categories:
- if category.name == CATEGORY_NAME and len(category.channels) < MAX_CHANNELS:
- return category
-
- return await _create_category(guild)
-
-
-async def _create_category(guild: discord.Guild) -> discord.CategoryChannel:
- """Create a new code jam category and return it."""
- log.info("Creating a new code jam category.")
-
- category_overwrites = {
- guild.default_role: discord.PermissionOverwrite(read_messages=False),
- guild.me: discord.PermissionOverwrite(read_messages=True)
- }
-
- category = await guild.create_category_channel(
- CATEGORY_NAME,
- overwrites=category_overwrites,
- reason="It's code jam time!"
- )
-
- await _send_status_update(
- guild, f"Created a new category with the ID {category.id} for this Code Jam's team channels."
- )
-
- return category
-
-
-def _get_overwrites(
- members: list[tuple[discord.Member, bool]],
- guild: discord.Guild,
-) -> dict[t.Union[discord.Member, discord.Role], discord.PermissionOverwrite]:
- """Get code jam team channels permission overwrites."""
- team_channel_overwrites = {
- guild.default_role: discord.PermissionOverwrite(read_messages=False),
- guild.get_role(Roles.code_jam_event_team): discord.PermissionOverwrite(read_messages=True)
- }
-
- for member, _ in members:
- team_channel_overwrites[member] = discord.PermissionOverwrite(
- read_messages=True
- )
-
- return team_channel_overwrites
-
-
-async def create_team_channel(
- guild: discord.Guild,
- team_name: str,
- members: list[tuple[discord.Member, bool]],
- team_leaders: discord.Role
-) -> None:
- """Create the team's text channel."""
- await _add_team_leader_roles(members, team_leaders)
-
- # Get permission overwrites and category
- team_channel_overwrites = _get_overwrites(members, guild)
- code_jam_category = await _get_category(guild)
-
- # Create a text channel for the team
- await code_jam_category.create_text_channel(
- team_name,
- overwrites=team_channel_overwrites,
- )
-
-
-async def create_team_leader_channel(guild: discord.Guild, team_leaders: discord.Role) -> None:
- """Create the Team Leader Chat channel for the Code Jam team leaders."""
- category: discord.CategoryChannel = guild.get_channel(Categories.summer_code_jam)
-
- team_leaders_chat = await category.create_text_channel(
- name="team-leaders-chat",
- overwrites={
- guild.default_role: discord.PermissionOverwrite(read_messages=False),
- team_leaders: discord.PermissionOverwrite(read_messages=True)
- }
- )
-
- await _send_status_update(guild, f"Created {team_leaders_chat.mention} in the {category} category.")
-
-
-async def _send_status_update(guild: discord.Guild, message: str) -> None:
- """Inform the events lead with a status update when the command is ran."""
- channel: discord.TextChannel = guild.get_channel(Channels.code_jam_planning)
-
- await channel.send(f"<@&{Roles.events_lead}>\n\n{message}")
-
-
-async def _add_team_leader_roles(members: list[tuple[discord.Member, bool]], team_leaders: discord.Role) -> None:
- """Assign the team leader role to the team leaders."""
- for member, is_leader in members:
- if is_leader:
- await member.add_roles(team_leaders)
diff --git a/bot/exts/events/code_jams/_cog.py b/bot/exts/events/code_jams/_cog.py
deleted file mode 100644
index 86c357863..000000000
--- a/bot/exts/events/code_jams/_cog.py
+++ /dev/null
@@ -1,239 +0,0 @@
-import asyncio
-import csv
-import typing as t
-from collections import defaultdict
-
-import discord
-from discord import Colour, Embed, Guild, Member
-from discord.ext import commands
-
-from bot.bot import Bot
-from bot.constants import Emojis, Roles
-from bot.exts.events.code_jams import _channels
-from bot.log import get_logger
-from bot.utils.members import get_or_fetch_member
-from bot.utils.services import PasteTooLongError, PasteUploadError, send_to_paste_service
-
-log = get_logger(__name__)
-
-TEAM_LEADERS_COLOUR = 0x11806a
-DELETION_REACTION = "\U0001f4a5"
-
-
-class CodeJams(commands.Cog):
- """Manages the code-jam related parts of our server."""
-
- def __init__(self, bot: Bot):
- self.bot = bot
-
- @commands.group(aliases=("cj", "jam"))
- @commands.has_any_role(Roles.admins)
- async def codejam(self, ctx: commands.Context) -> None:
- """A Group of commands for managing Code Jams."""
- if ctx.invoked_subcommand is None:
- await ctx.send_help(ctx.command)
-
- @codejam.command()
- async def create(self, ctx: commands.Context, csv_file: t.Optional[str] = None) -> None:
- """
- Create code-jam teams from a CSV file or a link to one, specifying the team names, leaders and members.
-
- The CSV file must have 3 columns: 'Team Name', 'Team Member Discord ID', and 'Team Leader'.
-
- This will create the text channels for the teams, and give the team leaders their roles.
- """
- async with ctx.typing():
- if csv_file:
- async with self.bot.http_session.get(csv_file) as response:
- if response.status != 200:
- await ctx.send(f"Got a bad response from the URL: {response.status}")
- return
-
- csv_file = await response.text()
-
- elif ctx.message.attachments:
- csv_file = (await ctx.message.attachments[0].read()).decode("utf8")
- else:
- raise commands.BadArgument("You must include either a CSV file or a link to one.")
-
- teams = defaultdict(list)
- reader = csv.DictReader(csv_file.splitlines())
-
- for row in reader:
- member = await get_or_fetch_member(ctx.guild, int(row["Team Member Discord ID"]))
-
- if member is None:
- log.trace(f"Got an invalid member ID: {row['Team Member Discord ID']}")
- continue
-
- teams[row["Team Name"]].append((member, row["Team Leader"].upper() == "Y"))
-
- team_leaders = await ctx.guild.create_role(name="Code Jam Team Leaders", colour=TEAM_LEADERS_COLOUR)
-
- for team_name, team_members in teams.items():
- await _channels.create_team_channel(ctx.guild, team_name, team_members, team_leaders)
-
- await _channels.create_team_leader_channel(ctx.guild, team_leaders)
- await ctx.send(f"{Emojis.check_mark} Created Code Jam with {len(teams)} teams.")
-
- @codejam.command()
- @commands.has_any_role(Roles.admins)
- async def end(self, ctx: commands.Context) -> None:
- """
- Delete all code jam channels.
-
- A confirmation message is displayed with the categories and channels to be deleted.. Pressing the added reaction
- deletes those channels.
- """
- def predicate_deletion_emoji_reaction(reaction: discord.Reaction, user: discord.User) -> bool:
- """Return True if the reaction :boom: was added by the context message author on this message."""
- return (
- reaction.message.id == message.id
- and user.id == ctx.author.id
- and str(reaction) == DELETION_REACTION
- )
-
- # A copy of the list of channels is stored. This is to make sure that we delete precisely the channels displayed
- # in the confirmation message.
- categories = self.jam_categories(ctx.guild)
- category_channels = {category: category.channels.copy() for category in categories}
-
- confirmation_message = await self._build_confirmation_message(category_channels)
- message = await ctx.send(confirmation_message)
- await message.add_reaction(DELETION_REACTION)
- try:
- await self.bot.wait_for(
- 'reaction_add',
- check=predicate_deletion_emoji_reaction,
- timeout=10
- )
-
- except asyncio.TimeoutError:
- await message.clear_reaction(DELETION_REACTION)
- await ctx.send("Command timed out.", reference=message)
- return
-
- else:
- await message.clear_reaction(DELETION_REACTION)
- for category, channels in category_channels.items():
- for channel in channels:
- await channel.delete(reason="Code jam ended.")
- await category.delete(reason="Code jam ended.")
-
- await message.add_reaction(Emojis.check_mark)
-
- @staticmethod
- async def _build_confirmation_message(
- categories: dict[discord.CategoryChannel, list[discord.abc.GuildChannel]]
- ) -> str:
- """Sends details of the channels to be deleted to the pasting service, and formats the confirmation message."""
- def channel_repr(channel: discord.abc.GuildChannel) -> str:
- """Formats the channel name and ID and a readable format."""
- return f"{channel.name} ({channel.id})"
-
- def format_category_info(category: discord.CategoryChannel, channels: list[discord.abc.GuildChannel]) -> str:
- """Displays the category and the channels within it in a readable format."""
- return f"{channel_repr(category)}:\n" + "\n".join(" - " + channel_repr(channel) for channel in channels)
-
- deletion_details = "\n\n".join(
- format_category_info(category, channels) for category, channels in categories.items()
- )
-
- try:
- message = await send_to_paste_service(deletion_details)
- except PasteTooLongError:
- message = "**Too long to upload to paste service.**"
- except PasteUploadError:
- message = "**Failed to upload to paste service.**"
-
- return f"Are you sure you want to delete all code jam channels?\n\nThe channels to be deleted: {message}"
-
- @codejam.command()
- @commands.has_any_role(Roles.admins, Roles.code_jam_event_team)
- async def info(self, ctx: commands.Context, member: Member) -> None:
- """
- Send an info embed about the member with the team they're in.
-
- The team is found by searching the permissions of the team channels.
- """
- channel = self.team_channel(ctx.guild, member)
- if not channel:
- await ctx.send(":x: I can't find the team channel for this member.")
- return
-
- embed = Embed(
- title=str(member),
- colour=Colour.og_blurple()
- )
- embed.add_field(name="Team", value=self.team_name(channel), inline=True)
-
- await ctx.send(embed=embed)
-
- @codejam.command()
- @commands.has_any_role(Roles.admins)
- async def move(self, ctx: commands.Context, member: Member, new_team_name: str) -> None:
- """Move participant from one team to another by changing the user's permissions for the relevant channels."""
- old_team_channel = self.team_channel(ctx.guild, member)
- if not old_team_channel:
- await ctx.send(":x: I can't find the team channel for this member.")
- return
-
- if old_team_channel.name == new_team_name or self.team_name(old_team_channel) == new_team_name:
- await ctx.send(f"`{member}` is already in `{new_team_name}`.")
- return
-
- new_team_channel = self.team_channel(ctx.guild, new_team_name)
- if not new_team_channel:
- await ctx.send(f":x: I can't find a team channel named `{new_team_name}`.")
- return
-
- await old_team_channel.set_permissions(member, overwrite=None, reason=f"Participant moved to {new_team_name}")
- await new_team_channel.set_permissions(
- member,
- overwrite=discord.PermissionOverwrite(read_messages=True),
- reason=f"Participant moved from {old_team_channel.name}"
- )
-
- await ctx.send(
- f"Participant moved from `{self.team_name(old_team_channel)}` to `{self.team_name(new_team_channel)}`."
- )
-
- @codejam.command()
- @commands.has_any_role(Roles.admins)
- async def remove(self, ctx: commands.Context, member: Member) -> None:
- """Remove the participant from their team. Does not remove the participants or leader roles."""
- channel = self.team_channel(ctx.guild, member)
- if not channel:
- await ctx.send(":x: I can't find the team channel for this member.")
- return
-
- await channel.set_permissions(
- member,
- overwrite=None,
- reason=f"Participant removed from the team {self.team_name(channel)}."
- )
- await ctx.send(f"Removed the participant from `{self.team_name(channel)}`.")
-
- @staticmethod
- def jam_categories(guild: Guild) -> list[discord.CategoryChannel]:
- """Get all the code jam team categories."""
- return [category for category in guild.categories if category.name == _channels.CATEGORY_NAME]
-
- @staticmethod
- def team_channel(guild: Guild, criterion: t.Union[str, Member]) -> t.Optional[discord.TextChannel]:
- """Get a team channel through either a participant or the team name."""
- for category in CodeJams.jam_categories(guild):
- for channel in category.channels:
- if isinstance(channel, discord.TextChannel):
- if (
- # If it's a string.
- criterion == channel.name or criterion == CodeJams.team_name(channel)
- # If it's a member.
- or criterion in channel.overwrites
- ):
- return channel
-
- @staticmethod
- def team_name(channel: discord.TextChannel) -> str:
- """Retrieves the team name from the given channel."""
- return channel.name.replace("-", " ").title()
diff --git a/bot/exts/filtering/_filter_context.py b/bot/exts/filtering/_filter_context.py
index 9d8bbba9a..31cea0bfd 100644
--- a/bot/exts/filtering/_filter_context.py
+++ b/bot/exts/filtering/_filter_context.py
@@ -48,7 +48,7 @@ class FilterContext:
action_descriptions: list[str] = field(default_factory=list) # What actions were taken
matches: list[str] = field(default_factory=list) # What exactly was found
notification_domain: str = "" # A domain to send the user for context
- filter_info: dict['Filter', str] = field(default_factory=dict) # Additional info from a filter.
+ filter_info: dict[Filter, str] = field(default_factory=dict) # Additional info from a filter.
messages_deletion: bool = False # Whether the messages were deleted. Can't upload deletion log otherwise.
blocked_exts: set[str] = field(default_factory=set) # Any extensions blocked (used for snekbox)
# Additional actions to perform
diff --git a/bot/exts/filtering/_filter_lists/__init__.py b/bot/exts/filtering/_filter_lists/__init__.py
index 82e0452f9..1273e5588 100644
--- a/bot/exts/filtering/_filter_lists/__init__.py
+++ b/bot/exts/filtering/_filter_lists/__init__.py
@@ -1,9 +1,9 @@
from os.path import dirname
-from bot.exts.filtering._filter_lists.filter_list import FilterList, ListType, list_type_converter
+from bot.exts.filtering._filter_lists.filter_list import FilterList, ListType, ListTypeConverter
from bot.exts.filtering._utils import subclasses_in_package
filter_list_types = subclasses_in_package(dirname(__file__), f"{__name__}.", FilterList)
filter_list_types = {filter_list.name: filter_list for filter_list in filter_list_types}
-__all__ = [filter_list_types, FilterList, ListType, list_type_converter]
+__all__ = [filter_list_types, FilterList, ListType, ListTypeConverter]
diff --git a/bot/exts/filtering/_filter_lists/antispam.py b/bot/exts/filtering/_filter_lists/antispam.py
index 94f80e6eb..22f35f40e 100644
--- a/bot/exts/filtering/_filter_lists/antispam.py
+++ b/bot/exts/filtering/_filter_lists/antispam.py
@@ -40,7 +40,7 @@ class AntispamList(UniquesListBase):
name = "antispam"
- def __init__(self, filtering_cog: 'Filtering'):
+ def __init__(self, filtering_cog: "Filtering"):
super().__init__(filtering_cog)
self.message_deletion_queue: dict[Member, DeletionContext] = dict()
@@ -98,7 +98,7 @@ class AntispamList(UniquesListBase):
if not current_infraction or new_infraction.infraction_type.value < current_infraction.value:
# Pick the first triggered filter for the reason, there's no good way to decide between them.
new_infraction.infraction_reason = (
- f"{triggers[0].name.replace('_', ' ')} spam – {ctx.filter_info[triggers[0]]}"
+ f"{triggers[0].name.replace('_', ' ')} spam - {ctx.filter_info[triggers[0]]}"
)
current_actions[InfractionAndNotification.name] = new_infraction
self.message_deletion_queue[ctx.author].current_infraction = new_infraction.infraction_type
diff --git a/bot/exts/filtering/_filter_lists/extension.py b/bot/exts/filtering/_filter_lists/extension.py
index d805fa7aa..84bd68aaa 100644
--- a/bot/exts/filtering/_filter_lists/extension.py
+++ b/bot/exts/filtering/_filter_lists/extension.py
@@ -103,7 +103,7 @@ class ExtensionsList(FilterList[ExtensionFilter]):
else:
meta_channel = bot.instance.get_channel(Channels.meta)
if not self._whitelisted_description:
- self._whitelisted_description = ', '.join(
+ self._whitelisted_description = ", ".join(
filter_.content for filter_ in self[ListType.ALLOW].filters.values()
)
ctx.dm_embed = DISALLOWED_EMBED_DESCRIPTION.format(
diff --git a/bot/exts/filtering/_filter_lists/filter_list.py b/bot/exts/filtering/_filter_lists/filter_list.py
index d4c975766..e5b6b2a65 100644
--- a/bot/exts/filtering/_filter_lists/filter_list.py
+++ b/bot/exts/filtering/_filter_lists/filter_list.py
@@ -9,7 +9,7 @@ from functools import reduce
from typing import Any
import arrow
-from discord.ext.commands import BadArgument
+from discord.ext.commands import BadArgument, Context, Converter
from bot.exts.filtering._filter_context import Event, FilterContext
from bot.exts.filtering._filters.filter import Filter, UniqueFilter
@@ -37,13 +37,15 @@ aliases = (
)
-def list_type_converter(argument: str) -> ListType:
+class ListTypeConverter(Converter):
"""A converter to get the appropriate list type."""
- argument = argument.lower()
- for list_type, list_aliases in aliases:
- if argument in list_aliases or argument in map(past_tense, list_aliases):
- return list_type
- raise BadArgument(f"No matching list type found for {argument!r}.")
+
+ async def convert(self, ctx: Context, argument: str) -> ListType:
+ argument = argument.lower()
+ for list_type, list_aliases in aliases:
+ if argument in list_aliases or argument in map(past_tense, list_aliases):
+ return list_type
+ raise BadArgument(f"No matching list type found for {argument!r}.")
# AtomicList and its subclasses must have eq=False, otherwise the dataclass deco will replace the hash function.
@@ -219,7 +221,7 @@ class FilterList(dict[ListType, AtomicList], typing.Generic[T], FieldRequiring):
filter_type = self.get_filter_type(content)
if filter_type:
return filter_type(filter_data, defaults)
- elif content not in self._already_warned:
+ if content not in self._already_warned:
log.warning(f"A filter named {content} was supplied, but no matching implementation found.")
self._already_warned.add(content)
return None
@@ -266,7 +268,7 @@ class UniquesListBase(FilterList[UniqueFilter], ABC):
Each unique filter subscribes to a subset of events to respond to.
"""
- def __init__(self, filtering_cog: 'Filtering'):
+ def __init__(self, filtering_cog: "Filtering"):
super().__init__()
self.filtering_cog = filtering_cog
self.loaded_types: dict[str, type[UniqueFilter]] = {}
diff --git a/bot/exts/filtering/_filter_lists/token.py b/bot/exts/filtering/_filter_lists/token.py
index 5bb21cfc5..186715c9f 100644
--- a/bot/exts/filtering/_filter_lists/token.py
+++ b/bot/exts/filtering/_filter_lists/token.py
@@ -69,6 +69,6 @@ class TokensList(FilterList[TokenFilter]):
def _expand_spoilers(text: str) -> str:
"""Return a string containing all interpretations of a spoilered message."""
split_text = SPOILER_RE.split(text)
- return ''.join(
+ return "".join(
split_text[0::2] + split_text[1::2] + split_text
)
diff --git a/bot/exts/filtering/_filters/antispam/mentions.py b/bot/exts/filtering/_filters/antispam/mentions.py
index eb5a5e41b..782e14a64 100644
--- a/bot/exts/filtering/_filters/antispam/mentions.py
+++ b/bot/exts/filtering/_filters/antispam/mentions.py
@@ -72,7 +72,7 @@ class DuplicatesFilter(UniqueFilter):
resolved.message_id
)
except NotFound:
- log.info('Could not fetch the reference message as it has been deleted.')
+ log.info("Could not fetch the reference message as it has been deleted.")
if resolved and not isinstance(resolved, DeletedReferencedMessage):
reply_author = resolved.author
diff --git a/bot/exts/filtering/_filters/unique/discord_token.py b/bot/exts/filtering/_filters/unique/discord_token.py
index f4b9cc741..1745fa86c 100644
--- a/bot/exts/filtering/_filters/unique/discord_token.py
+++ b/bot/exts/filtering/_filters/unique/discord_token.py
@@ -122,13 +122,12 @@ class DiscordTokenFilter(UniqueFilter):
user_name=str(user),
kind="BOT" if user.bot else "USER",
), True
- else:
- return UNKNOWN_USER_LOG_MESSAGE.format(user_id=user_id), False
+ return UNKNOWN_USER_LOG_MESSAGE.format(user_id=user_id), False
@staticmethod
def censor_hmac(hmac: str) -> str:
"""Return a censored version of the hmac."""
- return 'x' * (len(hmac) - 3) + hmac[-3:]
+ return "x" * (len(hmac) - 3) + hmac[-3:]
@classmethod
def format_log_message(cls, author: discord.User, channel: discord.abc.GuildChannel, token: Token) -> str:
@@ -166,7 +165,7 @@ class DiscordTokenFilter(UniqueFilter):
try:
decoded_bytes = base64.urlsafe_b64decode(b64_content)
- string = decoded_bytes.decode('utf-8')
+ string = decoded_bytes.decode("utf-8")
if not (string.isascii() and string.isdigit()):
# This case triggers if there are fancy unicode digits in the base64 encoding,
# that means it's not a valid user id.
@@ -196,9 +195,9 @@ class DiscordTokenFilter(UniqueFilter):
# is not checked.
if timestamp + TOKEN_EPOCH >= DISCORD_EPOCH:
return True
- else:
- log.debug(f"Invalid token timestamp '{b64_content}': smaller than Discord epoch")
- return False
+
+ log.debug(f"Invalid token timestamp '{b64_content}': smaller than Discord epoch")
+ return False
@staticmethod
def is_maybe_valid_hmac(b64_content: str) -> bool:
@@ -215,5 +214,4 @@ class DiscordTokenFilter(UniqueFilter):
" case-insensitively unique characters"
)
return False
- else:
- return True
+ return True
diff --git a/bot/exts/filtering/_filters/unique/rich_embed.py b/bot/exts/filtering/_filters/unique/rich_embed.py
index 2ee469f51..cfed18e64 100644
--- a/bot/exts/filtering/_filters/unique/rich_embed.py
+++ b/bot/exts/filtering/_filters/unique/rich_embed.py
@@ -42,10 +42,10 @@ class RichEmbedFilter(UniqueFilter):
# of the message, it's unlikely to be an auto-generated embed by Discord.
ctx.alert_embeds.extend(ctx.embeds)
return True
- else:
- log.trace(
- "Found a rich embed sent by a regular user account, "
- "but it was likely just an automatic URL embed."
- )
+
+ log.trace(
+ "Found a rich embed sent by a regular user account, "
+ "but it was likely just an automatic URL embed."
+ )
return False
diff --git a/bot/exts/filtering/_settings.py b/bot/exts/filtering/_settings.py
index f51a42704..766a5ea10 100644
--- a/bot/exts/filtering/_settings.py
+++ b/bot/exts/filtering/_settings.py
@@ -5,9 +5,7 @@ import traceback
from abc import abstractmethod
from copy import copy
from functools import reduce
-from typing import Any, NamedTuple, Optional, TypeVar
-
-from typing_extensions import Self
+from typing import Any, NamedTuple, Self, TypeVar
from bot.exts.filtering._filter_context import FilterContext
from bot.exts.filtering._settings_types import settings_types
@@ -26,7 +24,7 @@ T = TypeVar("T", bound=SettingsEntry)
def create_settings(
settings_data: dict, *, defaults: Defaults | None = None, keep_empty: bool = False
-) -> tuple[Optional[ActionSettings], Optional[ValidationSettings]]:
+) -> tuple[ActionSettings | None, ValidationSettings | None]:
"""
Create and return instances of the Settings subclasses from the given data.
@@ -110,7 +108,7 @@ class Settings(FieldRequiring, dict[str, T]):
"""Create a shallow copy of the object."""
return copy(self)
- def get_setting(self, key: str, default: Optional[Any] = None) -> Any:
+ def get_setting(self, key: str, default: Any | None = None) -> Any:
"""Get the setting matching the key, or fall back to the default value if the key is missing."""
for entry in self.values():
if hasattr(entry, key):
@@ -120,7 +118,7 @@ class Settings(FieldRequiring, dict[str, T]):
@classmethod
def create(
cls, settings_data: dict, *, defaults: Settings | None = None, keep_empty: bool = False
- ) -> Optional[Settings]:
+ ) -> Settings | None:
"""
Returns a Settings object from `settings_data` if it holds any value, None otherwise.
diff --git a/bot/exts/filtering/_settings_types/actions/infraction_and_notification.py b/bot/exts/filtering/_settings_types/actions/infraction_and_notification.py
index 508c09c2a..f12538294 100644
--- a/bot/exts/filtering/_settings_types/actions/infraction_and_notification.py
+++ b/bot/exts/filtering/_settings_types/actions/infraction_and_notification.py
@@ -1,5 +1,5 @@
from enum import Enum, auto
-from typing import ClassVar
+from typing import ClassVar, Self
import arrow
import discord.abc
@@ -9,7 +9,6 @@ from discord.errors import Forbidden
from pydantic import validator
from pydis_core.utils.logging import get_logger
from pydis_core.utils.members import get_or_fetch_member
-from typing_extensions import Self
import bot as bot_module
from bot.constants import Channels
@@ -223,7 +222,7 @@ class InfractionAndNotification(ActionEntry):
# Lower number -> higher in the hierarchy
if self.infraction_type is None:
return other.copy()
- elif other.infraction_type is None:
+ if other.infraction_type is None:
return self.copy()
if self.infraction_type.value < other.infraction_type.value:
diff --git a/bot/exts/filtering/_settings_types/actions/ping.py b/bot/exts/filtering/_settings_types/actions/ping.py
index ee40c54fe..fa3e2224f 100644
--- a/bot/exts/filtering/_settings_types/actions/ping.py
+++ b/bot/exts/filtering/_settings_types/actions/ping.py
@@ -1,7 +1,6 @@
-from typing import ClassVar
+from typing import ClassVar, Self
from pydantic import validator
-from typing_extensions import Self
from bot.exts.filtering._filter_context import FilterContext
from bot.exts.filtering._settings_types.settings_entry import ActionEntry
diff --git a/bot/exts/filtering/_settings_types/actions/remove_context.py b/bot/exts/filtering/_settings_types/actions/remove_context.py
index dc01426d8..b833978fa 100644
--- a/bot/exts/filtering/_settings_types/actions/remove_context.py
+++ b/bot/exts/filtering/_settings_types/actions/remove_context.py
@@ -1,11 +1,10 @@
from collections import defaultdict
-from typing import ClassVar
+from typing import ClassVar, Self
from discord import Message, Thread
from discord.errors import HTTPException
from pydis_core.utils import scheduling
from pydis_core.utils.logging import get_logger
-from typing_extensions import Self
import bot
from bot.constants import Channels
diff --git a/bot/exts/filtering/_settings_types/actions/send_alert.py b/bot/exts/filtering/_settings_types/actions/send_alert.py
index f554cdd4d..515edb0e6 100644
--- a/bot/exts/filtering/_settings_types/actions/send_alert.py
+++ b/bot/exts/filtering/_settings_types/actions/send_alert.py
@@ -1,6 +1,4 @@
-from typing import ClassVar
-
-from typing_extensions import Self
+from typing import ClassVar, Self
from bot.exts.filtering._filter_context import FilterContext
from bot.exts.filtering._settings_types.settings_entry import ActionEntry
diff --git a/bot/exts/filtering/_settings_types/settings_entry.py b/bot/exts/filtering/_settings_types/settings_entry.py
index e41ef5c7a..a3a2cbe1a 100644
--- a/bot/exts/filtering/_settings_types/settings_entry.py
+++ b/bot/exts/filtering/_settings_types/settings_entry.py
@@ -1,10 +1,9 @@
from __future__ import annotations
from abc import abstractmethod
-from typing import Any, ClassVar, Union
+from typing import Any, ClassVar, Self
from pydantic import BaseModel, PrivateAttr
-from typing_extensions import Self
from bot.exts.filtering._filter_context import FilterContext
from bot.exts.filtering._utils import FieldRequiring
@@ -22,7 +21,7 @@ class SettingsEntry(BaseModel, FieldRequiring):
name: ClassVar[str] = FieldRequiring.MUST_SET_UNIQUE
# Each subclass must define a description of what it does. If the data an entry type receives comprises
# several DB fields, the value should a dictionary of field names and their descriptions.
- description: ClassVar[Union[str, dict[str, str]]] = FieldRequiring.MUST_SET
+ description: ClassVar[str | dict[str, str]] = FieldRequiring.MUST_SET
_overrides: set[str] = PrivateAttr(default_factory=set)
diff --git a/bot/exts/filtering/_settings_types/validations/bypass_roles.py b/bot/exts/filtering/_settings_types/validations/bypass_roles.py
index d42e6407c..50fb1e650 100644
--- a/bot/exts/filtering/_settings_types/validations/bypass_roles.py
+++ b/bot/exts/filtering/_settings_types/validations/bypass_roles.py
@@ -12,7 +12,7 @@ class RoleBypass(ValidationEntry):
name: ClassVar[str] = "bypass_roles"
description: ClassVar[str] = "A list of role IDs or role names. Users with these roles will not trigger the filter."
- bypass_roles: set[Union[int, str]]
+ bypass_roles: set[Union[int, str]] # noqa: UP007
def triggers_on(self, ctx: FilterContext) -> bool:
"""Return whether the filter should be triggered on this user given their roles."""
diff --git a/bot/exts/filtering/_settings_types/validations/channel_scope.py b/bot/exts/filtering/_settings_types/validations/channel_scope.py
index 45b769d29..1880ae27e 100644
--- a/bot/exts/filtering/_settings_types/validations/channel_scope.py
+++ b/bot/exts/filtering/_settings_types/validations/channel_scope.py
@@ -31,10 +31,10 @@ class ChannelScope(ValidationEntry):
# NOTE: Don't change this to use the new 3.10 union syntax unless you ensure Pydantic type validation and coercion
# work properly. At the time of writing this code there's a difference.
- disabled_channels: set[Union[int, str]]
- disabled_categories: set[Union[int, str]]
- enabled_channels: set[Union[int, str]]
- enabled_categories: set[Union[int, str]]
+ disabled_channels: set[Union[int, str]] # noqa: UP007
+ disabled_categories: set[Union[int, str]] # noqa: UP007
+ enabled_channels: set[Union[int, str]] # noqa: UP007
+ enabled_categories: set[Union[int, str]] # noqa: UP007
@validator("*", pre=True)
@classmethod
diff --git a/bot/exts/filtering/_ui/filter.py b/bot/exts/filtering/_ui/filter.py
index 5b23b71e9..4300de19c 100644
--- a/bot/exts/filtering/_ui/filter.py
+++ b/bot/exts/filtering/_ui/filter.py
@@ -1,6 +1,7 @@
from __future__ import annotations
-from typing import Any, Callable
+from collections.abc import Callable
+from typing import Any
import discord
import discord.ui
@@ -387,7 +388,7 @@ def description_and_settings_converter(
if not SINGLE_SETTING_PATTERN.match(parsed[0]):
description, *parsed = parsed
- settings = {setting: value for setting, value in [part.split("=", maxsplit=1) for part in parsed]}
+ settings = {setting: value for setting, value in [part.split("=", maxsplit=1) for part in parsed]} # noqa: C416
template = None
if "--template" in settings:
template = settings.pop("--template")
@@ -447,7 +448,7 @@ def template_settings(
try:
filter_id = int(filter_id)
if filter_id < 0:
- raise ValueError()
+ raise ValueError
except ValueError:
raise BadArgument("Template value must be a non-negative integer.")
diff --git a/bot/exts/filtering/_ui/filter_list.py b/bot/exts/filtering/_ui/filter_list.py
index 4d6f76a89..062975ad7 100644
--- a/bot/exts/filtering/_ui/filter_list.py
+++ b/bot/exts/filtering/_ui/filter_list.py
@@ -1,6 +1,7 @@
from __future__ import annotations
-from typing import Any, Callable
+from collections.abc import Callable
+from typing import Any
import discord
from discord import Embed, Interaction, SelectOption, User
@@ -25,20 +26,20 @@ def settings_converter(loaded_settings: dict, input_data: str) -> dict[str, Any]
return {}
try:
- settings = {setting: value for setting, value in [part.split("=", maxsplit=1) for part in parsed]}
+ settings = {setting: value for setting, value in [part.split("=", maxsplit=1) for part in parsed]} # noqa: C416
except ValueError:
raise BadArgument("The settings provided are not in the correct format.")
for setting in settings:
if setting not in loaded_settings:
raise BadArgument(f"{setting!r} is not a recognized setting.")
- else:
- type_ = loaded_settings[setting][2]
- try:
- parsed_value = parse_value(settings.pop(setting), type_)
- settings[setting] = parsed_value
- except (TypeError, ValueError) as e:
- raise BadArgument(e)
+
+ type_ = loaded_settings[setting][2]
+ try:
+ parsed_value = parse_value(settings.pop(setting), type_)
+ settings[setting] = parsed_value
+ except (TypeError, ValueError) as e:
+ raise BadArgument(e)
return settings
diff --git a/bot/exts/filtering/_ui/search.py b/bot/exts/filtering/_ui/search.py
index dba7f3cea..ccb47cdd2 100644
--- a/bot/exts/filtering/_ui/search.py
+++ b/bot/exts/filtering/_ui/search.py
@@ -34,7 +34,7 @@ def search_criteria_converter(
return {}, {}, filter_type
try:
- settings = {setting: value for setting, value in [part.split("=", maxsplit=1) for part in parsed]}
+ settings = {setting: value for setting, value in [part.split("=", maxsplit=1) for part in parsed]} # noqa: C416
except ValueError:
raise BadArgument("The settings provided are not in the correct format.")
@@ -102,7 +102,7 @@ def template_settings(
try:
filter_id = int(filter_id)
if filter_id < 0:
- raise ValueError()
+ raise ValueError
except ValueError:
raise BadArgument("Template value must be a non-negative integer.")
diff --git a/bot/exts/filtering/_ui/ui.py b/bot/exts/filtering/_ui/ui.py
index bde66222e..7026c7d05 100644
--- a/bot/exts/filtering/_ui/ui.py
+++ b/bot/exts/filtering/_ui/ui.py
@@ -2,14 +2,14 @@ from __future__ import annotations
import re
from abc import ABC, abstractmethod
-from collections.abc import Iterable
+from collections.abc import Callable, Coroutine, Iterable
from enum import EnumMeta
from functools import partial
-from typing import Any, Callable, Coroutine, Optional, TypeVar, get_origin
+from typing import Any, TypeVar, get_origin
import discord
from discord import Embed, Interaction
-from discord.ext.commands import Context
+from discord.ext.commands import Context, Converter
from discord.ui.select import MISSING as SELECT_MISSING, SelectOption
from discord.utils import escape_markdown
from pydis_core.site_api import ResponseCodeError
@@ -53,7 +53,7 @@ EDIT_CONFIRMED_MESSAGE = "✅ Edit for `{0}` confirmed"
# Sentinel value to denote that a value is missing
MISSING = object()
-T = TypeVar('T')
+T = TypeVar("T")
async def _build_alert_message_content(ctx: FilterContext, current_message_length: int) -> str:
@@ -62,7 +62,7 @@ async def _build_alert_message_content(ctx: FilterContext, current_message_lengt
if ctx.messages_deletion and ctx.upload_deletion_logs and any((
ctx.related_messages,
len(ctx.uploaded_attachments) > 0,
- ctx.content.count('\n') > 15
+ ctx.content.count("\n") > 15
)):
to_upload = {ctx.message} | ctx.related_messages if ctx.message else ctx.related_messages
url = await upload_log(to_upload, bot.instance.user.id, ctx.uploaded_attachments)
@@ -77,8 +77,7 @@ async def _build_alert_message_content(ctx: FilterContext, current_message_lengt
log_site_msg = f"The full message can be found [here]({url})"
# 7 because that's the length of "[...]\n\n"
return alert_content[:remaining_chars - (7 + len(log_site_msg))] + "[...]\n\n" + log_site_msg
- else:
- return alert_content[:remaining_chars - 5] + "[...]"
+ return alert_content[:remaining_chars - 5] + "[...]"
return alert_content
@@ -126,7 +125,7 @@ def populate_embed_from_dict(embed: Embed, data: dict) -> None:
for setting, value in data.items():
if setting.startswith("_"):
continue
- if isinstance(value, (list, set, tuple)):
+ if isinstance(value, list | set | tuple):
value = f"[{', '.join(map(str, value))}]"
else:
value = str(value) if value not in ("", None) else "-"
@@ -184,7 +183,7 @@ class ArgumentCompletionSelect(discord.ui.Select):
arg_name: str,
options: list[str],
position: int,
- converter: Optional[Callable] = None
+ converter: Converter | None = None
):
super().__init__(
placeholder=f"Select a value for {arg_name!r}",
@@ -200,7 +199,7 @@ class ArgumentCompletionSelect(discord.ui.Select):
await interaction.response.defer()
value = interaction.data["values"][0]
if self.converter:
- value = self.converter(value)
+ value = await self.converter().convert(self.ctx, value)
args = self.args.copy() # This makes the view reusable.
args.insert(self.position, value)
log.trace(f"Argument filled with the value {value}. Re-invoking command")
@@ -217,7 +216,7 @@ class ArgumentCompletionView(discord.ui.View):
arg_name: str,
options: list[str],
position: int,
- converter: Optional[Callable] = None
+ converter: Converter | None = None
):
super().__init__()
log.trace(f"The {arg_name} argument was designated missing in the invocation {ctx.view.buffer!r}")
diff --git a/bot/exts/filtering/_utils.py b/bot/exts/filtering/_utils.py
index 97a0fa8d4..e109a47ee 100644
--- a/bot/exts/filtering/_utils.py
+++ b/bot/exts/filtering/_utils.py
@@ -7,14 +7,14 @@ import pkgutil
import types
from abc import ABC, abstractmethod
from collections import defaultdict
+from collections.abc import Iterable
from dataclasses import dataclass
from functools import cache
-from typing import Any, Iterable, TypeVar, Union, get_args, get_origin
+from typing import Any, Self, TypeVar, Union, get_args, get_origin
import discord
import regex
from discord.ext.commands import Command
-from typing_extensions import Self
import bot
from bot.bot import Bot
@@ -25,9 +25,9 @@ INVISIBLE_RE = regex.compile(rf"[{VARIATION_SELECTORS}\p{{UNASSIGNED}}\p{{FORMAT
ZALGO_RE = regex.compile(rf"[\p{{NONSPACING MARK}}\p{{ENCLOSING MARK}}--[{VARIATION_SELECTORS}]]", regex.V1)
-T = TypeVar('T')
+T = TypeVar("T")
-Serializable = Union[bool, int, float, str, list, dict, None]
+Serializable = bool | int | float | str | list | dict | None
def subclasses_in_package(package: str, prefix: str, parent: T) -> set[T]:
@@ -74,12 +74,12 @@ def to_serializable(item: Any, *, ui_repr: bool = False) -> Serializable:
`ui_repr` dictates whether to use the UI representation of `CustomIOField` instances (if any)
or the DB-oriented representation.
"""
- if isinstance(item, (bool, int, float, str, type(None))):
+ if isinstance(item, bool | int | float | str | type(None)):
return item
if isinstance(item, dict):
result = {}
for key, value in item.items():
- if not isinstance(key, (bool, int, float, str, type(None))):
+ if not isinstance(key, bool | int | float | str | type(None)):
key = str(key)
result[key] = to_serializable(value, ui_repr=ui_repr)
return result
@@ -103,8 +103,7 @@ def resolve_mention(mention: str) -> str:
else:
if any(mention == role.id for role in guild.roles):
return f"<@&{mention}>"
- else:
- return f"<@{mention}>"
+ return f"<@{mention}>"
# It's a name
for role in guild.roles:
@@ -121,14 +120,14 @@ def repr_equals(override: Any, default: Any) -> bool:
if override is None: # It's not an override
return True
- override_is_sequence = isinstance(override, (tuple, list, set))
- default_is_sequence = isinstance(default, (tuple, list, set))
+ override_is_sequence = isinstance(override, tuple | list | set)
+ default_is_sequence = isinstance(default, tuple | list | set)
if override_is_sequence != default_is_sequence: # One is a sequence and the other isn't.
return False
if override_is_sequence:
if len(override) != len(default):
return False
- return all(str(item1) == str(item2) for item1, item2 in zip(set(override), set(default)))
+ return all(str(item1) == str(item2) for item1, item2 in zip(set(override), set(default), strict=True))
return str(override) == str(default)
@@ -139,8 +138,7 @@ def normalize_type(type_: type, *, prioritize_nonetype: bool = True) -> type:
if type(None) in args:
if prioritize_nonetype:
return type(None)
- else:
- args = tuple(set(args) - {type(None)})
+ args = tuple(set(args) - {type(None)})
type_ = args[0] # Pick one, doesn't matter
if origin := get_origin(type_): # In case of a parameterized List, Set, Dict etc.
return origin
@@ -176,10 +174,9 @@ class FieldRequiring(ABC):
def __init_subclass__(cls, **kwargs):
def inherited(attr: str) -> bool:
"""True if `attr` was inherited from a parent class."""
- for parent in cls.__mro__[1:-1]: # The first element is the class itself, last element is object.
- if hasattr(parent, attr): # The attribute was inherited.
- return True
- return False
+ # The first element of cls.__mro__ is the class itself, last element is object, skip those.
+ # hasattr(parent, attr) means the attribute was inherited.
+ return any(hasattr(parent, attr) for parent in cls.__mro__[1:-1])
# If a new attribute with the value MUST_SET_UNIQUE was defined in an abstract class, record it.
if inspect.isabstract(cls):
@@ -196,18 +193,18 @@ class FieldRequiring(ABC):
value = getattr(cls, attribute)
if value is FieldRequiring.MUST_SET and inherited(attribute):
raise ValueError(f"You must set attribute {attribute!r} when creating {cls!r}")
- elif value is FieldRequiring.MUST_SET_UNIQUE and inherited(attribute):
+ if value is FieldRequiring.MUST_SET_UNIQUE and inherited(attribute):
raise ValueError(f"You must set a unique value to attribute {attribute!r} when creating {cls!r}")
- else:
- # Check if the value needs to be unique.
- for parent in cls.__mro__[1:-1]:
- # Find the parent class the attribute was first defined in.
- if attribute in FieldRequiring.__unique_attributes[parent]:
- if value in FieldRequiring.__unique_attributes[parent][attribute]:
- raise ValueError(f"Value of {attribute!r} in {cls!r} is not unique for parent {parent!r}.")
- else:
- # Add to the set of unique values for that field.
- FieldRequiring.__unique_attributes[parent][attribute].add(value)
+
+ # Check if the value needs to be unique.
+ for parent in cls.__mro__[1:-1]:
+ # Find the parent class the attribute was first defined in.
+ if attribute in FieldRequiring.__unique_attributes[parent]:
+ if value in FieldRequiring.__unique_attributes[parent][attribute]:
+ raise ValueError(f"Value of {attribute!r} in {cls!r} is not unique for parent {parent!r}.")
+
+ # Add to the set of unique values for that field.
+ FieldRequiring.__unique_attributes[parent][attribute].add(value)
@dataclass
diff --git a/bot/exts/filtering/filtering.py b/bot/exts/filtering/filtering.py
index 82006c9db..8e7ba7476 100644
--- a/bot/exts/filtering/filtering.py
+++ b/bot/exts/filtering/filtering.py
@@ -7,7 +7,7 @@ from collections.abc import Iterable, Mapping
from functools import partial, reduce
from io import BytesIO
from operator import attrgetter
-from typing import Literal, Optional, get_type_hints
+from typing import Literal, get_type_hints
import arrow
import discord
@@ -25,7 +25,7 @@ from bot.bot import Bot
from bot.constants import Channels, Guild, MODERATION_ROLES, Roles
from bot.exts.backend.branding._repository import HEADERS, PARAMS
from bot.exts.filtering._filter_context import Event, FilterContext
-from bot.exts.filtering._filter_lists import FilterList, ListType, filter_list_types, list_type_converter
+from bot.exts.filtering._filter_lists import FilterList, ListType, ListTypeConverter, filter_list_types
from bot.exts.filtering._filter_lists.filter_list import AtomicList
from bot.exts.filtering._filters.filter import Filter, UniqueFilter
from bot.exts.filtering._settings import ActionSettings
@@ -184,11 +184,11 @@ class Filtering(Cog):
async def schedule_offending_messages_deletion(self) -> None:
"""Load the messages that need to be scheduled for deletion from the database."""
- response = await self.bot.api_client.get('bot/offensive-messages')
+ response = await self.bot.api_client.get("bot/offensive-messages")
now = arrow.utcnow()
for msg in response:
- delete_at = arrow.get(msg['delete_date'])
+ delete_at = arrow.get(msg["delete_date"])
if delete_at < now:
await self._delete_offensive_msg(msg)
else:
@@ -298,7 +298,7 @@ class Filtering(Cog):
await ctx.send_help(ctx.command)
@blocklist.command(name="list", aliases=("get",))
- async def bl_list(self, ctx: Context, list_name: Optional[str] = None) -> None:
+ async def bl_list(self, ctx: Context, list_name: str | None = None) -> None:
"""List the contents of a specified blacklist."""
result = await self._resolve_list_type_and_name(ctx, ListType.DENY, list_name, exclude="list_type")
if not result:
@@ -310,11 +310,11 @@ class Filtering(Cog):
async def bl_add(
self,
ctx: Context,
- noui: Optional[Literal["noui"]],
- list_name: Optional[str],
+ noui: Literal["noui"] | None,
+ list_name: str | None,
content: str,
*,
- description_and_settings: Optional[str] = None
+ description_and_settings: str | None = None
) -> None:
"""
Add a blocked filter to the specified filter list.
@@ -341,7 +341,7 @@ class Filtering(Cog):
await ctx.send_help(ctx.command)
@allowlist.command(name="list", aliases=("get",))
- async def al_list(self, ctx: Context, list_name: Optional[str] = None) -> None:
+ async def al_list(self, ctx: Context, list_name: str | None = None) -> None:
"""List the contents of a specified whitelist."""
result = await self._resolve_list_type_and_name(ctx, ListType.ALLOW, list_name, exclude="list_type")
if not result:
@@ -353,11 +353,11 @@ class Filtering(Cog):
async def al_add(
self,
ctx: Context,
- noui: Optional[Literal["noui"]],
- list_name: Optional[str],
+ noui: Literal["noui"] | None,
+ list_name: str | None,
content: str,
*,
- description_and_settings: Optional[str] = None
+ description_and_settings: str | None = None
) -> None:
"""
Add an allowed filter to the specified filter list.
@@ -378,7 +378,7 @@ class Filtering(Cog):
# region: filter commands
@commands.group(aliases=("filters", "f"), invoke_without_command=True)
- async def filter(self, ctx: Context, id_: Optional[int] = None) -> None:
+ async def filter(self, ctx: Context, id_: int | None = None) -> None:
"""
Group for managing filters.
@@ -414,7 +414,10 @@ class Filtering(Cog):
@filter.command(name="list", aliases=("get",))
async def f_list(
- self, ctx: Context, list_type: Optional[list_type_converter] = None, list_name: Optional[str] = None
+ self,
+ ctx: Context,
+ list_type: ListTypeConverter | None = None,
+ list_name: str | None = None,
) -> None:
"""List the contents of a specified list of filters."""
result = await self._resolve_list_type_and_name(ctx, list_type, list_name)
@@ -425,7 +428,7 @@ class Filtering(Cog):
await self._send_list(ctx, filter_list, list_type)
@filter.command(name="describe", aliases=("explain", "manual"))
- async def f_describe(self, ctx: Context, filter_name: Optional[str]) -> None:
+ async def f_describe(self, ctx: Context, filter_name: str | None) -> None:
"""Show a description of the specified filter, or a list of possible values if no name is specified."""
if not filter_name:
filter_names = [f"» {f}" for f in self.loaded_filters]
@@ -448,12 +451,12 @@ class Filtering(Cog):
async def f_add(
self,
ctx: Context,
- noui: Optional[Literal["noui"]],
- list_type: Optional[list_type_converter],
- list_name: Optional[str],
+ noui: Literal["noui"] | None,
+ list_type: ListTypeConverter | None,
+ list_name: str | None,
content: str,
*,
- description_and_settings: Optional[str] = None
+ description_and_settings: str | None = None
) -> None:
"""
Add a filter to the specified filter list.
@@ -479,10 +482,10 @@ class Filtering(Cog):
async def f_edit(
self,
ctx: Context,
- noui: Optional[Literal["noui"]],
+ noui: Literal["noui"] | None,
filter_id: int,
*,
- description_and_settings: Optional[str] = None
+ description_and_settings: str | None = None
) -> None:
"""
Edit a filter specified by its ID.
@@ -561,7 +564,7 @@ class Filtering(Cog):
"""Delete the filter specified by its ID."""
async def delete_list() -> None:
"""The actual removal routine."""
- await bot.instance.api_client.delete(f'bot/filter/filters/{filter_id}')
+ await bot.instance.api_client.delete(f"bot/filter/filters/{filter_id}")
log.info(f"Successfully deleted filter with ID {filter_id}.")
filter_list[list_type].filters.pop(filter_id)
await ctx.reply(f"✅ Deleted filter: {filter_}")
@@ -697,7 +700,7 @@ class Filtering(Cog):
@filter.command(root_aliases=("compfilter", "compf"))
async def compadd(
- self, ctx: Context, list_name: Optional[str], content: str, *, description: Optional[str] = "Phishing"
+ self, ctx: Context, list_name: str | None, content: str, *, description: str | None = "Phishing"
) -> None:
"""Add a filter to detect a compromised account. Will apply the equivalent of a compban if triggered."""
result = await self._resolve_list_type_and_name(ctx, ListType.DENY, list_name, exclude="list_type")
@@ -727,7 +730,7 @@ class Filtering(Cog):
@filterlist.command(name="describe", aliases=("explain", "manual", "id"))
async def fl_describe(
- self, ctx: Context, list_type: Optional[list_type_converter] = None, list_name: Optional[str] = None
+ self, ctx: Context, list_type: ListTypeConverter | None = None, list_name: str | None = None
) -> None:
"""Show a description of the specified filter list, or a list of possible values if no values are provided."""
if not list_type and not list_name:
@@ -758,15 +761,14 @@ class Filtering(Cog):
@filterlist.command(name="add", aliases=("a",))
@has_any_role(Roles.admins)
- async def fl_add(self, ctx: Context, list_type: list_type_converter, list_name: str) -> None:
+ async def fl_add(self, ctx: Context, list_type: ListTypeConverter, list_name: str) -> None:
"""Add a new filter list."""
# Check if there's an implementation.
if list_name.lower() not in filter_list_types:
if list_name.lower()[:-1] not in filter_list_types: # Maybe the name was given with uppercase or in plural?
await ctx.reply(f":x: Cannot add a `{list_name}` filter list, as there is no matching implementation.")
return
- else:
- list_name = list_name.lower()[:-1]
+ list_name = list_name.lower()[:-1]
# Check it doesn't already exist.
list_description = f"{past_tense(list_type.name.lower())} {list_name.lower()}"
@@ -796,9 +798,9 @@ class Filtering(Cog):
async def fl_edit(
self,
ctx: Context,
- noui: Optional[Literal["noui"]],
- list_type: Optional[list_type_converter] = None,
- list_name: Optional[str] = None,
+ noui: Literal["noui"] | None,
+ list_type: ListTypeConverter | None = None,
+ list_name: str | None = None,
*,
settings: str | None
) -> None:
@@ -840,7 +842,7 @@ class Filtering(Cog):
@filterlist.command(name="delete", aliases=("remove",))
@has_any_role(Roles.admins)
async def fl_delete(
- self, ctx: Context, list_type: Optional[list_type_converter] = None, list_name: Optional[str] = None
+ self, ctx: Context, list_type: ListTypeConverter | None = None, list_name: str | None = None
) -> None:
"""Remove the filter list and all of its filters from the database."""
async def delete_list() -> None:
@@ -1025,7 +1027,7 @@ class Filtering(Cog):
await ctx.send(
"The **list_type** argument is unspecified. Please pick a value from the options below:",
view=ArgumentCompletionView(
- ctx, args, "list_type", [option.name for option in ListType], 0, list_type_converter
+ ctx, args, "list_type", [option.name for option in ListType], 0, ListTypeConverter
)
)
return None
@@ -1059,21 +1061,22 @@ class Filtering(Cog):
await LinePaginator.paginate(lines, ctx, embed, max_lines=15, empty=False, reply=True)
- def _get_filter_by_id(self, id_: int) -> Optional[tuple[Filter, FilterList, ListType]]:
+ def _get_filter_by_id(self, id_: int) -> tuple[Filter, FilterList, ListType] | None:
"""Get the filter object corresponding to the provided ID, along with its containing list and list type."""
for filter_list in self.filter_lists.values():
for list_type, sublist in filter_list.items():
if id_ in sublist.filters:
return sublist.filters[id_], filter_list, list_type
+ return None
async def _add_filter(
self,
ctx: Context,
- noui: Optional[Literal["noui"]],
+ noui: Literal["noui"] | None,
list_type: ListType,
filter_list: FilterList,
content: str,
- description_and_settings: Optional[str] = None
+ description_and_settings: str | None = None
) -> None:
"""Add a filter to the database."""
# Validations.
@@ -1196,7 +1199,7 @@ class Filtering(Cog):
"filter_list": list_id, "content": content, "description": description,
"additional_settings": filter_settings, **settings
}
- response = await bot.instance.api_client.post('bot/filter/filters', json=to_serializable(payload))
+ response = await bot.instance.api_client.post("bot/filter/filters", json=to_serializable(payload))
new_filter = filter_list.add_filter(list_type, response)
log.info(f"Added new filter: {new_filter}.")
if new_filter:
@@ -1240,7 +1243,7 @@ class Filtering(Cog):
"additional_settings": filter_settings, **settings
}
response = await bot.instance.api_client.patch(
- f'bot/filter/filters/{filter_.id}', json=to_serializable(payload)
+ f"bot/filter/filters/{filter_.id}", json=to_serializable(payload)
)
# Return type can be None, but if it's being edited then it's not supposed to be.
edited_filter = filter_list.add_filter(list_type, response)
@@ -1253,7 +1256,7 @@ class Filtering(Cog):
"""POST the new data of the filter list to the site API."""
payload = {"name": list_name, "list_type": list_type.value, **to_serializable(settings)}
filterlist_name = f"{past_tense(list_type.name.lower())} {list_name}"
- response = await bot.instance.api_client.post('bot/filter/filter_lists', json=payload)
+ response = await bot.instance.api_client.post("bot/filter/filter_lists", json=payload)
log.info(f"Successfully posted the new {filterlist_name} filterlist.")
self._load_raw_filter_list(response)
await msg.reply(f"✅ Added a new filter list: {filterlist_name}")
@@ -1263,7 +1266,7 @@ class Filtering(Cog):
"""PATCH the new data of the filter list to the site API."""
list_id = filter_list[list_type].id
response = await bot.instance.api_client.patch(
- f'bot/filter/filter_lists/{list_id}', json=to_serializable(settings)
+ f"bot/filter/filter_lists/{list_id}", json=to_serializable(settings)
)
log.info(f"Successfully patched the {filter_list[list_type].label} filterlist, reloading...")
filter_list.pop(list_type, None)
@@ -1338,9 +1341,9 @@ class Filtering(Cog):
async def _delete_offensive_msg(self, msg: Mapping[str, int]) -> None:
"""Delete an offensive message, and then delete it from the DB."""
try:
- channel = self.bot.get_channel(msg['channel_id'])
+ channel = self.bot.get_channel(msg["channel_id"])
if channel:
- msg_obj = await channel.fetch_message(msg['id'])
+ msg_obj = await channel.fetch_message(msg["id"])
await msg_obj.delete()
except discord.NotFound:
log.info(
@@ -1355,8 +1358,8 @@ class Filtering(Cog):
def _schedule_msg_delete(self, msg: dict) -> None:
"""Delete an offensive message once its deletion date is reached."""
- delete_at = arrow.get(msg['delete_date']).datetime
- self.delete_scheduler.schedule_at(delete_at, msg['id'], self._delete_offensive_msg(msg))
+ delete_at = arrow.get(msg["delete_date"]).datetime
+ self.delete_scheduler.schedule_at(delete_at, msg["id"], self._delete_offensive_msg(msg))
async def _maybe_schedule_msg_delete(self, ctx: FilterContext, actions: ActionSettings | None) -> None:
"""Post the message to the database and schedule it for deletion if it's not set to be deleted already."""
@@ -1366,13 +1369,13 @@ class Filtering(Cog):
delete_date = (msg.created_at + OFFENSIVE_MSG_DELETE_TIME).isoformat()
data = {
- 'id': msg.id,
- 'channel_id': msg.channel.id,
- 'delete_date': delete_date
+ "id": msg.id,
+ "channel_id": msg.channel.id,
+ "delete_date": delete_date
}
try:
- await self.bot.api_client.post('bot/offensive-messages', json=data)
+ await self.bot.api_client.post("bot/offensive-messages", json=data)
except ResponseCodeError as e:
if e.status == 400 and "already exists" in e.response_json.get("id", [""])[0]:
log.debug(f"Offensive message {msg.id} already exists.")
diff --git a/bot/exts/fun/duck_pond.py b/bot/exts/fun/duck_pond.py
index fee933b47..63a455131 100644
--- a/bot/exts/fun/duck_pond.py
+++ b/bot/exts/fun/duck_pond.py
@@ -1,5 +1,4 @@
import asyncio
-from typing import Union
import discord
from discord import Color, Embed, Message, RawReactionActionEvent, errors
@@ -54,12 +53,11 @@ class DuckPond(Cog):
return False
@staticmethod
- def _is_duck_emoji(emoji: Union[str, discord.PartialEmoji, discord.Emoji]) -> bool:
+ def _is_duck_emoji(emoji: str | discord.PartialEmoji | discord.Emoji) -> bool:
"""Check if the emoji is a valid duck emoji."""
if isinstance(emoji, str):
return emoji == "🦆"
- else:
- return hasattr(emoji, "name") and emoji.name.startswith("ducky_")
+ return hasattr(emoji, "name") and emoji.name.startswith("ducky_")
async def count_ducks(self, message: Message) -> int:
"""
diff --git a/bot/exts/fun/off_topic_names.py b/bot/exts/fun/off_topic_names.py
index 86be8edae..120e3b4a4 100644
--- a/bot/exts/fun/off_topic_names.py
+++ b/bot/exts/fun/off_topic_names.py
@@ -4,7 +4,6 @@ import difflib
import json
import random
from functools import partial
-from typing import Optional
from discord import ButtonStyle, Colour, Embed, Interaction
from discord.ext import tasks
@@ -54,7 +53,7 @@ class OffTopicNames(Cog):
try:
channel_0_name, channel_1_name, channel_2_name = await self.bot.api_client.get(
- 'bot/off-topic-channel-names', params={'random_items': 3}
+ "bot/off-topic-channel-names", params={"random_items": 3}
)
except ResponseCodeError as e:
log.error(f"Failed to get new off-topic channel names: code {e.response.status}")
@@ -81,7 +80,7 @@ class OffTopicNames(Cog):
async def list_ot_names(self, ctx: Context, active: bool = True) -> None:
"""Send an embed containing active/deactivated off-topic channel names."""
- result = await self.bot.api_client.get('bot/off-topic-channel-names', params={'active': json.dumps(active)})
+ result = await self.bot.api_client.get("bot/off-topic-channel-names", params={"active": json.dumps(active)})
lines = sorted(f"• {name}" for name in result)
embed = Embed(
title=f"{'Active' if active else 'Deactivated'} off-topic names (`{len(result)}` total)",
@@ -93,13 +92,13 @@ class OffTopicNames(Cog):
embed.description = "Hmmm, seems like there's nothing here yet."
await ctx.send(embed=embed)
- @group(name='otname', aliases=('otnames', 'otn'), invoke_without_command=True)
+ @group(name="otname", aliases=("otnames", "otn"), invoke_without_command=True)
@has_any_role(*MODERATION_ROLES)
async def otname_group(self, ctx: Context) -> None:
"""Add or list items from the off-topic channel name rotation."""
await ctx.send_help(ctx.command)
- @otname_group.command(name='add', aliases=('a',))
+ @otname_group.command(name="add", aliases=("a",))
@has_any_role(*MODERATION_ROLES)
async def add_command(self, ctx: Context, *, name: OffTopicName) -> None:
"""
@@ -107,7 +106,7 @@ class OffTopicNames(Cog):
The name is not added if it is too similar to an existing name.
"""
- existing_names = await self.bot.api_client.get('bot/off-topic-channel-names')
+ existing_names = await self.bot.api_client.get("bot/off-topic-channel-names")
close_match = difflib.get_close_matches(name, existing_names, n=1, cutoff=0.8)
if close_match:
@@ -122,7 +121,7 @@ class OffTopicNames(Cog):
else:
await self._add_name(ctx, name)
- @otname_group.command(name='forceadd', aliases=('fa',))
+ @otname_group.command(name="forceadd", aliases=("fa",))
@has_any_role(*MODERATION_ROLES)
async def force_add_command(self, ctx: Context, *, name: OffTopicName) -> None:
"""Forcefully adds a new off-topic name to the rotation."""
@@ -130,35 +129,35 @@ class OffTopicNames(Cog):
async def _add_name(self, ctx: Context, name: str) -> None:
"""Adds an off-topic channel name to the site storage."""
- await self.bot.api_client.post('bot/off-topic-channel-names', params={'name': name})
+ await self.bot.api_client.post("bot/off-topic-channel-names", params={"name": name})
log.info(f"{ctx.author} added the off-topic channel name '{name}'")
await ctx.send(f":ok_hand: Added `{name}` to the names list.")
- @otname_group.command(name='delete', aliases=('remove', 'rm', 'del', 'd'))
+ @otname_group.command(name="delete", aliases=("remove", "rm", "del", "d"))
@has_any_role(*MODERATION_ROLES)
async def delete_command(self, ctx: Context, *, name: OffTopicName) -> None:
"""Removes a off-topic name from the rotation."""
- await self.bot.api_client.delete(f'bot/off-topic-channel-names/{name}')
+ await self.bot.api_client.delete(f"bot/off-topic-channel-names/{name}")
log.info(f"{ctx.author} deleted the off-topic channel name '{name}'")
await ctx.send(f":ok_hand: Removed `{name}` from the names list.")
- @otname_group.command(name='activate', aliases=('whitelist',))
+ @otname_group.command(name="activate", aliases=("whitelist",))
@has_any_role(*MODERATION_ROLES)
async def activate_ot_name(self, ctx: Context, name: OffTopicName) -> None:
"""Activate an existing off-topic name."""
await self.toggle_ot_name_activity(ctx, name, True)
- @otname_group.command(name='deactivate', aliases=('blacklist',))
+ @otname_group.command(name="deactivate", aliases=("blacklist",))
@has_any_role(*MODERATION_ROLES)
async def de_activate_ot_name(self, ctx: Context, name: OffTopicName) -> None:
"""Deactivate a specific off-topic name."""
await self.toggle_ot_name_activity(ctx, name, False)
- @otname_group.command(name='reroll')
+ @otname_group.command(name="reroll")
@has_any_role(*MODERATION_ROLES)
- async def re_roll_command(self, ctx: Context, ot_channel_index: Optional[int] = None) -> None:
+ async def re_roll_command(self, ctx: Context, ot_channel_index: int | None = None) -> None:
"""
Re-roll an off-topic name for a specific off-topic channel and deactivate the current name.
@@ -183,7 +182,7 @@ class OffTopicNames(Cog):
await self.de_activate_ot_name(ctx, old_ot_name)
response = await self.bot.api_client.get(
- 'bot/off-topic-channel-names', params={'random_items': 1}
+ "bot/off-topic-channel-names", params={"random_items": 1}
)
try:
new_channel_name = response[0]
@@ -248,7 +247,7 @@ class OffTopicNames(Cog):
await ctx.message.reply(embed=embed, view=view)
- @otname_group.group(name='list', aliases=('l',), invoke_without_command=True)
+ @otname_group.group(name="list", aliases=("l",), invoke_without_command=True)
@has_any_role(*MODERATION_ROLES)
async def list_command(self, ctx: Context) -> None:
"""
@@ -258,19 +257,19 @@ class OffTopicNames(Cog):
"""
await self.active_otnames_command(ctx)
- @list_command.command(name='active', aliases=('a',))
+ @list_command.command(name="active", aliases=("a",))
@has_any_role(*MODERATION_ROLES)
async def active_otnames_command(self, ctx: Context) -> None:
"""List active off-topic channel names."""
await self.list_ot_names(ctx, True)
- @list_command.command(name='deactivated', aliases=('d',))
+ @list_command.command(name="deactivated", aliases=("d",))
@has_any_role(*MODERATION_ROLES)
async def deactivated_otnames_command(self, ctx: Context) -> None:
"""List deactivated off-topic channel names."""
await self.list_ot_names(ctx, False)
- @otname_group.command(name='search', aliases=('s',))
+ @otname_group.command(name="search", aliases=("s",))
@has_any_role(*MODERATION_ROLES)
async def search_command(self, ctx: Context, *, query: OffTopicName) -> None:
"""Search for an off-topic name."""
@@ -279,11 +278,11 @@ class OffTopicNames(Cog):
# Map normalized names to returned names for search purposes
result = {
OffTopicName.translate_name(name, from_unicode=False).lower(): name
- for name in await self.bot.api_client.get('bot/off-topic-channel-names')
+ for name in await self.bot.api_client.get("bot/off-topic-channel-names")
}
# Search normalized keys
- in_matches = {name for name in result.keys() if query in name}
+ in_matches = {name for name in result if query in name}
close_matches = difflib.get_close_matches(query, result.keys(), n=10, cutoff=0.70)
# Send Results
diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py
index 774e9178e..1365b3df0 100644
--- a/bot/exts/help_channels/_channel.py
+++ b/bot/exts/help_channels/_channel.py
@@ -46,7 +46,7 @@ async def _close_help_post(closed_post: discord.Thread, closing_reason: _stats.C
"""Close the help post and record stats."""
embed = discord.Embed(description=CLOSED_POST_MSG)
embed.set_author(name=f"{POST_TITLE} closed", icon_url=CLOSED_POST_ICON_URL)
- message = ''
+ message = ""
# Include a ping in the close message if no one else engages, to encourage them
# to read the guide for asking better questions
@@ -149,7 +149,7 @@ async def help_post_opened(opened_post: discord.Thread, *, reopen: bool = False)
if isinstance(e, discord.HTTPException):
if e.code == 10003: # Post not found.
return
- elif e.code != 10008: # 10008 - Starter message not found.
+ if e.code != 10008: # 10008 - Starter message not found.
raise e
await send_opened_post_message(opened_post)
diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py
index dd6dee9ee..0d41388a7 100644
--- a/bot/exts/help_channels/_cog.py
+++ b/bot/exts/help_channels/_cog.py
@@ -121,7 +121,7 @@ class HelpForum(commands.Cog):
return
thread = message.channel
- if not message.id == thread.id:
+ if message.id != thread.id:
# Opener messages have the same ID as the thread
return
@@ -155,7 +155,7 @@ class HelpForum(commands.Cog):
async def new_post_message_listener(self, message: discord.Message) -> None:
"""Defer application of new message logic for messages in the help forum to the _message helper."""
if not _channel.is_help_forum_post(message.channel):
- return None
+ return
await _message.notify_session_participants(message)
diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py
index 98bfe59b8..aa6e52340 100644
--- a/bot/exts/help_channels/_message.py
+++ b/bot/exts/help_channels/_message.py
@@ -14,7 +14,7 @@ NAMESPACE = "help"
def _serialise_session_participants(participants: set[int]) -> str:
"""Convert a set to a comma separated string."""
- return ','.join(str(p) for p in participants)
+ return ",".join(str(p) for p in participants)
def _deserialise_session_participants(s: str) -> set[int]:
diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py
index bfe32459e..dc5c2258a 100644
--- a/bot/exts/info/code_snippets.py
+++ b/bot/exts/info/code_snippets.py
@@ -16,26 +16,26 @@ from bot.utils.messages import wait_for_deletion
log = get_logger(__name__)
GITHUB_RE = re.compile(
- r'https://github\.com/(?P<repo>[a-zA-Z0-9-]+/[\w.-]+)/blob/'
- r'(?P<path>[^#>]+)(\?[^#>]+)?(#L(?P<start_line>\d+)(([-~:]|(\.\.))L(?P<end_line>\d+))?)'
+ r"https://github\.com/(?P<repo>[a-zA-Z0-9-]+/[\w.-]+)/blob/"
+ r"(?P<path>[^#>]+)(\?[^#>]+)?(#L(?P<start_line>\d+)(([-~:]|(\.\.))L(?P<end_line>\d+))?)"
)
GITHUB_GIST_RE = re.compile(
- r'https://gist\.github\.com/([a-zA-Z0-9-]+)/(?P<gist_id>[a-zA-Z0-9]+)/*'
- r'(?P<revision>[a-zA-Z0-9]*)/*#file-(?P<file_path>[^#>]+?)(\?[^#>]+)?'
- r'(-L(?P<start_line>\d+)([-~:]L(?P<end_line>\d+))?)'
+ r"https://gist\.github\.com/([a-zA-Z0-9-]+)/(?P<gist_id>[a-zA-Z0-9]+)/*"
+ r"(?P<revision>[a-zA-Z0-9]*)/*#file-(?P<file_path>[^#>]+?)(\?[^#>]+)?"
+ r"(-L(?P<start_line>\d+)([-~:]L(?P<end_line>\d+))?)"
)
-GITHUB_HEADERS = {'Accept': 'application/vnd.github.v3.raw'}
+GITHUB_HEADERS = {"Accept": "application/vnd.github.v3.raw"}
GITLAB_RE = re.compile(
- r'https://gitlab\.com/(?P<repo>[\w.-]+/[\w.-]+)/\-/blob/(?P<path>[^#>]+)'
- r'(\?[^#>]+)?(#L(?P<start_line>\d+)(-(?P<end_line>\d+))?)'
+ r"https://gitlab\.com/(?P<repo>[\w.-]+/[\w.-]+)/\-/blob/(?P<path>[^#>]+)"
+ r"(\?[^#>]+)?(#L(?P<start_line>\d+)(-(?P<end_line>\d+))?)"
)
BITBUCKET_RE = re.compile(
- r'https://bitbucket\.org/(?P<repo>[a-zA-Z0-9-]+/[\w.-]+)/src/(?P<ref>[0-9a-zA-Z]+)'
- r'/(?P<file_path>[^#>]+)(\?[^#>]+)?(#lines-(?P<start_line>\d+)(:(?P<end_line>\d+))?)'
+ r"https://bitbucket\.org/(?P<repo>[a-zA-Z0-9-]+/[\w.-]+)/src/(?P<ref>[0-9a-zA-Z]+)"
+ r"/(?P<file_path>[^#>]+)(\?[^#>]+)?(#lines-(?P<start_line>\d+)(:(?P<end_line>\d+))?)"
)
@@ -60,19 +60,20 @@ class CodeSnippets(Cog):
async def _fetch_response(self, url: str, response_format: str, **kwargs) -> Any:
"""Makes http requests using aiohttp."""
async with self.bot.http_session.get(url, raise_for_status=True, **kwargs) as response:
- if response_format == 'text':
+ if response_format == "text":
return await response.text()
- elif response_format == 'json':
+ if response_format == "json":
return await response.json()
+ return None
def _find_ref(self, path: str, refs: tuple) -> tuple:
"""Loops through all branches and tags to find the required ref."""
# Base case: there is no slash in the branch name
- ref, file_path = path.split('/', 1)
+ ref, file_path = path.split("/", 1)
# In case there are slashes in the branch name, we loop through all branches and tags
for possible_ref in refs:
- if path.startswith(possible_ref['name'] + '/'):
- ref = possible_ref['name']
+ if path.startswith(possible_ref["name"] + "/"):
+ ref = possible_ref["name"]
file_path = path[len(ref) + 1:]
break
return ref, file_path
@@ -87,17 +88,17 @@ class CodeSnippets(Cog):
"""Fetches a snippet from a GitHub repo."""
# Search the GitHub API for the specified branch
branches = await self._fetch_response(
- f'https://api.github.com/repos/{repo}/branches',
- 'json',
+ f"https://api.github.com/repos/{repo}/branches",
+ "json",
headers=GITHUB_HEADERS
)
- tags = await self._fetch_response(f'https://api.github.com/repos/{repo}/tags', 'json', headers=GITHUB_HEADERS)
+ tags = await self._fetch_response(f"https://api.github.com/repos/{repo}/tags", "json", headers=GITHUB_HEADERS)
refs = branches + tags
ref, file_path = self._find_ref(path, refs)
file_contents = await self._fetch_response(
- f'https://api.github.com/repos/{repo}/contents/{file_path}?ref={ref}',
- 'text',
+ f"https://api.github.com/repos/{repo}/contents/{file_path}?ref={ref}",
+ "text",
headers=GITHUB_HEADERS,
)
return self._snippet_to_codeblock(file_contents, file_path, start_line, end_line)
@@ -113,19 +114,19 @@ class CodeSnippets(Cog):
"""Fetches a snippet from a GitHub gist."""
gist_json = await self._fetch_response(
f'https://api.github.com/gists/{gist_id}{f"/{revision}" if len(revision) > 0 else ""}',
- 'json',
+ "json",
headers=GITHUB_HEADERS,
)
# Check each file in the gist for the specified file
- for gist_file in gist_json['files']:
- if file_path == gist_file.lower().replace('.', '-'):
+ for gist_file in gist_json["files"]:
+ if file_path == gist_file.lower().replace(".", "-"):
file_contents = await self._fetch_response(
- gist_json['files'][gist_file]['raw_url'],
- 'text',
+ gist_json["files"][gist_file]["raw_url"],
+ "text",
)
return self._snippet_to_codeblock(file_contents, gist_file, start_line, end_line)
- return ''
+ return ""
async def _fetch_gitlab_snippet(
self,
@@ -139,18 +140,18 @@ class CodeSnippets(Cog):
# Searches the GitLab API for the specified branch
branches = await self._fetch_response(
- f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/branches',
- 'json'
+ f"https://gitlab.com/api/v4/projects/{enc_repo}/repository/branches",
+ "json"
)
- tags = await self._fetch_response(f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/tags', 'json')
+ tags = await self._fetch_response(f"https://gitlab.com/api/v4/projects/{enc_repo}/repository/tags", "json")
refs = branches + tags
ref, file_path = self._find_ref(path, refs)
enc_ref = quote_plus(ref)
enc_file_path = quote_plus(file_path)
file_contents = await self._fetch_response(
- f'https://gitlab.com/api/v4/projects/{enc_repo}/repository/files/{enc_file_path}/raw?ref={enc_ref}',
- 'text',
+ f"https://gitlab.com/api/v4/projects/{enc_repo}/repository/files/{enc_file_path}/raw?ref={enc_ref}",
+ "text",
)
return self._snippet_to_codeblock(file_contents, file_path, start_line, end_line)
@@ -164,8 +165,8 @@ class CodeSnippets(Cog):
) -> str:
"""Fetches a snippet from a BitBucket repo."""
file_contents = await self._fetch_response(
- f'https://bitbucket.org/{quote_plus(repo)}/raw/{quote_plus(ref)}/{quote_plus(file_path)}',
- 'text',
+ f"https://bitbucket.org/{quote_plus(repo)}/raw/{quote_plus(ref)}/{quote_plus(file_path)}",
+ "text",
)
return self._snippet_to_codeblock(file_contents, file_path, start_line, end_line)
@@ -194,31 +195,31 @@ class CodeSnippets(Cog):
if start_line > end_line:
start_line, end_line = end_line, start_line
if start_line > len(split_file_contents) or end_line < 1:
- return ''
+ return ""
start_line = max(1, start_line)
end_line = min(len(split_file_contents), end_line)
# Gets the code lines, dedents them, and inserts zero-width spaces to prevent Markdown injection
- required = '\n'.join(split_file_contents[start_line - 1:end_line])
- required = textwrap.dedent(required).rstrip().replace('`', '`\u200b')
+ required = "\n".join(split_file_contents[start_line - 1:end_line])
+ required = textwrap.dedent(required).rstrip().replace("`", "`\u200b")
# Extracts the code language and checks whether it's a "valid" language
- language = file_path.split('/')[-1].split('.')[-1]
- trimmed_language = language.replace('-', '').replace('+', '').replace('_', '')
+ language = file_path.split("/")[-1].split(".")[-1]
+ trimmed_language = language.replace("-", "").replace("+", "").replace("_", "")
is_valid_language = trimmed_language.isalnum()
if not is_valid_language:
- language = ''
+ language = ""
# Adds a label showing the file path to the snippet
if start_line == end_line:
- ret = f'`{file_path}` line {start_line}\n'
+ ret = f"`{file_path}` line {start_line}\n"
else:
- ret = f'`{file_path}` lines {start_line} to {end_line}\n'
+ ret = f"`{file_path}` lines {start_line} to {end_line}\n"
if len(required) != 0:
- return f'{ret}```{language}\n{required}```'
+ return f"{ret}```{language}\n{required}```"
# Returns an empty codeblock if the snippet is empty
- return f'{ret}``` ```'
+ return f"{ret}``` ```"
async def _parse_snippets(self, content: str) -> str:
"""Parse message content and return a string with a code block for each URL found."""
@@ -230,15 +231,15 @@ class CodeSnippets(Cog):
snippet = await handler(**match.groupdict())
all_snippets.append((match.start(), snippet))
except ClientResponseError as error:
- error_message = error.message # noqa: B306
+ error_message = error.message
log.log(
logging.DEBUG if error.status == 404 else logging.ERROR,
- f'Failed to fetch code snippet from {match[0]!r}: {error.status} '
- f'{error_message} for GET {error.request_info.real_url.human_repr()}'
+ f"Failed to fetch code snippet from {match[0]!r}: {error.status} "
+ f"{error_message} for GET {error.request_info.real_url.human_repr()}"
)
# Sorts the list of snippets by their match index and joins them into a single message
- return '\n'.join(map(lambda x: x[1], sorted(all_snippets)))
+ return "\n".join(x[1] for x in sorted(all_snippets))
@Cog.listener()
async def on_message(self, message: discord.Message) -> None:
@@ -252,7 +253,7 @@ class CodeSnippets(Cog):
message_to_send = await self._parse_snippets(message.content)
destination = message.channel
- if 0 < len(message_to_send) <= 2000 and message_to_send.count('\n') <= 15:
+ if 0 < len(message_to_send) <= 2000 and message_to_send.count("\n") <= 15:
try:
await message.edit(suppress=True)
except discord.NotFound:
@@ -265,8 +266,8 @@ class CodeSnippets(Cog):
destination = self.bot.get_channel(Channels.bot_commands)
await message.channel.send(
- 'The snippet you tried to send was too long. '
- f'Please see {destination.mention} for the full snippet.'
+ "The snippet you tried to send was too long. "
+ f"Please see {destination.mention} for the full snippet."
)
await wait_for_deletion(
diff --git a/bot/exts/info/codeblock/_cog.py b/bot/exts/info/codeblock/_cog.py
index e72f32887..24d956b9f 100644
--- a/bot/exts/info/codeblock/_cog.py
+++ b/bot/exts/info/codeblock/_cog.py
@@ -1,5 +1,4 @@
import time
-from typing import Optional
import discord
from discord import Message, RawMessageUpdateEvent
@@ -67,7 +66,7 @@ class CodeBlockCog(Cog, name="Code Block"):
"""Return an embed which displays code block formatting `instructions`."""
return discord.Embed(description=instructions)
- async def get_sent_instructions(self, payload: RawMessageUpdateEvent) -> Optional[Message]:
+ async def get_sent_instructions(self, payload: RawMessageUpdateEvent) -> Message | None:
"""
Return the bot's sent instructions message associated with a user's message `payload`.
diff --git a/bot/exts/info/codeblock/_instructions.py b/bot/exts/info/codeblock/_instructions.py
index 8fcadeec2..0a2bdf586 100644
--- a/bot/exts/info/codeblock/_instructions.py
+++ b/bot/exts/info/codeblock/_instructions.py
@@ -1,6 +1,5 @@
"""This module generates and formats instructional messages about fixing Markdown code blocks."""
-from typing import Optional
from bot.exts.info.codeblock import _parsing
from bot.log import get_logger
@@ -32,7 +31,7 @@ def _get_example(language: str) -> str:
return _EXAMPLE_CODE_BLOCKS.format(content=content)
-def _get_bad_ticks_message(code_block: _parsing.CodeBlock) -> Optional[str]:
+def _get_bad_ticks_message(code_block: _parsing.CodeBlock) -> str | None:
"""Return instructions on using the correct ticks for `code_block`."""
log.trace("Creating instructions for incorrect code block ticks.")
@@ -66,7 +65,7 @@ def _get_bad_ticks_message(code_block: _parsing.CodeBlock) -> Optional[str]:
return instructions
-def _get_no_ticks_message(content: str) -> Optional[str]:
+def _get_no_ticks_message(content: str) -> str | None:
"""If `content` is Python/REPL code, return instructions on using code blocks."""
log.trace("Creating instructions for a missing code block.")
@@ -79,11 +78,11 @@ def _get_no_ticks_message(content: str) -> Optional[str]:
"helps improve the legibility and makes it easier for us to help you.\n\n"
f"**To do this, use the following method:**\n{example_blocks}"
)
- else:
- log.trace("Aborting missing code block instructions: content is not Python code.")
+ log.trace("Aborting missing code block instructions: content is not Python code.")
+ return None
-def _get_bad_lang_message(content: str) -> Optional[str]:
+def _get_bad_lang_message(content: str) -> str | None:
"""
Return instructions on fixing the Python language specifier for a code block.
@@ -95,7 +94,7 @@ def _get_bad_lang_message(content: str) -> Optional[str]:
info = _parsing.parse_bad_language(content)
if not info:
log.trace("Aborting bad language instructions: language specified isn't Python.")
- return
+ return None
lines = []
language = info.language
@@ -120,11 +119,11 @@ def _get_bad_lang_message(content: str) -> Optional[str]:
f"It looks like you incorrectly specified a language for your code block.\n\n{lines}"
f"\n\n**Here is an example of how it should look:**\n{example_blocks}"
)
- else:
- log.trace("Nothing wrong with the language specifier; no instructions to return.")
+ log.trace("Nothing wrong with the language specifier; no instructions to return.")
+ return None
-def _get_no_lang_message(content: str) -> Optional[str]:
+def _get_no_lang_message(content: str) -> str | None:
"""
Return instructions on specifying a language for a code block.
@@ -142,11 +141,11 @@ def _get_no_lang_message(content: str) -> Optional[str]:
"it easier for us to help you.\n\n"
f"**To do this, use the following method:**\n{example_blocks}"
)
- else:
- log.trace("Aborting missing language instructions: content is not Python code.")
+ log.trace("Aborting missing language instructions: content is not Python code.")
+ return None
-def get_instructions(content: str) -> Optional[str]:
+def get_instructions(content: str) -> str | None:
"""
Parse `content` and return code block formatting instructions if something is wrong.
@@ -157,7 +156,7 @@ def get_instructions(content: str) -> Optional[str]:
blocks = _parsing.find_code_blocks(content)
if blocks is None:
log.trace("At least one valid code block found; no instructions to return.")
- return
+ return None
if not blocks:
log.trace("No code blocks were found in message.")
diff --git a/bot/exts/info/codeblock/_parsing.py b/bot/exts/info/codeblock/_parsing.py
index 3c193d6c5..abad09eef 100644
--- a/bot/exts/info/codeblock/_parsing.py
+++ b/bot/exts/info/codeblock/_parsing.py
@@ -3,7 +3,8 @@
import ast
import re
import textwrap
-from typing import NamedTuple, Optional, Sequence
+from collections.abc import Sequence
+from typing import NamedTuple
from bot import constants
from bot.log import get_logger
@@ -69,7 +70,7 @@ class BadLanguage(NamedTuple):
has_terminal_newline: bool
-def find_code_blocks(message: str) -> Optional[Sequence[CodeBlock]]:
+def find_code_blocks(message: str) -> Sequence[CodeBlock] | None:
"""
Find and return all Markdown code blocks in the `message`.
@@ -90,7 +91,7 @@ def find_code_blocks(message: str) -> Optional[Sequence[CodeBlock]]:
if groups["tick"] == BACKTICK and language:
log.trace("Message has a valid code block with a language; returning None.")
return None
- elif has_lines(groups["code"], constants.CodeBlock.minimum_lines):
+ if has_lines(groups["code"], constants.CodeBlock.minimum_lines):
code_block = CodeBlock(groups["code"], language, groups["tick"])
code_blocks.append(code_block)
else:
@@ -119,9 +120,9 @@ def _is_python_code(content: str) -> bool:
if not all(isinstance(node, ast.Expr) for node in tree.body):
log.trace("Code is valid python.")
return True
- else:
- log.trace("Code consists only of expressions.")
- return False
+
+ log.trace("Code consists only of expressions.")
+ return False
def _is_repl_code(content: str, threshold: int = 3) -> bool:
@@ -161,7 +162,7 @@ def is_python_code(content: str) -> bool:
)
-def parse_bad_language(content: str) -> Optional[BadLanguage]:
+def parse_bad_language(content: str) -> BadLanguage | None:
"""
Return information about a poorly formatted Python language in code block `content`.
@@ -188,6 +189,7 @@ def _get_leading_spaces(content: str) -> int:
leading_spaces += 1
else:
return leading_spaces
+ return None
def _fix_indentation(content: str) -> str:
diff --git a/bot/exts/info/doc/_batch_parser.py b/bot/exts/info/doc/_batch_parser.py
index 53d931830..79bfb5482 100644
--- a/bot/exts/info/doc/_batch_parser.py
+++ b/bot/exts/info/doc/_batch_parser.py
@@ -2,10 +2,10 @@ from __future__ import annotations
import asyncio
import collections
-from collections import defaultdict
+from collections import defaultdict, deque
from contextlib import suppress
from operator import attrgetter
-from typing import Deque, Dict, List, NamedTuple, Optional, Union
+from typing import NamedTuple
import discord
from bs4 import BeautifulSoup
@@ -61,7 +61,7 @@ class QueueItem(NamedTuple):
doc_item: _cog.DocItem
soup: BeautifulSoup
- def __eq__(self, other: Union[QueueItem, _cog.DocItem]):
+ def __eq__(self, other: QueueItem | _cog.DocItem):
if isinstance(other, _cog.DocItem):
return self.doc_item == other
return NamedTuple.__eq__(self, other)
@@ -90,14 +90,14 @@ class BatchParser:
"""
def __init__(self):
- self._queue: Deque[QueueItem] = collections.deque()
- self._page_doc_items: Dict[str, List[_cog.DocItem]] = defaultdict(list)
- self._item_futures: Dict[_cog.DocItem, ParseResultFuture] = defaultdict(ParseResultFuture)
+ self._queue: deque[QueueItem] = collections.deque()
+ self._page_doc_items: dict[str, list[_cog.DocItem]] = defaultdict(list)
+ self._item_futures: dict[_cog.DocItem, ParseResultFuture] = defaultdict(ParseResultFuture)
self._parse_task = None
self.stale_inventory_notifier = StaleInventoryNotifier()
- async def get_markdown(self, doc_item: _cog.DocItem) -> Optional[str]:
+ async def get_markdown(self, doc_item: _cog.DocItem) -> str | None:
"""
Get the result Markdown of `doc_item`.
@@ -164,7 +164,7 @@ class BatchParser:
self._parse_task = None
log.trace("Finished parsing queue.")
- def _move_to_front(self, item: Union[QueueItem, _cog.DocItem]) -> None:
+ def _move_to_front(self, item: QueueItem | _cog.DocItem) -> None:
"""Move `item` to the front of the parse queue."""
# The parse queue stores soups along with the doc symbols in QueueItem objects,
# in case we're moving a DocItem we have to get the associated QueueItem first and then move it.
diff --git a/bot/exts/info/doc/_cog.py b/bot/exts/info/doc/_cog.py
index 2d0f28406..c82f40557 100644
--- a/bot/exts/info/doc/_cog.py
+++ b/bot/exts/info/doc/_cog.py
@@ -6,7 +6,7 @@ import textwrap
from collections import defaultdict
from contextlib import suppress
from types import SimpleNamespace
-from typing import Dict, Literal, NamedTuple, Optional, Tuple, Union
+from typing import Literal, NamedTuple
import aiohttp
import discord
@@ -66,7 +66,7 @@ class DocCog(commands.Cog):
# Used to calculate inventory diffs on refreshes and to display all currently stored inventories.
self.base_urls = {}
self.bot = bot
- self.doc_symbols: Dict[str, DocItem] = {} # Maps symbol names to objects containing their metadata.
+ self.doc_symbols: dict[str, DocItem] = {} # Maps symbol names to objects containing their metadata.
self.item_fetcher = _batch_parser.BatchParser()
# Maps a conflicting symbol name to a list of the new, disambiguated names created from conflicts with the name.
self.renamed_symbols = defaultdict(list)
@@ -182,19 +182,17 @@ class DocCog(commands.Cog):
# Instead of renaming the current symbol, rename the symbol with which it conflicts.
self.doc_symbols[new_name] = self.doc_symbols[symbol_name]
return symbol_name
- else:
- return new_name
+ return new_name
# When there's a conflict, and the package names of the items differ, use the package name as a prefix.
if package_name != item.package:
if package_name in PRIORITY_PACKAGES:
return rename(item.package, rename_extant=True)
- else:
- return rename(package_name)
+ return rename(package_name)
# If the symbol's group is a non-priority group from FORCE_PREFIX_GROUPS,
# add it as a prefix to disambiguate the symbols.
- elif group_name in FORCE_PREFIX_GROUPS:
+ if group_name in FORCE_PREFIX_GROUPS:
if item.group in FORCE_PREFIX_GROUPS:
needs_moving = FORCE_PREFIX_GROUPS.index(group_name) < FORCE_PREFIX_GROUPS.index(item.group)
else:
@@ -203,8 +201,7 @@ class DocCog(commands.Cog):
# If the above conditions didn't pass, either the existing symbol has its group in FORCE_PREFIX_GROUPS,
# or deciding which item to rename would be arbitrary, so we rename the existing symbol.
- else:
- return rename(item.group, rename_extant=True)
+ return rename(item.group, rename_extant=True)
async def refresh_inventories(self) -> None:
"""Refresh internal documentation inventories."""
@@ -227,7 +224,7 @@ class DocCog(commands.Cog):
log.debug("Finished inventory refresh.")
self.refresh_event.set()
- def get_symbol_item(self, symbol_name: str) -> Tuple[str, Optional[DocItem]]:
+ def get_symbol_item(self, symbol_name: str) -> tuple[str, DocItem | None]:
"""
Get the `DocItem` and the symbol name used to fetch it from the `doc_symbols` dict.
@@ -267,7 +264,7 @@ class DocCog(commands.Cog):
return "Unable to parse the requested symbol."
return markdown
- async def create_symbol_embed(self, symbol_name: str) -> Optional[discord.Embed]:
+ async def create_symbol_embed(self, symbol_name: str) -> discord.Embed | None:
"""
Attempt to scrape and fetch the data for the given `symbol_name`, and build an embed from its contents.
@@ -305,12 +302,12 @@ class DocCog(commands.Cog):
return embed
@commands.group(name="docs", aliases=("doc", "d"), invoke_without_command=True)
- async def docs_group(self, ctx: commands.Context, *, symbol_name: Optional[str]) -> None:
+ async def docs_group(self, ctx: commands.Context, *, symbol_name: str | None) -> None:
"""Look up documentation for Python symbols."""
await self.get_command(ctx, symbol_name=symbol_name)
@docs_group.command(name="getdoc", aliases=("g",))
- async def get_command(self, ctx: commands.Context, *, symbol_name: Optional[str]) -> None:
+ async def get_command(self, ctx: commands.Context, *, symbol_name: str | None) -> None:
"""
Return a documentation embed for a given symbol.
@@ -452,7 +449,7 @@ class DocCog(commands.Cog):
async def clear_cache_command(
self,
ctx: commands.Context,
- package_name: Union[PackageName, Literal["*"]]
+ package_name: PackageName | Literal["*"]
) -> None:
"""Clear the persistent redis cache for `package`."""
if await doc_cache.delete(package_name):
diff --git a/bot/exts/info/doc/_html.py b/bot/exts/info/doc/_html.py
index 497246375..062d1bb05 100644
--- a/bot/exts/info/doc/_html.py
+++ b/bot/exts/info/doc/_html.py
@@ -1,5 +1,5 @@
+from collections.abc import Callable, Container, Iterable
from functools import partial
-from typing import Callable, Container, Iterable, List, Union
from bs4 import BeautifulSoup
from bs4.element import NavigableString, PageElement, SoupStrainer, Tag
@@ -32,26 +32,26 @@ class Strainer(SoupStrainer):
log.warning("`text` is not a supported kwarg in the custom strainer.")
super().__init__(**kwargs)
- Markup = Union[PageElement, List["Markup"]]
+ Markup = PageElement | list["Markup"]
- def search(self, markup: Markup) -> Union[PageElement, str]:
+ def search(self, markup: Markup) -> PageElement | str:
"""Extend default SoupStrainer behaviour to allow matching both `Tag`s` and `NavigableString`s."""
if isinstance(markup, str):
# Let everything through the text filter if we're including strings and tags.
if not self.name and not self.attrs and self.include_strings:
return markup
- else:
- return super().search(markup)
+ return None
+ return super().search(markup)
def _find_elements_until_tag(
start_element: PageElement,
- end_tag_filter: Union[Container[str], Callable[[Tag], bool]],
+ end_tag_filter: Container[str] | Callable[[Tag], bool],
*,
func: Callable,
include_strings: bool = False,
limit: int = None,
-) -> List[Union[Tag, NavigableString]]:
+) -> list[Tag | NavigableString]:
"""
Get all elements up to `limit` or until a tag matching `end_tag_filter` is found.
@@ -95,7 +95,7 @@ def _class_filter_factory(class_names: Iterable[str]) -> Callable[[Tag], bool]:
return match_tag
-def get_general_description(start_element: Tag) -> List[Union[Tag, NavigableString]]:
+def get_general_description(start_element: Tag) -> list[Tag | NavigableString]:
"""
Get page content to a table or a tag with its class in `SEARCH_END_TAG_ATTRS`.
@@ -108,13 +108,13 @@ def get_general_description(start_element: Tag) -> List[Union[Tag, NavigableStri
return _find_next_siblings_until_tag(start_tag, _class_filter_factory(_SEARCH_END_TAG_ATTRS), include_strings=True)
-def get_dd_description(symbol: PageElement) -> List[Union[Tag, NavigableString]]:
+def get_dd_description(symbol: PageElement) -> list[Tag | NavigableString]:
"""Get the contents of the next dd tag, up to a dt or a dl tag."""
description_tag = symbol.find_next("dd")
return _find_next_children_until_tag(description_tag, ("dt", "dl"), include_strings=True)
-def get_signatures(start_signature: PageElement) -> List[str]:
+def get_signatures(start_signature: PageElement) -> list[str]:
"""
Collect up to `_MAX_SIGNATURE_AMOUNT` signatures from dt tags around the `start_signature` dt tag.
diff --git a/bot/exts/info/doc/_inventory_parser.py b/bot/exts/info/doc/_inventory_parser.py
index e69246d47..4bfd5c919 100644
--- a/bot/exts/info/doc/_inventory_parser.py
+++ b/bot/exts/info/doc/_inventory_parser.py
@@ -1,7 +1,7 @@
import re
import zlib
from collections import defaultdict
-from typing import AsyncIterator, DefaultDict, List, Optional, Tuple
+from collections.abc import AsyncIterator
import aiohttp
@@ -11,9 +11,9 @@ from bot.log import get_logger
log = get_logger(__name__)
FAILED_REQUEST_ATTEMPTS = 3
-_V2_LINE_RE = re.compile(r'(?x)(.+?)\s+(\S*:\S*)\s+(-?\d+)\s+?(\S*)\s+(.*)')
+_V2_LINE_RE = re.compile(r"(?x)(.+?)\s+(\S*:\S*)\s+(-?\d+)\s+?(\S*)\s+(.*)")
-InventoryDict = DefaultDict[str, List[Tuple[str, str]]]
+InventoryDict = defaultdict[str, list[tuple[str, str]]]
class InvalidHeaderError(Exception):
@@ -38,14 +38,14 @@ class ZlibStreamReader:
async def __aiter__(self) -> AsyncIterator[str]:
"""Yield lines of decompressed text."""
- buf = b''
+ buf = b""
async for chunk in self._read_compressed_chunks():
buf += chunk
- pos = buf.find(b'\n')
+ pos = buf.find(b"\n")
while pos != -1:
yield buf[:pos].decode()
buf = buf[pos + 1:]
- pos = buf.find(b'\n')
+ pos = buf.find(b"\n")
async def _load_v1(stream: aiohttp.StreamReader) -> InventoryDict:
@@ -97,7 +97,7 @@ async def _fetch_inventory(url: str) -> InventoryDict:
if inventory_version == 1:
return await _load_v1(stream)
- elif inventory_version == 2:
+ if inventory_version == 2:
if b"zlib" not in await stream.readline():
raise InvalidHeaderError("'zlib' not found in header of compressed inventory.")
return await _load_v2(stream)
@@ -105,7 +105,7 @@ async def _fetch_inventory(url: str) -> InventoryDict:
raise InvalidHeaderError("Incompatible inventory version.")
-async def fetch_inventory(url: str) -> Optional[InventoryDict]:
+async def fetch_inventory(url: str) -> InventoryDict | None:
"""
Get an inventory dict from `url`, retrying `FAILED_REQUEST_ATTEMPTS` times on errors.
diff --git a/bot/exts/info/doc/_parsing.py b/bot/exts/info/doc/_parsing.py
index b37aadc01..dd2d6496c 100644
--- a/bot/exts/info/doc/_parsing.py
+++ b/bot/exts/info/doc/_parsing.py
@@ -4,7 +4,8 @@ import re
import string
import textwrap
from collections import namedtuple
-from typing import Collection, Iterable, Iterator, List, Optional, TYPE_CHECKING, Union
+from collections.abc import Collection, Iterable, Iterator
+from typing import TYPE_CHECKING
from bs4 import BeautifulSoup
from bs4.element import NavigableString, Tag
@@ -56,7 +57,7 @@ def _split_parameters(parameters_string: str) -> Iterator[str]:
"""
last_split = 0
depth = 0
- current_search: Optional[BracketPair] = None
+ current_search: BracketPair | None = None
enumerated_string = enumerate(parameters_string)
for index, character in enumerated_string:
@@ -92,7 +93,7 @@ def _split_parameters(parameters_string: str) -> Iterator[str]:
yield parameters_string[last_split:]
-def _truncate_signatures(signatures: Collection[str]) -> Union[List[str], Collection[str]]:
+def _truncate_signatures(signatures: Collection[str]) -> list[str] | Collection[str]:
"""
Truncate passed signatures to not exceed `_MAX_SIGNATURES_LENGTH`.
@@ -136,7 +137,7 @@ def _truncate_signatures(signatures: Collection[str]) -> Union[List[str], Collec
def _get_truncated_description(
- elements: Iterable[Union[Tag, NavigableString]],
+ elements: Iterable[Tag | NavigableString],
markdown_converter: DocMarkdownConverter,
max_length: int,
max_lines: int,
@@ -213,7 +214,7 @@ def _get_truncated_description(
return truncated_result.strip(_TRUNCATE_STRIP_CHARACTERS) + "..."
-def _create_markdown(signatures: Optional[List[str]], description: Iterable[Tag], url: str) -> str:
+def _create_markdown(signatures: list[str] | None, description: Iterable[Tag], url: str) -> str:
"""
Create a Markdown string with the signatures at the top, and the converted html description below them.
@@ -230,11 +231,10 @@ def _create_markdown(signatures: Optional[List[str]], description: Iterable[Tag]
if signatures is not None:
signature = "".join(f"```py\n{signature}```" for signature in _truncate_signatures(signatures))
return f"{signature}\n{description}"
- else:
- return description
+ return description
-def get_symbol_markdown(soup: BeautifulSoup, symbol_data: DocItem) -> Optional[str]:
+def get_symbol_markdown(soup: BeautifulSoup, symbol_data: DocItem) -> str | None:
"""
Return parsed Markdown of the passed item using the passed in soup, truncated to fit within a discord message.
diff --git a/bot/exts/info/doc/_redis_cache.py b/bot/exts/info/doc/_redis_cache.py
index ef9abd981..58627fafb 100644
--- a/bot/exts/info/doc/_redis_cache.py
+++ b/bot/exts/info/doc/_redis_cache.py
@@ -3,7 +3,7 @@ from __future__ import annotations
import datetime
import fnmatch
import time
-from typing import Optional, TYPE_CHECKING
+from typing import TYPE_CHECKING
from async_rediscache.types.base import RedisObject
@@ -66,7 +66,7 @@ class DocRedisCache(RedisObject):
await self.redis_session.client.expire(redis_key, WEEK_SECONDS)
log.info(f"Set {redis_key} to expire in a week.")
- async def get(self, item: DocItem) -> Optional[str]:
+ async def get(self, item: DocItem) -> str | None:
"""Return the Markdown content of the symbol `item` if it exists."""
return await self.redis_session.client.hget(f"{self.namespace}:{item_key(item)}", item.symbol_id)
diff --git a/bot/exts/info/help.py b/bot/exts/info/help.py
index 8950f4936..69d7de584 100644
--- a/bot/exts/info/help.py
+++ b/bot/exts/info/help.py
@@ -4,7 +4,6 @@ import itertools
import re
from collections import namedtuple
from contextlib import suppress
-from typing import List, Optional, Union
from discord import ButtonStyle, Colour, Embed, Emoji, Interaction, PartialEmoji, ui
from discord.ext.commands import Bot, Cog, Command, CommandError, Context, DisabledCommand, Group, HelpCommand
@@ -41,12 +40,12 @@ class SubcommandButton(ui.Button):
command: Command,
*,
style: ButtonStyle = ButtonStyle.primary,
- label: Optional[str] = None,
+ label: str | None = None,
disabled: bool = False,
- custom_id: Optional[str] = None,
- url: Optional[str] = None,
- emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
- row: Optional[int] = None
+ custom_id: str | None = None,
+ url: str | None = None,
+ emoji: str | Emoji | PartialEmoji | None = None,
+ row: int | None = None
):
super().__init__(
style=style, label=label, disabled=disabled, custom_id=custom_id, url=url, emoji=emoji, row=row
@@ -79,12 +78,12 @@ class GroupButton(ui.Button):
command: Command,
*,
style: ButtonStyle = ButtonStyle.secondary,
- label: Optional[str] = None,
+ label: str | None = None,
disabled: bool = False,
- custom_id: Optional[str] = None,
- url: Optional[str] = None,
- emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
- row: Optional[int] = None
+ custom_id: str | None = None,
+ url: str | None = None,
+ emoji: str | Emoji | PartialEmoji | None = None,
+ row: int | None = None
):
super().__init__(
style=style, label=label, disabled=disabled, custom_id=custom_id, url=url, emoji=emoji, row=row
@@ -123,7 +122,7 @@ class CommandView(ui.View):
if any(role.id in constants.MODERATION_ROLES for role in interaction.user.roles):
return True
- elif interaction.user.id == self.context.author.id:
+ if interaction.user.id == self.context.author.id:
return True
return False
@@ -150,7 +149,7 @@ class GroupView(CommandView):
self.add_item(SubcommandButton(help_command, subcommand, label=subcommand.name))
-class HelpQueryNotFound(ValueError):
+class HelpQueryNotFoundError(ValueError):
"""
Raised when a HelpSession Query doesn't match a command or cog.
@@ -244,7 +243,7 @@ class CustomHelpCommand(HelpCommand):
choices.update(cog.category for cog in self.context.bot.cogs.values() if hasattr(cog, "category"))
return choices
- async def command_not_found(self, query: str) -> "HelpQueryNotFound":
+ async def command_not_found(self, query: str) -> HelpQueryNotFoundError:
"""
Handles when a query does not match a valid command, group, cog or category.
@@ -257,9 +256,9 @@ class CustomHelpCommand(HelpCommand):
if len(query) >= 100:
query = query[:100] + "..."
- return HelpQueryNotFound(f'Query "{query}" not found.', {choice[0]: choice[1] for choice in result})
+ return HelpQueryNotFoundError(f'Query "{query}" not found.', {choice[0]: choice[1] for choice in result})
- async def subcommand_not_found(self, command: Command, string: str) -> "HelpQueryNotFound":
+ async def subcommand_not_found(self, command: Command, string: str) -> HelpQueryNotFoundError:
"""
Redirects the error to `command_not_found`.
@@ -267,7 +266,7 @@ class CustomHelpCommand(HelpCommand):
"""
return await self.command_not_found(f"{command.qualified_name} {string}")
- async def send_error_message(self, error: HelpQueryNotFound) -> None:
+ async def send_error_message(self, error: HelpQueryNotFoundError) -> None:
"""Send the error message to the channel."""
embed = Embed(colour=Colour.red(), title=str(error))
@@ -277,7 +276,7 @@ class CustomHelpCommand(HelpCommand):
await self.context.send(embed=embed)
- async def command_formatting(self, command: Command) -> tuple[Embed, Optional[CommandView]]:
+ async def command_formatting(self, command: Command) -> tuple[Embed, CommandView | None]:
"""
Takes a command and turns it into an embed.
@@ -326,7 +325,7 @@ class CustomHelpCommand(HelpCommand):
await wait_for_deletion(message, (self.context.author.id,))
@staticmethod
- def get_commands_brief_details(commands_: List[Command], return_as_list: bool = False) -> Union[List[str], str]:
+ def get_commands_brief_details(commands_: list[Command], return_as_list: bool = False) -> list[str] | str:
"""
Formats the prefix, command name and signature, and short doc for an iterable of commands.
@@ -340,10 +339,9 @@ class CustomHelpCommand(HelpCommand):
)
if return_as_list:
return details
- else:
- return "".join(details)
+ return "".join(details)
- async def format_group_help(self, group: Group) -> tuple[Embed, Optional[CommandView]]:
+ async def format_group_help(self, group: Group) -> tuple[Embed, CommandView | None]:
"""Formats help for a group command."""
subcommands = group.commands
@@ -398,8 +396,7 @@ class CustomHelpCommand(HelpCommand):
if command.cog.category:
return f"**{command.cog.category}**"
return f"**{command.cog_name}**"
- else:
- return "**\u200bNo Category:**"
+ return "**\u200bNo Category:**"
async def send_category_help(self, category: Category) -> None:
"""
diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py
index c680da2bc..ab9185867 100644
--- a/bot/exts/info/information.py
+++ b/bot/exts/info/information.py
@@ -2,8 +2,9 @@ import colorsys
import pprint
import textwrap
from collections import defaultdict
+from collections.abc import Mapping
from textwrap import shorten
-from typing import Any, DefaultDict, Mapping, Optional, Set, TYPE_CHECKING, Tuple, Union
+from typing import Any, TYPE_CHECKING
import rapidfuzz
from discord import AllowedMentions, Colour, Embed, Guild, Message, Role
@@ -44,7 +45,7 @@ class Information(Cog):
self.bot = bot
@staticmethod
- def get_channel_type_counts(guild: Guild) -> DefaultDict[str, int]:
+ def get_channel_type_counts(guild: Guild) -> defaultdict[str, int]:
"""Return the total amounts of the various types of channels in `guild`."""
channel_counter = defaultdict(int)
@@ -57,7 +58,7 @@ class Information(Cog):
return channel_counter
@staticmethod
- def join_role_stats(role_ids: list[int], guild: Guild, name: Optional[str] = None) -> dict[str, int]:
+ def join_role_stats(role_ids: list[int], guild: Guild, name: str | None = None) -> dict[str, int]:
"""Return a dictionary with the number of `members` of each role given, and the `name` for this joined group."""
member_count = 0
for role_id in role_ids:
@@ -134,7 +135,7 @@ class Information(Cog):
@has_any_role(*constants.STAFF_PARTNERS_COMMUNITY_ROLES)
@command(name="role")
- async def role_info(self, ctx: Context, *roles: Union[Role, str]) -> None:
+ async def role_info(self, ctx: Context, *roles: Role | str) -> None:
"""
Return information on a role or list of roles.
@@ -237,7 +238,7 @@ class Information(Cog):
await ctx.send(embed=embed)
@command(name="user", aliases=["user_info", "member", "member_info", "u"])
- async def user_info(self, ctx: Context, user_or_message: Union[MemberOrUser, Message] = None) -> None:
+ async def user_info(self, ctx: Context, user_or_message: MemberOrUser | Message = None) -> None:
"""Returns info about a user."""
if passed_as_message := isinstance(user_or_message, Message):
user = user_or_message.author
@@ -336,24 +337,24 @@ class Information(Cog):
return embed
- async def basic_user_infraction_counts(self, user: MemberOrUser) -> Tuple[str, str]:
+ async def basic_user_infraction_counts(self, user: MemberOrUser) -> tuple[str, str]:
"""Gets the total and active infraction counts for the given `member`."""
infractions = await self.bot.api_client.get(
- 'bot/infractions',
+ "bot/infractions",
params={
- 'hidden': 'False',
- 'user__id': str(user.id)
+ "hidden": "False",
+ "user__id": str(user.id)
}
)
total_infractions = len(infractions)
- active_infractions = sum(infraction['active'] for infraction in infractions)
+ active_infractions = sum(infraction["active"] for infraction in infractions)
infraction_output = f"Total: {total_infractions}\nActive: {active_infractions}"
return "Infractions", infraction_output
- async def expanded_user_infraction_counts(self, user: MemberOrUser) -> Tuple[str, str]:
+ async def expanded_user_infraction_counts(self, user: MemberOrUser) -> tuple[str, str]:
"""
Gets expanded infraction counts for the given `member`.
@@ -361,9 +362,9 @@ class Information(Cog):
in the output as well.
"""
infractions = await self.bot.api_client.get(
- 'bot/infractions',
+ "bot/infractions",
params={
- 'user__id': str(user.id)
+ "user__id": str(user.id)
}
)
@@ -376,7 +377,7 @@ class Information(Cog):
infraction_counter = defaultdict(int)
for infraction in infractions:
infraction_type = infraction["type"]
- infraction_active = 'active' if infraction["active"] else 'inactive'
+ infraction_active = "active" if infraction["active"] else "inactive"
infraction_types.add(infraction_type)
infraction_counter[f"{infraction_active} {infraction_type}"] += 1
@@ -394,12 +395,12 @@ class Information(Cog):
return "Infractions", "\n".join(infraction_output)
- async def user_nomination_counts(self, user: MemberOrUser) -> Tuple[str, str]:
+ async def user_nomination_counts(self, user: MemberOrUser) -> tuple[str, str]:
"""Gets the active and historical nomination counts for the given `member`."""
nominations = await self.bot.api_client.get(
- 'bot/nominations',
+ "bot/nominations",
params={
- 'user__id': str(user.id)
+ "user__id": str(user.id)
}
)
@@ -419,7 +420,7 @@ class Information(Cog):
return "Nominations", "\n".join(output)
- async def user_messages(self, user: MemberOrUser) -> Tuple[Union[bool, str], Tuple[str, str]]:
+ async def user_messages(self, user: MemberOrUser) -> tuple[bool | str, tuple[str, str]]:
"""
Gets the amount of messages for `member`.
@@ -438,12 +439,13 @@ class Information(Cog):
activity_output.append(f"{user_activity['activity_blocks']:,}" or "No activity")
activity_output = "\n".join(
- f"{name}: {metric}" for name, metric in zip(["Messages", "Activity blocks"], activity_output)
+ f"{name}: {metric}"
+ for name, metric in zip(["Messages", "Activity blocks"], activity_output, strict=True)
)
return ("Activity", activity_output)
- def format_fields(self, mapping: Mapping[str, Any], field_width: Optional[int] = None) -> str:
+ def format_fields(self, mapping: Mapping[str, Any], field_width: int | None = None) -> str:
"""Format a mapping to be readable to a human."""
# sorting is technically superfluous but nice if you want to look for a specific field
fields = sorted(mapping.items(), key=lambda item: item[0])
@@ -451,29 +453,29 @@ class Information(Cog):
if field_width is None:
field_width = len(max(mapping.keys(), key=len))
- out = ''
+ out = ""
for key, val in fields:
if isinstance(val, dict):
# if we have dicts inside dicts we want to apply the same treatment to the inner dictionaries
inner_width = int(field_width * 1.6)
- val = '\n' + self.format_fields(val, field_width=inner_width)
+ val = "\n" + self.format_fields(val, field_width=inner_width)
elif isinstance(val, str):
# split up text since it might be long
text = textwrap.fill(val, width=100, replace_whitespace=False)
# indent it, I guess you could do this with `wrap` and `join` but this is nicer
- val = textwrap.indent(text, ' ' * (field_width + len(': ')))
+ val = textwrap.indent(text, " " * (field_width + len(": ")))
# the first line is already indented so we `str.lstrip` it
val = val.lstrip()
- if key == 'color':
+ if key == "color":
# makes the base 10 representation of a hex number readable to humans
val = hex(val)
- out += '{0:>{width}}: {1}\n'.format(key, val, width=field_width)
+ out += "{0:>{width}}: {1}\n".format(key, val, width=field_width)
# remove trailing whitespace
return out.rstrip()
@@ -495,17 +497,17 @@ class Information(Cog):
paginator = Paginator()
def add_content(title: str, content: str) -> None:
- paginator.add_line(f'== {title} ==\n')
+ paginator.add_line(f"== {title} ==\n")
# Replace backticks as it breaks out of code blocks.
# An invisible character seemed to be the most reasonable solution. We hope it's not close to 2000.
- paginator.add_line(content.replace('`', '`\u200b'))
+ paginator.add_line(content.replace("`", "`\u200b"))
paginator.close_page()
if message.content:
- add_content('Raw message', message.content)
+ add_content("Raw message", message.content)
transformer = pprint.pformat if json else self.format_fields
- for field_name in ('embeds', 'attachments'):
+ for field_name in ("embeds", "attachments"):
data = raw_data[field_name]
if not data:
@@ -513,7 +515,7 @@ class Information(Cog):
total = len(data)
for current, item in enumerate(data, start=1):
- title = f'Raw {field_name} ({current}/{total})'
+ title = f"Raw {field_name} ({current}/{total})"
add_content(title, transformer(item))
for page in paginator.pages:
@@ -543,7 +545,7 @@ class Information(Cog):
self.rules.help = help_string
@command(aliases=("rule",))
- async def rules(self, ctx: Context, *, args: Optional[str]) -> Optional[Set[int]]:
+ async def rules(self, ctx: Context, *, args: str | None) -> set[int] | None:
"""
Provides a link to all rules or, if specified, displays specific rule(s).
@@ -574,7 +576,7 @@ class Information(Cog):
# Neither rules nor keywords were submitted. Return the default description.
rules_embed.description = DEFAULT_RULES_DESCRIPTION
await ctx.send(embed=rules_embed)
- return
+ return None
# Remove duplicates and sort the rule indices
rule_numbers = sorted(set(rule_numbers))
@@ -585,7 +587,7 @@ class Information(Cog):
if invalid:
await ctx.send(shorten(":x: Invalid rule indices: " + invalid, 75, placeholder=" ..."))
- return
+ return None
final_rules = []
final_rule_numbers = {keyword_to_rule_number[keyword] for keyword in keywords}
diff --git a/bot/exts/info/pep.py b/bot/exts/info/pep.py
index 6c659379f..6f50769d0 100644
--- a/bot/exts/info/pep.py
+++ b/bot/exts/info/pep.py
@@ -1,7 +1,6 @@
-from datetime import datetime, timedelta
+from datetime import UTC, datetime, timedelta
from email.parser import HeaderParser
from io import StringIO
-from typing import Dict, Optional, Tuple
from discord import Colour, Embed
from discord.ext.commands import Cog, Context, command
@@ -29,9 +28,9 @@ class PythonEnhancementProposals(Cog):
def __init__(self, bot: Bot):
self.bot = bot
- self.peps: Dict[int, str] = {}
+ self.peps: dict[int, str] = {}
# To avoid situations where we don't have last datetime, set this to now.
- self.last_refreshed_peps: datetime = datetime.now()
+ self.last_refreshed_peps: datetime = datetime.now(tz=UTC)
async def cog_load(self) -> None:
"""Carry out cog asynchronous initialisation."""
@@ -42,7 +41,7 @@ class PythonEnhancementProposals(Cog):
# Wait until HTTP client is available
await self.bot.wait_until_ready()
log.trace("Started refreshing PEP URLs.")
- self.last_refreshed_peps = datetime.now()
+ self.last_refreshed_peps = datetime.now(tz=UTC)
async with self.bot.http_session.get(
PEPS_LISTING_API_URL,
@@ -78,11 +77,11 @@ class PythonEnhancementProposals(Cog):
return pep_embed
- async def validate_pep_number(self, pep_nr: int) -> Optional[Embed]:
+ async def validate_pep_number(self, pep_nr: int) -> Embed | None:
"""Validate is PEP number valid. When it isn't, return error embed, otherwise None."""
if (
pep_nr not in self.peps
- and (self.last_refreshed_peps + timedelta(minutes=30)) <= datetime.now()
+ and (self.last_refreshed_peps + timedelta(minutes=30)) <= datetime.now(tz=UTC)
and len(str(pep_nr)) < 5
):
await self.refresh_peps_urls()
@@ -97,7 +96,7 @@ class PythonEnhancementProposals(Cog):
return None
- def generate_pep_embed(self, pep_header: Dict, pep_nr: int) -> Embed:
+ def generate_pep_embed(self, pep_header: dict, pep_nr: int) -> Embed:
"""Generate PEP embed based on PEP headers data."""
# the parsed header can be wrapped to multiple lines, so we need to make sure that is removed
# for an example of a pep with this issue, see pep 500
@@ -121,7 +120,7 @@ class PythonEnhancementProposals(Cog):
return pep_embed
@pep_cache(arg_offset=1)
- async def get_pep_embed(self, pep_nr: int) -> Tuple[Embed, bool]:
+ async def get_pep_embed(self, pep_nr: int) -> tuple[Embed, bool]:
"""Fetch, generate and return PEP embed. Second item of return tuple show does getting success."""
response = await self.bot.http_session.get(self.peps[pep_nr])
@@ -132,17 +131,17 @@ class PythonEnhancementProposals(Cog):
# Taken from https://github.com/python/peps/blob/master/pep0/pep.py#L179
pep_header = HeaderParser().parse(StringIO(pep_content))
return self.generate_pep_embed(pep_header, pep_nr), True
- else:
- log.trace(
- f"The user requested PEP {pep_nr}, but the response had an unexpected status code: {response.status}."
- )
- return Embed(
- title="Unexpected error",
- description="Unexpected HTTP error during PEP search. Please let us know.",
- colour=Colour.red()
- ), False
- @command(name='pep', aliases=('get_pep', 'p'))
+ log.trace(
+ f"The user requested PEP {pep_nr}, but the response had an unexpected status code: {response.status}."
+ )
+ return Embed(
+ title="Unexpected error",
+ description="Unexpected HTTP error during PEP search. Please let us know.",
+ colour=Colour.red()
+ ), False
+
+ @command(name="pep", aliases=("get_pep", "p"))
async def pep_command(self, ctx: Context, pep_number: int) -> None:
"""Fetches information about a PEP and sends it to the channel."""
# Trigger typing in chat to show users that bot is responding
diff --git a/bot/exts/info/python_news.py b/bot/exts/info/python_news.py
index 111b2dcaf..5f3575f4e 100644
--- a/bot/exts/info/python_news.py
+++ b/bot/exts/info/python_news.py
@@ -1,6 +1,6 @@
import re
import typing as t
-from datetime import date, datetime
+from datetime import UTC, datetime
import discord
import feedparser
@@ -39,7 +39,7 @@ class PythonNews(Cog):
def __init__(self, bot: Bot):
self.bot = bot
self.webhook_names = {}
- self.webhook: t.Optional[discord.Webhook] = None
+ self.webhook: discord.Webhook | None = None
async def cog_load(self) -> None:
"""Carry out cog asynchronous initialisation."""
@@ -108,14 +108,15 @@ class PythonNews(Cog):
data["entries"].reverse()
for new in data["entries"]:
try:
- new_datetime = datetime.strptime(new["published"], "%a, %d %b %Y %X %Z")
+ # %Z doesn't actually set the tzinfo of the datetime object, manually set this to UTC
+ new_datetime = datetime.strptime(new["published"], "%a, %d %b %Y %X %Z").replace(tzinfo=UTC)
except ValueError:
log.warning(f"Wrong datetime format passed in PEP new: {new['published']}")
continue
pep_nr = new["title"].split(":")[0].split()[1]
if (
pep_nr in pep_numbers
- or new_datetime.date() < date.today()
+ or new_datetime.date() < datetime.now(tz=UTC).date()
):
continue
@@ -181,8 +182,8 @@ class PythonNews(Cog):
if (
thread_information["thread_id"] in existing_news["data"][maillist]
- or 'Re: ' in thread_information["subject"]
- or new_date.date() < date.today()
+ or "Re: " in thread_information["subject"]
+ or new_date.date() < datetime.now(tz=UTC).date()
):
continue
@@ -223,7 +224,7 @@ class PythonNews(Cog):
await self.bot.api_client.put("bot/bot-settings/news", json=payload)
- async def get_thread_and_first_mail(self, maillist: str, thread_identifier: str) -> t.Tuple[t.Any, t.Any]:
+ async def get_thread_and_first_mail(self, maillist: str, thread_identifier: str) -> tuple[t.Any, t.Any]:
"""Get mail thread and first mail from mail.python.org based on `maillist` and `thread_identifier`."""
async with self.bot.http_session.get(
THREAD_TEMPLATE_URL.format(name=maillist, id=thread_identifier)
diff --git a/bot/exts/info/resources.py b/bot/exts/info/resources.py
index eeb9dd757..99caa2ae0 100644
--- a/bot/exts/info/resources.py
+++ b/bot/exts/info/resources.py
@@ -1,5 +1,4 @@
import re
-from typing import Optional
from urllib.parse import quote
from discord import Embed
@@ -25,7 +24,7 @@ def to_kebabcase(resource_topic: str) -> str:
# First, make it lowercase, and just remove any apostrophes.
# We remove the apostrophes because "wasnt" is better than "wasn-t"
resource_topic = resource_topic.casefold()
- resource_topic = resource_topic.replace("'", '')
+ resource_topic = resource_topic.replace("'", "")
# Now, replace any non-alphanumerics that remains with a dash.
# If there are multiple consecutive non-letters, just replace them with a single dash.
@@ -48,7 +47,7 @@ class Resources(commands.Cog):
self.bot = bot
@commands.command(name="resources", aliases=("res",))
- async def resources_command(self, ctx: commands.Context, *, resource_topic: Optional[str]) -> None:
+ async def resources_command(self, ctx: commands.Context, *, resource_topic: str | None) -> None:
"""Display information and a link to the Python Discord website Resources page."""
url = RESOURCE_URL
diff --git a/bot/exts/info/source.py b/bot/exts/info/source.py
index f735cc744..4514a3d77 100644
--- a/bot/exts/info/source.py
+++ b/bot/exts/info/source.py
@@ -1,6 +1,5 @@
import inspect
from pathlib import Path
-from typing import Optional, Tuple, Union
from discord import Embed
from discord.ext import commands
@@ -10,7 +9,7 @@ from bot.constants import URLs
from bot.converters import SourceConverter
from bot.exts.info.tags import TagIdentifier
-SourceType = Union[commands.HelpCommand, commands.Command, commands.Cog, TagIdentifier, commands.ExtensionNotLoaded]
+SourceType = commands.HelpCommand | commands.Command | commands.Cog | TagIdentifier | commands.ExtensionNotLoaded
class BotSource(commands.Cog):
@@ -32,7 +31,7 @@ class BotSource(commands.Cog):
embed = await self.build_embed(source_item)
await ctx.send(embed=embed)
- def get_source_link(self, source_item: SourceType) -> Tuple[str, str, Optional[int]]:
+ def get_source_link(self, source_item: SourceType) -> tuple[str, str, int | None]:
"""
Build GitHub link of source item, return this link, file location and first line number.
@@ -73,7 +72,7 @@ class BotSource(commands.Cog):
return url, file_location, first_line_no or None
- async def build_embed(self, source_object: SourceType) -> Optional[Embed]:
+ async def build_embed(self, source_object: SourceType) -> Embed | None:
"""Build embed based on source object."""
url, location, first_line = self.get_source_link(source_object)
diff --git a/bot/exts/info/stats.py b/bot/exts/info/stats.py
index 65a4b5b6c..af7663c28 100644
--- a/bot/exts/info/stats.py
+++ b/bot/exts/info/stats.py
@@ -42,10 +42,10 @@ class Stats(Cog):
return
channel = message.channel
- if hasattr(channel, 'parent') and channel.parent:
+ if hasattr(channel, "parent") and channel.parent:
channel = channel.parent
reformatted_name = CHANNEL_NAME_OVERRIDES.get(channel.id, channel.name)
- reformatted_name = "".join(char if char in ALLOWED_CHARS else '_' for char in reformatted_name)
+ reformatted_name = "".join(char if char in ALLOWED_CHARS else "_" for char in reformatted_name)
stat_name = f"channels.{reformatted_name}"
self.bot.stats.incr(stat_name)
diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py
index aff1302bb..5e5237f7f 100644
--- a/bot/exts/info/subscribe.py
+++ b/bot/exts/info/subscribe.py
@@ -1,6 +1,5 @@
import calendar
import operator
-import typing as t
from dataclasses import dataclass
import arrow
@@ -26,8 +25,8 @@ class AssignableRole:
"""
role_id: int
- months_available: t.Optional[tuple[int]]
- name: t.Optional[str] = None # This gets populated within Subscribe.cog_load()
+ months_available: tuple[int] | None
+ name: str | None = None # This gets populated within Subscribe.cog_load()
def is_currently_available(self) -> bool:
"""Check if the role is available for the current month."""
diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py
index 27dfa1913..42ac38be6 100644
--- a/bot/exts/info/tags.py
+++ b/bot/exts/info/tags.py
@@ -4,7 +4,7 @@ import enum
import re
import time
from pathlib import Path
-from typing import Literal, NamedTuple, Optional, Union
+from typing import Literal, NamedTuple
import discord
import frontmatter
@@ -37,7 +37,7 @@ class COOLDOWN(enum.Enum):
class TagIdentifier(NamedTuple):
"""Stores the group and name used as an identifier for a tag."""
- group: Optional[str]
+ group: str | None
name: str
def get_fuzzy_score(self, fuzz_tag_identifier: TagIdentifier) -> float:
@@ -59,8 +59,7 @@ class TagIdentifier(NamedTuple):
def __str__(self) -> str:
if self.group is not None:
return f"{self.group} {self.name}"
- else:
- return self.name
+ return self.name
@classmethod
def from_string(cls, string: str) -> TagIdentifier:
@@ -68,8 +67,7 @@ class TagIdentifier(NamedTuple):
split_string = string.removeprefix(constants.Bot.prefix).split(" ", maxsplit=2)
if len(split_string) == 1:
return cls(None, split_string[0])
- else:
- return cls(split_string[0], split_string[1])
+ return cls(split_string[0], split_string[1])
class Tag:
@@ -187,13 +185,13 @@ class Tags(Cog):
member: Member,
channel: discord.abc.Messageable,
tag_identifier: TagIdentifier,
- ) -> Optional[Union[Embed, Literal[COOLDOWN.obj]]]:
+ ) -> Embed | Literal[COOLDOWN.obj] | None:
"""
Generate an embed of the requested tag or of suggestions if the tag doesn't exist
or isn't accessible by the member.
If the requested tag is on cooldown return `COOLDOWN.obj`, otherwise if no suggestions were found return None.
- """ # noqa: D205, D415
+ """ # noqa: D205
filtered_tags = [
(ident, tag) for ident, tag in
self.get_fuzzy_matches(tag_identifier)[:10]
@@ -228,18 +226,17 @@ class Tags(Cog):
)
return tag.embed
- else:
- if not filtered_tags:
- return None
- suggested_tags_text = "\n".join(
- f"**\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}** {identifier}"
- for identifier, tag in filtered_tags
- if not tag.on_cooldown_in(channel)
- )
- return Embed(
- title="Did you mean ...",
- description=suggested_tags_text
- )
+ if not filtered_tags:
+ return None
+ suggested_tags_text = "\n".join(
+ f"**\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}** {identifier}"
+ for identifier, tag in filtered_tags
+ if not tag.on_cooldown_in(channel)
+ )
+ return Embed(
+ title="Did you mean ...",
+ description=suggested_tags_text
+ )
def accessible_tags(self, member: Member) -> list[str]:
"""Return a formatted list of tags that are accessible by `member`; groups first, and alphabetically sorted."""
@@ -318,7 +315,7 @@ class Tags(Cog):
@app_commands.command(name="tag")
@app_commands.guild_only()
- async def get_command(self, interaction: Interaction, *, name: Optional[str]) -> bool:
+ async def get_command(self, interaction: Interaction, *, name: str | None) -> bool:
"""
If a single argument matching a group name is given, list all accessible tags from that group
Otherwise display the tag if one was found for the given arguments, or try to display suggestions for that name.
@@ -327,7 +324,7 @@ class Tags(Cog):
Returns True if a message was sent, or if the tag is on cooldown.
Returns False if no message was sent.
- """ # noqa: D205, D415
+ """ # noqa: D205
if not name:
if self.tags:
await LinePaginator.paginate(
@@ -377,7 +374,7 @@ class Tags(Cog):
current: str
) -> list[app_commands.Choice[str]]:
"""Autocompleter for `/tag get` command."""
- names = [tag.name for tag in self.tags.keys()]
+ names = [tag.name for tag in self.tags]
choices = [
app_commands.Choice(name=tag, value=tag)
for tag in names if current.lower() in tag
diff --git a/bot/exts/moderation/clean.py b/bot/exts/moderation/clean.py
index aee751345..22a38a647 100644
--- a/bot/exts/moderation/clean.py
+++ b/bot/exts/moderation/clean.py
@@ -3,10 +3,11 @@ import itertools
import re
import time
from collections import defaultdict
+from collections.abc import Callable, Iterable
from contextlib import suppress
from datetime import datetime
from itertools import takewhile
-from typing import Callable, Iterable, Literal, Optional, TYPE_CHECKING, Union
+from typing import Literal, TYPE_CHECKING
from discord import Colour, Message, NotFound, TextChannel, Thread, User, errors
from discord.ext.commands import Cog, Context, Converter, Greedy, command, group, has_any_role
@@ -29,7 +30,7 @@ MESSAGE_DELETE_DELAY = 5
# Type alias for checks for whether a message should be deleted.
Predicate = Callable[[Message], bool]
# Type alias for message lookup ranges.
-CleanLimit = Union[Message, Age, ISODateTime]
+CleanLimit = Message | Age | ISODateTime
class CleanChannels(Converter):
@@ -37,7 +38,7 @@ class CleanChannels(Converter):
_channel_converter = TextChannelConverter()
- async def convert(self, ctx: Context, argument: str) -> Union[Literal["*"], list[TextChannel]]:
+ async def convert(self, ctx: Context, argument: str) -> Literal["*"] | list[TextChannel]:
"""Converts a string to a list of channels to clean, or the literal `*` for all public channels."""
if argument == "*":
return "*"
@@ -59,7 +60,7 @@ class Regex(Converter):
if TYPE_CHECKING: # Used to allow method resolution in IDEs like in converters.py.
- CleanChannels = Union[Literal["*"], list[TextChannel]] # noqa: F811
+ CleanChannels = Literal["*"] | list[TextChannel] # noqa: F811
Regex = re.Pattern # noqa: F811
@@ -87,11 +88,11 @@ class Clean(Cog):
@staticmethod
def _validate_input(
- channels: Optional[CleanChannels],
+ channels: CleanChannels | None,
bots_only: bool,
- users: Optional[list[User]],
- first_limit: Optional[CleanLimit],
- second_limit: Optional[CleanLimit],
+ users: list[User] | None,
+ first_limit: CleanLimit | None,
+ second_limit: CleanLimit | None,
) -> None:
"""Raise errors if an argument value or a combination of values is invalid."""
if first_limit is None:
@@ -133,7 +134,7 @@ class Clean(Cog):
if channels == "*":
channels = {
channel for channel in itertools.chain(ctx.guild.channels, ctx.guild.threads)
- if isinstance(channel, (TextChannel, Thread))
+ if isinstance(channel, TextChannel | Thread)
# Assume that non-public channels are not needed to optimize for speed.
and channel.permissions_for(ctx.guild.default_role).view_channel
}
@@ -145,10 +146,10 @@ class Clean(Cog):
@staticmethod
def _build_predicate(
first_limit: datetime,
- second_limit: Optional[datetime] = None,
+ second_limit: datetime | None = None,
bots_only: bool = False,
- users: Optional[list[User]] = None,
- regex: Optional[re.Pattern] = None,
+ users: list[User] | None = None,
+ regex: re.Pattern | None = None,
) -> Predicate:
"""Return the predicate that decides whether to delete a given message."""
def predicate_bots_only(message: Message) -> bool:
@@ -244,7 +245,7 @@ class Clean(Cog):
channels: Iterable[TextChannel],
to_delete: Predicate,
after: datetime,
- before: Optional[datetime] = None
+ before: datetime | None = None
) -> tuple[defaultdict[TextChannel, list], list]:
"""
Collect the messages for deletion by iterating over the histories of the appropriate channels.
@@ -343,7 +344,7 @@ class Clean(Cog):
messages: list[Message],
channels: CleanChannels,
ctx: Context
- ) -> Optional[str]:
+ ) -> str | None:
"""Log the deleted messages to the modlog, returning the log url if logging was successful."""
if not messages:
# Can't build an embed, nothing to clean!
@@ -381,14 +382,14 @@ class Clean(Cog):
async def _clean_messages(
self,
ctx: Context,
- channels: Optional[CleanChannels],
+ channels: CleanChannels | None,
bots_only: bool = False,
- users: Optional[list[User]] = None,
- regex: Optional[re.Pattern] = None,
- first_limit: Optional[CleanLimit] = None,
- second_limit: Optional[CleanLimit] = None,
+ users: list[User] | None = None,
+ regex: re.Pattern | None = None,
+ first_limit: CleanLimit | None = None,
+ second_limit: CleanLimit | None = None,
attempt_delete_invocation: bool = True,
- ) -> Optional[str]:
+ ) -> str | None:
"""A helper function that does the actual message cleaning, returns the log url if logging was successful."""
self._validate_input(channels, bots_only, users, first_limit, second_limit)
@@ -464,10 +465,10 @@ class Clean(Cog):
self,
ctx: Context,
users: Greedy[User] = None,
- first_limit: Optional[CleanLimit] = None,
- second_limit: Optional[CleanLimit] = None,
- regex: Optional[Regex] = None,
- bots_only: Optional[bool] = False,
+ first_limit: CleanLimit | None = None,
+ second_limit: CleanLimit | None = None,
+ regex: Regex | None = None,
+ bots_only: bool | None = False,
*,
channels: CleanChannels = None # "Optional" with discord.py silently ignores incorrect input.
) -> None:
@@ -627,7 +628,7 @@ class Clean(Cog):
await self._delete_invocation(ctx)
@command()
- async def purge(self, ctx: Context, users: Greedy[User], age: Optional[Union[Age, ISODateTime]] = None) -> None:
+ async def purge(self, ctx: Context, users: Greedy[User], age: Age | ISODateTime | None = None) -> None:
"""
Clean messages of `users` from all public channels up to a certain message `age` (10 minutes by default).
diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py
index ee870ea57..3c16f8e0e 100644
--- a/bot/exts/moderation/defcon.py
+++ b/bot/exts/moderation/defcon.py
@@ -3,7 +3,6 @@ import traceback
from collections import namedtuple
from datetime import datetime
from enum import Enum
-from typing import Optional, Union
import arrow
from async_rediscache import RedisCache
@@ -44,7 +43,7 @@ SECONDS_IN_DAY = 86400
class Action(Enum):
"""Defcon Action."""
- ActionInfo = namedtuple('LogInfoDetails', ['icon', 'emoji', 'color', 'template'])
+ ActionInfo = namedtuple("LogInfoDetails", ["icon", "emoji", "color", "template"])
SERVER_OPEN = ActionInfo(Icons.defcon_unshutdown, Emojis.defcon_unshutdown, Colours.soft_green, "")
SERVER_SHUTDOWN = ActionInfo(Icons.defcon_shutdown, Emojis.defcon_shutdown, Colours.soft_red, "")
@@ -142,13 +141,13 @@ class Defcon(Cog):
message, member.display_avatar.url
)
- @group(name='defcon', aliases=('dc',), invoke_without_command=True)
+ @group(name="defcon", aliases=("dc",), invoke_without_command=True)
@has_any_role(*MODERATION_ROLES)
async def defcon_group(self, ctx: Context) -> None:
"""Check the DEFCON status or run a subcommand."""
await ctx.send_help(ctx.command)
- @defcon_group.command(aliases=('s',))
+ @defcon_group.command(aliases=("s",))
@has_any_role(*MODERATION_ROLES)
async def status(self, ctx: Context) -> None:
"""Check the current status of DEFCON mode."""
@@ -164,10 +163,10 @@ class Defcon(Cog):
await ctx.send(embed=embed)
- @defcon_group.command(name="threshold", aliases=('t', 'd'))
+ @defcon_group.command(name="threshold", aliases=("t", "d"))
@has_any_role(*MODERATION_ROLES)
async def threshold_command(
- self, ctx: Context, threshold: Union[DurationDelta, int], expiry: Optional[Expiry] = None
+ self, ctx: Context, threshold: DurationDelta | int, expiry: Expiry | None = None
) -> None:
"""
Set how old an account must be to join the server.
@@ -215,7 +214,7 @@ class Defcon(Cog):
async def _update_channel_topic(self) -> None:
"""Update the #defcon channel topic with the current DEFCON status."""
- threshold = time.humanize_delta(self.threshold) if self.threshold else '-'
+ threshold = time.humanize_delta(self.threshold) if self.threshold else "-"
new_topic = f"{BASE_CHANNEL_TOPIC}\n(Threshold: {threshold})"
(await self.get_mod_log()).ignore(Event.guild_channel_update, Channels.defcon)
@@ -227,7 +226,7 @@ class Defcon(Cog):
author: User,
channel: TextChannel,
threshold: relativedelta,
- expiry: Optional[Expiry] = None
+ expiry: Expiry | None = None
) -> None:
"""Update the new threshold in the cog, cache, defcon channel, and logs, and additionally schedule expiry."""
self.threshold = threshold
@@ -247,8 +246,8 @@ class Defcon(Cog):
try:
await self.defcon_settings.update(
{
- 'threshold': Defcon._stringify_relativedelta(self.threshold) if self.threshold else "",
- 'expiry': expiry.isoformat() if expiry else 0
+ "threshold": Defcon._stringify_relativedelta(self.threshold) if self.threshold else "",
+ "expiry": expiry.isoformat() if expiry else 0
}
)
except RedisError:
diff --git a/bot/exts/moderation/incidents.py b/bot/exts/moderation/incidents.py
index 3e06cc215..e953eefe7 100644
--- a/bot/exts/moderation/incidents.py
+++ b/bot/exts/moderation/incidents.py
@@ -1,8 +1,7 @@
import asyncio
import re
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from enum import Enum
-from typing import Optional
import discord
from async_rediscache import RedisCache
@@ -53,10 +52,10 @@ ALL_SIGNALS: set[str] = {signal.value for signal in Signal}
# An embed coupled with an optional file to be dispatched
# If the file is not None, the embed attempts to show it in its body
-FileEmbed = tuple[discord.Embed, Optional[discord.File]]
+FileEmbed = tuple[discord.Embed, discord.File | None]
-async def download_file(attachment: discord.Attachment) -> Optional[discord.File]:
+async def download_file(attachment: discord.Attachment) -> discord.File | None:
"""
Download & return `attachment` file.
@@ -111,7 +110,7 @@ async def make_embed(incident: discord.Message, outcome: Signal, actioned_by: di
embed = discord.Embed(
description=description,
colour=colour,
- timestamp=datetime.now(timezone.utc)
+ timestamp=datetime.now(UTC)
)
embed.set_footer(text=footer, icon_url=actioned_by.display_avatar.url)
@@ -178,7 +177,7 @@ def shorten_text(text: str) -> str:
return text
-async def make_message_link_embed(ctx: Context, message_link: str) -> Optional[discord.Embed]:
+async def make_message_link_embed(ctx: Context, message_link: str) -> discord.Embed | None:
"""
Create an embedded representation of the discord message link contained in the incident report.
@@ -228,7 +227,7 @@ async def make_message_link_embed(ctx: Context, message_link: str) -> Optional[d
f"Helpers don't have read permissions in #{channel.name},"
f" not sending message link embed for {message_link}"
)
- return
+ return None
embed = discord.Embed(
colour=discord.Colour.gold(),
@@ -487,7 +486,7 @@ class Incidents(Cog):
# Deletes the message link embeds found in cache from the channel and cache.
await self.delete_msg_link_embed(incident.id)
- async def resolve_message(self, message_id: int) -> Optional[discord.Message]:
+ async def resolve_message(self, message_id: int) -> discord.Message | None:
"""
Get `discord.Message` for `message_id` from cache, or API.
@@ -502,7 +501,7 @@ class Incidents(Cog):
"""
await self.bot.wait_until_guild_available() # First make sure that the cache is ready
log.trace(f"Resolving message for: {message_id=}")
- message: Optional[discord.Message] = self.bot._connection._get_message(message_id)
+ message: discord.Message | None = self.bot._connection._get_message(message_id)
if message is not None:
log.trace("Message was found in cache")
@@ -595,7 +594,7 @@ class Incidents(Cog):
if self.incidents_webhook:
await self.delete_msg_link_embed(payload.message_id)
- async def extract_message_links(self, message: discord.Message) -> Optional[list[discord.Embed]]:
+ async def extract_message_links(self, message: discord.Message) -> list[discord.Embed] | None:
"""
Check if there's any message links in the text content.
@@ -612,7 +611,7 @@ class Incidents(Cog):
log.trace(
f"No message links detected on incident message with id {message.id}."
)
- return
+ return None
embeds = []
for message_link in message_links[:10]:
@@ -628,7 +627,7 @@ class Incidents(Cog):
webhook_embed_list: list,
message: discord.Message,
webhook: discord.Webhook,
- ) -> Optional[int]:
+ ) -> int | None:
"""
Send message link embeds to #incidents channel.
diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py
index 78f140e28..a079f775e 100644
--- a/bot/exts/moderation/infraction/_scheduler.py
+++ b/bot/exts/moderation/infraction/_scheduler.py
@@ -80,7 +80,7 @@ class InfractionScheduler:
async def reapply_infraction(
self,
infraction: _utils.Infraction,
- action: t.Optional[Callable[[], Awaitable[None]]]
+ action: Callable[[], Awaitable[None]] | None
) -> None:
"""
Reapply an infraction if it's still active or deactivate it if less than 60 sec left.
@@ -127,8 +127,8 @@ class InfractionScheduler:
ctx: Context,
infraction: _utils.Infraction,
user: MemberOrUser,
- action: t.Optional[Callable[[], Awaitable[None]]] = None,
- user_reason: t.Optional[str] = None,
+ action: Callable[[], Awaitable[None]] | None = None,
+ user_reason: str | None = None,
additional_info: str = "",
) -> bool:
"""
@@ -146,8 +146,8 @@ class InfractionScheduler:
infr_type = infraction["type"]
icon = _utils.INFRACTION_ICONS[infr_type][0]
reason = infraction["reason"]
- id_ = infraction['id']
- jump_url = infraction['jump_url']
+ id_ = infraction["id"]
+ jump_url = infraction["jump_url"]
expiry = time.format_with_duration(
infraction["expires_at"],
infraction["last_applied"]
@@ -294,7 +294,7 @@ class InfractionScheduler:
ctx: Context,
infr_type: str,
user: MemberOrUser,
- pardon_reason: t.Optional[str] = None,
+ pardon_reason: str | None = None,
*,
send_msg: bool = True,
notify: bool = True
@@ -315,11 +315,11 @@ class InfractionScheduler:
# Check the current active infraction
log.trace(f"Fetching active {infr_type} infractions for {user}.")
response = await self.bot.api_client.get(
- 'bot/infractions',
+ "bot/infractions",
params={
- 'active': 'true',
- 'type': infr_type,
- 'user__id': user.id
+ "active": "true",
+ "type": infr_type,
+ "user__id": user.id
}
)
@@ -334,7 +334,7 @@ class InfractionScheduler:
log_text["Member"] = messages.format_user(user)
log_text["Actor"] = ctx.author.mention
log_content = None
- id_ = response[0]['id']
+ id_ = response[0]["id"]
footer = f"ID: {id_}"
# Accordingly display whether the user was successfully notified via DM.
@@ -382,11 +382,11 @@ class InfractionScheduler:
async def deactivate_infraction(
self,
infraction: _utils.Infraction,
- pardon_reason: t.Optional[str] = None,
+ pardon_reason: str | None = None,
*,
send_log: bool = True,
notify: bool = True
- ) -> t.Dict[str, str]:
+ ) -> dict[str, str]:
"""
Deactivate an active infraction and return a dictionary of lines to send in a mod log.
@@ -524,7 +524,7 @@ class InfractionScheduler:
self,
infraction: _utils.Infraction,
notify: bool
- ) -> t.Optional[t.Dict[str, str]]:
+ ) -> dict[str, str] | None:
"""
Execute deactivation steps specific to the infraction's type and return a log dict.
diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py
index 18d296752..8d8c76741 100644
--- a/bot/exts/moderation/infraction/_utils.py
+++ b/bot/exts/moderation/infraction/_utils.py
@@ -1,4 +1,3 @@
-import typing as t
import arrow
import discord
@@ -29,7 +28,7 @@ INFRACTION_ICONS = {
RULES_URL = "https://pythondiscord.com/pages/rules"
# Type aliases
-Infraction = t.Dict[str, t.Union[str, int, bool]]
+Infraction = dict[str, str | int | bool]
APPEAL_SERVER_INVITE = "https://discord.gg/WXrCJxWBnm"
MODMAIL_ACCOUNT_ID = "683001325440860340"
@@ -37,8 +36,8 @@ MODMAIL_ACCOUNT_ID = "683001325440860340"
INFRACTION_TITLE = "Please review our rules"
INFRACTION_APPEAL_SERVER_FOOTER = f"\nTo appeal this infraction, join our [appeals server]({APPEAL_SERVER_INVITE})."
INFRACTION_APPEAL_MODMAIL_FOOTER = (
- '\nIf you would like to discuss or appeal this infraction, '
- f'send a message to the ModMail bot (<@{MODMAIL_ACCOUNT_ID}>).'
+ "\nIf you would like to discuss or appeal this infraction, "
+ f"send a message to the ModMail bot (<@{MODMAIL_ACCOUNT_ID}>)."
)
INFRACTION_AUTHOR_NAME = "Infraction information"
@@ -52,7 +51,7 @@ INFRACTION_DESCRIPTION_TEMPLATE = (
)
-async def post_user(ctx: Context, user: MemberOrUser) -> t.Optional[dict]:
+async def post_user(ctx: Context, user: MemberOrUser) -> dict | None:
"""
Create a new user in the database.
@@ -61,15 +60,15 @@ async def post_user(ctx: Context, user: MemberOrUser) -> t.Optional[dict]:
log.trace(f"Attempting to add user {user.id} to the database.")
payload = {
- 'discriminator': int(user.discriminator),
- 'id': user.id,
- 'in_guild': False,
- 'name': user.name,
- 'roles': []
+ "discriminator": int(user.discriminator),
+ "id": user.id,
+ "in_guild": False,
+ "name": user.name,
+ "roles": []
}
try:
- response = await ctx.bot.api_client.post('bot/users', json=payload)
+ response = await ctx.bot.api_client.post("bot/users", json=payload)
log.info(f"User {user.id} added to the DB.")
return response
except ResponseCodeError as e:
@@ -82,13 +81,13 @@ async def post_infraction(
user: MemberOrUser,
infr_type: str,
reason: str,
- duration_or_expiry: t.Optional[DurationOrExpiry] = None,
+ duration_or_expiry: DurationOrExpiry | None = None,
hidden: bool = False,
active: bool = True,
dm_sent: bool = False,
-) -> t.Optional[dict]:
+) -> dict | None:
"""Posts an infraction to the API."""
- if isinstance(user, (discord.Member, discord.User)) and user.bot:
+ if isinstance(user, discord.Member | discord.User) and user.bot:
log.trace(f"Posting of {infr_type} infraction for {user} to the API aborted. User is a bot.")
raise InvalidInfractedUserError(user)
@@ -124,17 +123,18 @@ async def post_infraction(
# Try to apply the infraction. If it fails because the user doesn't exist, try to add it.
for should_post_user in (True, False):
try:
- response = await ctx.bot.api_client.post('bot/infractions', json=payload)
+ response = await ctx.bot.api_client.post("bot/infractions", json=payload)
return response
except ResponseCodeError as e:
- if e.status == 400 and 'user' in e.response_json:
+ if e.status == 400 and "user" in e.response_json:
# Only one attempt to add the user to the database, not two:
if not should_post_user or await post_user(ctx, user) is None:
- return
+ return None
else:
log.exception(f"Unexpected error while adding an infraction for {user}:")
await ctx.send(f":x: There was an error adding the infraction: status {e.status}.")
- return
+ return None
+ return None
async def get_active_infraction(
@@ -142,7 +142,7 @@ async def get_active_infraction(
user: MemberOrUser,
infr_type: str,
send_msg: bool = True
-) -> t.Optional[dict]:
+) -> dict | None:
"""
Retrieves an active infraction of the given type for the user.
@@ -153,11 +153,11 @@ async def get_active_infraction(
log.trace(f"Checking if {user} has active infractions of type {infr_type}.")
active_infractions = await ctx.bot.api_client.get(
- 'bot/infractions',
+ "bot/infractions",
params={
- 'active': 'true',
- 'type': infr_type,
- 'user__id': str(user.id)
+ "active": "true",
+ "type": infr_type,
+ "user__id": str(user.id)
}
)
if active_infractions:
@@ -166,8 +166,8 @@ async def get_active_infraction(
log.trace(f"{user} has active infractions of type {infr_type}.")
await send_active_infraction_message(ctx, active_infractions[0])
return active_infractions[0]
- else:
- log.trace(f"{user} does not have active infractions of type {infr_type}.")
+ log.trace(f"{user} does not have active infractions of type {infr_type}.")
+ return None
async def send_active_infraction_message(ctx: Context, infraction: Infraction) -> None:
@@ -181,7 +181,7 @@ async def send_active_infraction_message(ctx: Context, infraction: Infraction) -
async def notify_infraction(
infraction: Infraction,
user: MemberOrUser,
- reason: t.Optional[str] = None
+ reason: str | None = None
) -> bool:
"""
DM a user about their new infraction and return True if the DM is successful.
@@ -224,7 +224,7 @@ async def notify_infraction(
if len(text) > 4096 - LONGEST_EXTRAS:
text = f"{text[:4093-LONGEST_EXTRAS]}..."
- text += INFRACTION_APPEAL_SERVER_FOOTER if infraction["type"] == 'ban' else INFRACTION_APPEAL_MODMAIL_FOOTER
+ text += INFRACTION_APPEAL_SERVER_FOOTER if infraction["type"] == "ban" else INFRACTION_APPEAL_MODMAIL_FOOTER
embed = discord.Embed(
description=text,
diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py
index e785216c9..15d80cd58 100644
--- a/bot/exts/moderation/infraction/infractions.py
+++ b/bot/exts/moderation/infraction/infractions.py
@@ -30,8 +30,9 @@ if t.TYPE_CHECKING:
# Comp ban
-LINK_PASSWORD = "https://support.discord.com/hc/en-us/articles/218410947-I-forgot-my-Password-Where-can-I-set-a-new-one"
-LINK_2FA = "https://support.discord.com/hc/en-us/articles/219576828-Setting-up-Two-Factor-Authentication"
+DISCORD_ARTICLE_URL = "https://support.discord.com/hc/en-us/articles"
+LINK_PASSWORD = DISCORD_ARTICLE_URL + "/218410947-I-forgot-my-Password-Where-can-I-set-a-new-one"
+LINK_2FA = DISCORD_ARTICLE_URL + "/219576828-Setting-up-Two-Factor-Authentication"
COMP_BAN_REASON = (
"Your account has been used to send links to a phishing website. You have been automatically banned. "
"If you are not aware of sending them, that means your account has been compromised.\n\n"
@@ -68,7 +69,7 @@ class Infractions(InfractionScheduler, commands.Cog):
# region: Permanent infractions
@command(aliases=("warning",))
- async def warn(self, ctx: Context, user: UnambiguousMemberOrUser, *, reason: t.Optional[str] = None) -> None:
+ async def warn(self, ctx: Context, user: UnambiguousMemberOrUser, *, reason: str | None = None) -> None:
"""Warn a user for the given reason."""
if not isinstance(user, Member):
await ctx.send(":x: The user doesn't appear to be on the server.")
@@ -81,7 +82,7 @@ class Infractions(InfractionScheduler, commands.Cog):
await self.apply_infraction(ctx, infraction, user)
@command()
- async def kick(self, ctx: Context, user: UnambiguousMemberOrUser, *, reason: t.Optional[str] = None) -> None:
+ async def kick(self, ctx: Context, user: UnambiguousMemberOrUser, *, reason: str | None = None) -> None:
"""Kick a user for the given reason."""
if not isinstance(user, Member):
await ctx.send(":x: The user doesn't appear to be on the server.")
@@ -95,9 +96,9 @@ class Infractions(InfractionScheduler, commands.Cog):
self,
ctx: Context,
user: UnambiguousMemberOrUser,
- duration_or_expiry: t.Optional[DurationOrExpiry] = None,
+ duration_or_expiry: DurationOrExpiry | None = None,
*,
- reason: t.Optional[str] = None
+ reason: str | None = None
) -> None:
"""
Permanently ban a `user` for the given `reason` and stop watching them with Big Brother.
@@ -114,16 +115,16 @@ class Infractions(InfractionScheduler, commands.Cog):
self,
ctx: Context,
user: UnambiguousMemberOrUser,
- duration: t.Optional[DurationOrExpiry] = None,
+ duration: DurationOrExpiry | None = None,
*,
- reason: t.Optional[str] = None
+ reason: str | None = None
) -> None:
"""
Same as ban, but also cleans all their messages from the last hour.
If duration is specified, it temporarily bans that user for the given duration.
"""
- clean_cog: t.Optional[Clean] = self.bot.get_cog("Clean")
+ clean_cog: Clean | None = self.bot.get_cog("Clean")
if clean_cog is None:
# If we can't get the clean cog, fall back to native purgeban.
await self.apply_ban(ctx, user, reason, purge_days=1, duration_or_expiry=duration)
@@ -148,7 +149,7 @@ class Infractions(InfractionScheduler, commands.Cog):
# Cleaning failed, or there were no messages to clean, exit early.
return
- infr_manage_cog: t.Optional[ModManagement] = self.bot.get_cog("ModManagement")
+ infr_manage_cog: ModManagement | None = self.bot.get_cog("ModManagement")
if infr_manage_cog is None:
# If we can't get the mod management cog, don't bother appending the log.
return
@@ -182,9 +183,9 @@ class Infractions(InfractionScheduler, commands.Cog):
self,
ctx: Context,
user: UnambiguousMemberOrUser,
- duration: t.Optional[DurationOrExpiry] = None,
+ duration: DurationOrExpiry | None = None,
*,
- reason: t.Optional[str]
+ reason: str | None
) -> None:
"""
Permanently mute user in voice channels.
@@ -201,9 +202,9 @@ class Infractions(InfractionScheduler, commands.Cog):
async def timeout(
self, ctx: Context,
user: UnambiguousMemberOrUser,
- duration: t.Optional[DurationOrExpiry] = None,
+ duration: DurationOrExpiry | None = None,
*,
- reason: t.Optional[str] = None
+ reason: str | None = None
) -> None:
"""
Timeout a user for the given reason and duration.
@@ -221,7 +222,7 @@ class Infractions(InfractionScheduler, commands.Cog):
Alternatively, an ISO 8601 timestamp can be provided for the duration.
If no duration is given, a one-hour duration is used by default.
- """
+ """ # noqa: RUF002
if not isinstance(user, Member):
await ctx.send(":x: The user doesn't appear to be on the server.")
return
@@ -255,7 +256,7 @@ class Infractions(InfractionScheduler, commands.Cog):
user: UnambiguousMemberOrUser,
duration_or_expiry: DurationOrExpiry,
*,
- reason: t.Optional[str] = None
+ reason: str | None = None
) -> None:
"""
Temporarily ban a user for the given reason and duration.
@@ -271,7 +272,7 @@ class Infractions(InfractionScheduler, commands.Cog):
\u2003`s` - seconds
Alternatively, an ISO 8601 timestamp can be provided for the duration.
- """
+ """ # noqa: RUF002
await self.apply_ban(ctx, user, reason, duration_or_expiry=duration_or_expiry)
@command(aliases=("tempvban", "tvban"))
@@ -291,7 +292,7 @@ class Infractions(InfractionScheduler, commands.Cog):
user: UnambiguousMemberOrUser,
duration: DurationOrExpiry,
*,
- reason: t.Optional[str]
+ reason: str | None
) -> None:
"""
Temporarily voice mute a user for the given reason and duration.
@@ -307,14 +308,14 @@ class Infractions(InfractionScheduler, commands.Cog):
\u2003`s` - seconds
Alternatively, an ISO 8601 timestamp can be provided for the duration.
- """
+ """ # noqa: RUF002
await self.apply_voice_mute(ctx, user, reason, duration_or_expiry=duration)
# endregion
# region: Permanent shadow infractions
@command(hidden=True)
- async def note(self, ctx: Context, user: UnambiguousMemberOrUser, *, reason: t.Optional[str] = None) -> None:
+ async def note(self, ctx: Context, user: UnambiguousMemberOrUser, *, reason: str | None = 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)
if infraction is None:
@@ -322,8 +323,8 @@ class Infractions(InfractionScheduler, commands.Cog):
await self.apply_infraction(ctx, infraction, user)
- @command(hidden=True, aliases=['shadowban', 'sban'])
- async def shadow_ban(self, ctx: Context, user: UnambiguousMemberOrUser, *, reason: t.Optional[str] = None) -> None:
+ @command(hidden=True, aliases=["shadowban", "sban"])
+ async def shadow_ban(self, ctx: Context, user: UnambiguousMemberOrUser, *, reason: str | None = None) -> None:
"""Permanently ban a user for the given reason without notifying the user."""
await self.apply_ban(ctx, user, reason, hidden=True)
@@ -338,7 +339,7 @@ class Infractions(InfractionScheduler, commands.Cog):
user: UnambiguousMemberOrUser,
duration: DurationOrExpiry,
*,
- reason: t.Optional[str] = None
+ reason: str | None = None
) -> None:
"""
Temporarily ban a user for the given reason and duration without notifying the user.
@@ -354,7 +355,7 @@ class Infractions(InfractionScheduler, commands.Cog):
\u2003`s` - seconds
Alternatively, an ISO 8601 timestamp can be provided for the duration.
- """
+ """ # noqa: RUF002
await self.apply_ban(ctx, user, reason, duration_or_expiry=duration, hidden=True)
# endregion
@@ -366,7 +367,7 @@ class Infractions(InfractionScheduler, commands.Cog):
ctx: Context,
user: UnambiguousMemberOrUser,
*,
- pardon_reason: t.Optional[str] = None
+ pardon_reason: str | None = None
) -> None:
"""Prematurely end the active timeout infraction for the user."""
await self.pardon_infraction(ctx, "timeout", user, pardon_reason)
@@ -391,7 +392,7 @@ class Infractions(InfractionScheduler, commands.Cog):
ctx: Context,
user: UnambiguousMemberOrUser,
*,
- pardon_reason: t.Optional[str] = None
+ pardon_reason: str | None = None
) -> None:
"""Prematurely end the active voice mute infraction for the user."""
await self.pardon_infraction(ctx, "voice_mute", user, pardon_reason)
@@ -400,11 +401,11 @@ class Infractions(InfractionScheduler, commands.Cog):
# region: Base apply functions
@respect_role_hierarchy(member_arg=2)
- async def apply_timeout(self, ctx: Context, user: Member, reason: t.Optional[str], **kwargs) -> None:
+ async def apply_timeout(self, ctx: Context, user: Member, reason: str | None, **kwargs) -> None:
"""Apply a timeout infraction with kwargs passed to `post_infraction`."""
if isinstance(user, Member) and user.top_role >= ctx.me.top_role:
await ctx.send(":x: I can't timeout users above or equal to me in the role hierarchy.")
- return None
+ return
if active := await _utils.get_active_infraction(ctx, user, "timeout", send_msg=False):
if active["actor"] != self.bot.user.id:
@@ -439,7 +440,7 @@ class Infractions(InfractionScheduler, commands.Cog):
await self.apply_infraction(ctx, infraction, user, action)
@respect_role_hierarchy(member_arg=2)
- async def apply_kick(self, ctx: Context, user: Member, reason: t.Optional[str], **kwargs) -> None:
+ async def apply_kick(self, ctx: Context, user: Member, reason: str | None, **kwargs) -> None:
"""Apply a kick infraction with kwargs passed to `post_infraction`."""
if user.top_role >= ctx.me.top_role:
await ctx.send(":x: I can't kick users above or equal to me in the role hierarchy.")
@@ -464,10 +465,10 @@ class Infractions(InfractionScheduler, commands.Cog):
self,
ctx: Context,
user: MemberOrUser,
- reason: t.Optional[str],
- purge_days: t.Optional[int] = 0,
+ reason: str | None,
+ purge_days: int | None = 0,
**kwargs
- ) -> t.Optional[dict]:
+ ) -> dict | None:
"""
Apply a ban infraction with kwargs passed to `post_infraction`.
@@ -510,8 +511,8 @@ class Infractions(InfractionScheduler, commands.Cog):
await self.apply_infraction(ctx, infraction, user, action)
- bb_cog: t.Optional[BigBrother] = self.bot.get_cog("Big Brother")
- if infraction.get('expires_at') is not None:
+ bb_cog: BigBrother | None = self.bot.get_cog("Big Brother")
+ if infraction.get("expires_at") is not None:
log.trace(f"Ban isn't permanent; user {user} won't be unwatched by Big Brother.")
elif not bb_cog:
log.error(f"Big Brother cog not loaded; perma-banned user {user} won't be unwatched.")
@@ -523,7 +524,7 @@ class Infractions(InfractionScheduler, commands.Cog):
return infraction
@respect_role_hierarchy(member_arg=2)
- async def apply_voice_mute(self, ctx: Context, user: MemberOrUser, reason: t.Optional[str], **kwargs) -> None:
+ async def apply_voice_mute(self, ctx: Context, user: MemberOrUser, reason: str | None, **kwargs) -> None:
"""Apply a voice mute infraction with kwargs passed to `post_infraction`."""
if await _utils.get_active_infraction(ctx, user, "voice_mute"):
return
@@ -554,10 +555,10 @@ class Infractions(InfractionScheduler, commands.Cog):
self,
user_id: int,
guild: discord.Guild,
- reason: t.Optional[str],
+ reason: str | None,
*,
notify: bool = True
- ) -> t.Dict[str, str]:
+ ) -> dict[str, str]:
"""Remove a user's timeout, optionally DM them a notification, and return a log dict."""
user = await get_or_fetch_member(guild, user_id)
log_text = {}
@@ -586,7 +587,7 @@ class Infractions(InfractionScheduler, commands.Cog):
return log_text
- async def pardon_ban(self, user_id: int, guild: discord.Guild, reason: t.Optional[str]) -> t.Dict[str, str]:
+ async def pardon_ban(self, user_id: int, guild: discord.Guild, reason: str | None) -> dict[str, str]:
"""Remove a user's ban on the Discord guild and return a log dict."""
user = discord.Object(user_id)
log_text = {}
@@ -607,7 +608,7 @@ class Infractions(InfractionScheduler, commands.Cog):
guild: discord.Guild,
*,
notify: bool = True
- ) -> t.Dict[str, str]:
+ ) -> dict[str, str]:
"""Optionally DM the user a pardon notification and return a log dict."""
user = await get_or_fetch_member(guild, user_id)
log_text = {}
@@ -629,7 +630,7 @@ class Infractions(InfractionScheduler, commands.Cog):
return log_text
- async def _pardon_action(self, infraction: _utils.Infraction, notify: bool) -> t.Optional[t.Dict[str, str]]:
+ async def _pardon_action(self, infraction: _utils.Infraction, notify: bool) -> dict[str, str] | None:
"""
Execute deactivation steps specific to the infraction's type and return a log dict.
@@ -642,10 +643,11 @@ class Infractions(InfractionScheduler, commands.Cog):
if infraction["type"] == "timeout":
return await self.pardon_timeout(user_id, guild, reason, notify=notify)
- elif infraction["type"] == "ban":
+ if infraction["type"] == "ban":
return await self.pardon_ban(user_id, guild, reason)
- elif infraction["type"] == "voice_mute":
+ if infraction["type"] == "voice_mute":
return await self.pardon_voice_mute(user_id, guild, notify=notify)
+ return None
# endregion
diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py
index 83390d76c..8c51eb3bb 100644
--- a/bot/exts/moderation/infraction/management.py
+++ b/bot/exts/moderation/infraction/management.py
@@ -13,7 +13,7 @@ from bot.bot import Bot
from bot.constants import Categories
from bot.converters import DurationOrExpiry, Infraction, MemberOrUser, Snowflake, UnambiguousUser
from bot.decorators import ensure_future_timestamp
-from bot.errors import InvalidInfraction
+from bot.errors import InvalidInfractionError
from bot.exts.moderation.infraction import _utils
from bot.exts.moderation.infraction.infractions import Infractions
from bot.exts.moderation.modlog import ModLog
@@ -45,7 +45,7 @@ class ModManagement(commands.Cog):
"""Get currently loaded Infractions cog instance."""
return self.bot.get_cog("Infractions")
- @commands.group(name='infraction', aliases=('infr', 'infractions', 'inf', 'i'), invoke_without_command=True)
+ @commands.group(name="infraction", aliases=("infr", "infractions", "inf", "i"), invoke_without_command=True)
async def infraction_group(self, ctx: Context, infraction: Infraction = None) -> None:
"""
Infraction management commands.
@@ -92,7 +92,7 @@ class ModManagement(commands.Cog):
self,
ctx: Context,
infraction: Infraction,
- duration: t.Union[DurationOrExpiry, t.Literal["p", "permanent"], None],
+ duration: DurationOrExpiry | t.Literal["p", "permanent"] | None,
*,
reason: str = None
) -> None:
@@ -117,7 +117,7 @@ class ModManagement(commands.Cog):
If a previous infraction reason does not end with an ending punctuation mark, this automatically
adds a period before the amended reason.
- """
+ """ # noqa: RUF002
old_reason = infraction["reason"]
if old_reason is not None and reason is not None:
@@ -126,13 +126,13 @@ class ModManagement(commands.Cog):
await self.infraction_edit(ctx, infraction, duration, reason=reason)
- @infraction_group.command(name='edit', aliases=('e',))
+ @infraction_group.command(name="edit", aliases=("e",))
@ensure_future_timestamp(timestamp_arg=3)
async def infraction_edit(
self,
ctx: Context,
infraction: Infraction,
- duration: t.Union[DurationOrExpiry, t.Literal["p", "permanent"], None],
+ duration: DurationOrExpiry | t.Literal["p", "permanent"] | None,
*,
reason: str = None
) -> None:
@@ -154,7 +154,7 @@ class ModManagement(commands.Cog):
Use "p" or "permanent" to mark the infraction as permanent. Alternatively, an ISO 8601
timestamp can be provided for the duration.
- """
+ """ # noqa: RUF002
if duration is None and reason is None:
# Unlike UserInputError, the error handler will show a specified message for BadArgument
raise commands.BadArgument("Neither a new expiry nor a new reason was specified.")
@@ -165,27 +165,28 @@ class ModManagement(commands.Cog):
confirm_messages = []
log_text = ""
- if duration is not None and not infraction['active']:
- if (infr_type := infraction['type']) in ('note', 'warning'):
+ if duration is not None and not infraction["active"]:
+ if (infr_type := infraction["type"]) in ("note", "warning"):
await ctx.send(f":x: Cannot edit the expiration of a {infr_type}.")
else:
await ctx.send(":x: Cannot edit the expiration of an expired infraction.")
return
- elif isinstance(duration, str):
- request_data['expires_at'] = None
+
+ if isinstance(duration, str):
+ request_data["expires_at"] = None
confirm_messages.append("marked as permanent")
elif duration is not None:
origin, expiry = unpack_duration(duration)
# Update `last_applied` if expiry changes.
- request_data['last_applied'] = origin.isoformat()
- request_data['expires_at'] = expiry.isoformat()
+ request_data["last_applied"] = origin.isoformat()
+ request_data["expires_at"] = expiry.isoformat()
expiry = time.format_with_duration(expiry, origin)
confirm_messages.append(f"set to expire on {expiry}")
else:
confirm_messages.append("expiry unchanged")
if reason:
- request_data['reason'] = reason
+ request_data["reason"] = reason
confirm_messages.append("set a new reason")
log_text += f"""
Previous reason: {infraction['reason']}
@@ -196,18 +197,18 @@ class ModManagement(commands.Cog):
# Update the infraction
new_infraction = await self.bot.api_client.patch(
- f'bot/infractions/{infraction_id}',
+ f"bot/infractions/{infraction_id}",
json=request_data,
)
# Re-schedule infraction if the expiration has been updated
- if 'expires_at' in request_data:
+ if "expires_at" in request_data:
# A scheduled task should only exist if the old infraction wasn't permanent
- if infraction['expires_at']:
+ if infraction["expires_at"]:
self.infractions_cog.scheduler.cancel(infraction_id)
# If the infraction was not marked as permanent, schedule a new expiration task
- if request_data['expires_at']:
+ if request_data["expires_at"]:
self.infractions_cog.schedule_expiration(new_infraction)
log_text += f"""
@@ -215,11 +216,11 @@ class ModManagement(commands.Cog):
New expiry: {time.until_expiration(new_infraction['expires_at'])}
""".rstrip()
- changes = ' & '.join(confirm_messages)
+ changes = " & ".join(confirm_messages)
await ctx.send(f":ok_hand: Updated infraction #{infraction_id}: {changes}")
# Get information about the infraction's user
- user_id = new_infraction['user']
+ user_id = new_infraction["user"]
user = await get_or_fetch_member(ctx.guild, user_id)
if user:
@@ -254,8 +255,8 @@ class ModManagement(commands.Cog):
# endregion
# region: Search infractions
- @infraction_group.group(name="search", aliases=('s',), invoke_without_command=True)
- async def infraction_search_group(self, ctx: Context, query: t.Union[UnambiguousUser, Snowflake, str]) -> None:
+ @infraction_group.group(name="search", aliases=("s",), invoke_without_command=True)
+ async def infraction_search_group(self, ctx: Context, query: UnambiguousUser | Snowflake | str) -> None:
"""Searches for infractions in the database."""
if isinstance(query, int):
await self.search_user(ctx, discord.Object(query))
@@ -265,14 +266,14 @@ class ModManagement(commands.Cog):
await self.search_user(ctx, query)
@infraction_search_group.command(name="user", aliases=("member", "userid"))
- async def search_user(self, ctx: Context, user: t.Union[MemberOrUser, discord.Object]) -> None:
+ async def search_user(self, ctx: Context, user: MemberOrUser | discord.Object) -> None:
"""Search for infractions by member."""
infraction_list = await self.bot.api_client.get(
- 'bot/infractions/expanded',
- params={'user__id': str(user.id)}
+ "bot/infractions/expanded",
+ params={"user__id": str(user.id)}
)
- if isinstance(user, (discord.Member, discord.User)):
+ if isinstance(user, discord.Member | discord.User):
user_str = escape_markdown(str(user))
else:
if infraction_list:
@@ -297,8 +298,8 @@ class ModManagement(commands.Cog):
raise commands.BadArgument(f"Invalid regular expression in `reason`: {e}")
infraction_list = await self.bot.api_client.get(
- 'bot/infractions/expanded',
- params={'search': reason}
+ "bot/infractions/expanded",
+ params={"search": reason}
)
formatted_infraction_count = self.format_infraction_count(len(infraction_list))
@@ -315,7 +316,7 @@ class ModManagement(commands.Cog):
async def search_by_actor(
self,
ctx: Context,
- actor: t.Union[t.Literal["m", "me"], UnambiguousUser],
+ actor: t.Literal["m", "me"] | UnambiguousUser,
oldest_first: bool = False
) -> None:
"""
@@ -329,15 +330,15 @@ class ModManagement(commands.Cog):
actor = ctx.author
if oldest_first:
- ordering = 'inserted_at' # oldest infractions first
+ ordering = "inserted_at" # oldest infractions first
else:
- ordering = '-inserted_at' # newest infractions first
+ ordering = "-inserted_at" # newest infractions first
infraction_list = await self.bot.api_client.get(
- 'bot/infractions/expanded',
+ "bot/infractions/expanded",
params={
- 'actor__id': str(actor.id),
- 'ordering': ordering
+ "actor__id": str(actor.id),
+ "ordering": ordering
}
)
@@ -368,7 +369,7 @@ class ModManagement(commands.Cog):
self,
ctx: Context,
embed: discord.Embed,
- infractions: t.Iterable[t.Dict[str, t.Any]]
+ infractions: t.Iterable[dict[str, t.Any]]
) -> None:
"""Send a paginated embed of infractions for the specified user."""
if not infractions:
@@ -389,7 +390,7 @@ class ModManagement(commands.Cog):
max_size=1000
)
- def infraction_to_string(self, infraction: t.Dict[str, t.Any]) -> str:
+ def infraction_to_string(self, infraction: dict[str, t.Any]) -> str:
"""Convert the infraction object to a string representation."""
active = infraction["active"]
user = infraction["user"]
@@ -408,7 +409,7 @@ class ModManagement(commands.Cog):
user_str = messages.format_user(user_obj)
else:
# Use the user data retrieved from the DB.
- name = escape_markdown(user['name'])
+ name = escape_markdown(user["name"])
user_str = f"<@{user['id']}> ({name}#{user['discriminator']:04})"
if active:
@@ -476,7 +477,7 @@ class ModManagement(commands.Cog):
await ctx.send(str(error.errors[0]))
error.handled = True
- elif isinstance(error, InvalidInfraction):
+ elif isinstance(error, InvalidInfractionError):
if error.infraction_arg.isdigit():
await ctx.send(f":x: Could not find an infraction with id `{error.infraction_arg}`.")
else:
diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py
index 6cb2c3354..7de4d08b0 100644
--- a/bot/exts/moderation/infraction/superstarify.py
+++ b/bot/exts/moderation/infraction/superstarify.py
@@ -1,7 +1,6 @@
import json
import random
import textwrap
-import typing as t
from pathlib import Path
from discord import Embed, Member
@@ -110,9 +109,9 @@ class Superstarify(InfractionScheduler, Cog):
self,
ctx: Context,
member: Member,
- duration: t.Optional[DurationOrExpiry],
+ duration: DurationOrExpiry | None,
*,
- reason: str = '',
+ reason: str = "",
) -> None:
"""
Temporarily force a random superstar name (like Taylor Swift) to be the user's nickname.
@@ -131,7 +130,7 @@ class Superstarify(InfractionScheduler, Cog):
An optional reason can be provided, which would be added to a message stating their old nickname
and linking to the nickname policy.
- """
+ """ # noqa: RUF002
if member.top_role >= ctx.me.top_role:
await ctx.send(":x: I can't starify users above or equal to me in the role hierarchy.")
return
@@ -144,7 +143,7 @@ class Superstarify(InfractionScheduler, Cog):
# Post the infraction to the API
old_nick = member.display_name
- infraction_reason = f'Old nickname: {old_nick}. {reason}'
+ infraction_reason = f"Old nickname: {old_nick}. {reason}"
infraction = await _utils.post_infraction(ctx, member, "superstar", infraction_reason, duration, active=True)
id_ = infraction["id"]
@@ -177,7 +176,7 @@ class Superstarify(InfractionScheduler, Cog):
successful = await self.apply_infraction(
ctx, infraction, member, action,
- user_reason=user_message(reason=f'**Additional details:** {reason}\n\n' if reason else ''),
+ user_reason=user_message(reason=f"**Additional details:** {reason}\n\n" if reason else ""),
additional_info=nickname_info
)
@@ -187,7 +186,7 @@ class Superstarify(InfractionScheduler, Cog):
embed = Embed(
title="Superstarified!",
colour=constants.Colours.soft_orange,
- description=user_message(reason='')
+ description=user_message(reason="")
)
await ctx.send(embed=embed)
@@ -196,10 +195,10 @@ 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, notify: bool) -> t.Optional[t.Dict[str, str]]:
+ async def _pardon_action(self, infraction: _utils.Infraction, notify: bool) -> dict[str, str] | None:
"""Pardon a superstar infraction, optionally notify the user via DM, and return a log dict."""
if infraction["type"] != "superstar":
- return
+ return None
guild = self.bot.get_guild(constants.Guild.id)
user = await get_or_fetch_member(guild, infraction["user"])
diff --git a/bot/exts/moderation/metabase.py b/bot/exts/moderation/metabase.py
index aeb589b5b..f5483325f 100644
--- a/bot/exts/moderation/metabase.py
+++ b/bot/exts/moderation/metabase.py
@@ -2,7 +2,7 @@ import csv
import json
from datetime import timedelta
from io import StringIO
-from typing import Dict, List, Literal, Optional
+from typing import Literal
import arrow
from aiohttp.client_exceptions import ClientResponseError
@@ -34,11 +34,11 @@ class Metabase(Cog):
self.bot = bot
self._session_scheduler = Scheduler(self.__class__.__name__)
- self.session_token: Optional[str] = None # session_info["session_token"]: str
- self.session_expiry: Optional[float] = None # session_info["session_expiry"]: UtcPosixTimestamp
+ self.session_token: str | None = None # session_info["session_token"]: str
+ self.session_expiry: float | None = None # session_info["session_expiry"]: UtcPosixTimestamp
self.headers = BASE_HEADERS
- self.exports: Dict[int, List[Dict]] = {} # Saves the output of each question, so internal eval can access it
+ self.exports: dict[int, list[dict]] = {} # Saves the output of each question, so internal eval can access it
async def cog_command_error(self, ctx: Context, error: Exception) -> None:
"""Handle ClientResponseError errors locally to invalidate token if needed."""
diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py
index 47a21753c..b349f4d5d 100644
--- a/bot/exts/moderation/modlog.py
+++ b/bot/exts/moderation/modlog.py
@@ -1,8 +1,7 @@
import asyncio
import difflib
import itertools
-import typing as t
-from datetime import datetime, timezone
+from datetime import UTC, datetime
import discord
from dateutil.relativedelta import relativedelta
@@ -20,7 +19,7 @@ from bot.utils.messages import format_user, upload_log
log = get_logger(__name__)
-GUILD_CHANNEL = t.Union[discord.CategoryChannel, discord.TextChannel, discord.VoiceChannel]
+GUILD_CHANNEL = discord.CategoryChannel | discord.TextChannel | discord.VoiceChannel
CHANNEL_CHANGES_UNSUPPORTED = ("permissions",)
CHANNEL_CHANGES_SUPPRESSED = ("_overwrites", "position")
@@ -50,18 +49,18 @@ class ModLog(Cog, name="ModLog"):
async def send_log_message(
self,
- icon_url: t.Optional[str],
- colour: t.Union[discord.Colour, int],
- title: t.Optional[str],
+ icon_url: str | None,
+ colour: discord.Colour | int,
+ title: str | None,
text: str,
- thumbnail: t.Optional[t.Union[str, discord.Asset]] = None,
+ thumbnail: str | discord.Asset | None = None,
channel_id: int = Channels.mod_log,
ping_everyone: bool = False,
- files: t.Optional[t.List[discord.File]] = None,
- content: t.Optional[str] = None,
- additional_embeds: t.Optional[t.List[discord.Embed]] = None,
- timestamp_override: t.Optional[datetime] = None,
- footer: t.Optional[str] = None,
+ files: list[discord.File] | None = None,
+ content: str | None = None,
+ additional_embeds: list[discord.Embed] | None = None,
+ timestamp_override: datetime | None = None,
+ footer: str | None = None,
) -> Context:
"""Generate log embed and send to logging channel."""
await self.bot.wait_until_guild_available()
@@ -74,7 +73,7 @@ class ModLog(Cog, name="ModLog"):
embed.set_author(name=title, icon_url=icon_url)
embed.colour = colour
- embed.timestamp = timestamp_override or datetime.utcnow()
+ embed.timestamp = timestamp_override or datetime.now(tz=UTC)
if footer:
embed.set_footer(text=footer)
@@ -366,7 +365,7 @@ class ModLog(Cog, name="ModLog"):
if member.guild.id != GuildConstant.id:
return
- now = datetime.now(timezone.utc)
+ now = datetime.now(tz=UTC)
difference = abs(relativedelta(now, member.created_at))
message = format_user(member) + "\n\n**Account age:** " + time.humanize_delta(difference)
@@ -416,7 +415,7 @@ class ModLog(Cog, name="ModLog"):
)
@staticmethod
- def get_role_diff(before: t.List[discord.Role], after: t.List[discord.Role]) -> t.List[str]:
+ def get_role_diff(before: list[discord.Role], after: list[discord.Role]) -> list[str]:
"""Return a list of strings describing the roles added and removed."""
changes = []
before_roles = set(before)
@@ -645,16 +644,16 @@ class ModLog(Cog, name="ModLog"):
for diff_type, diff_words in itertools.groupby(diff, key=lambda s: s[0])
)
- content_before: t.List[str] = []
- content_after: t.List[str] = []
+ content_before: list[str] = []
+ content_after: list[str] = []
for index, (diff_type, words) in enumerate(diff_groups):
- sub = ' '.join(words)
- if diff_type == '-':
+ sub = " ".join(words)
+ if diff_type == "-":
content_before.append(f"[{sub}](http://o.hi)")
- elif diff_type == '+':
+ elif diff_type == "+":
content_after.append(f"[{sub}](http://o.hi)")
- elif diff_type == ' ':
+ elif diff_type == " ":
if len(words) > 2:
sub = (
f"{words[0] if index > 0 else ''}"
diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py
index 16423b3d0..ca05f2c25 100644
--- a/bot/exts/moderation/modpings.py
+++ b/bot/exts/moderation/modpings.py
@@ -89,7 +89,7 @@ class ModPings(Cog):
log.info("Scheduling modpings schedule for applicable moderators found in cache.")
for mod_id, schedule in schedule_cache.items():
start_timestamp, work_time = schedule.split("|")
- start = datetime.datetime.fromtimestamp(float(start_timestamp))
+ start = datetime.datetime.fromtimestamp(float(start_timestamp), tz=datetime.UTC)
mod = await self.bot.fetch_user(mod_id)
self._modpings_scheduler.schedule_at(
@@ -134,13 +134,13 @@ class ModPings(Cog):
await mod.add_roles(self.moderators_role, reason="Pings off period expired.")
await self.pings_off_mods.delete(mod.id)
- @group(name='modpings', aliases=('modping',), invoke_without_command=True)
+ @group(name="modpings", aliases=("modping",), invoke_without_command=True)
@has_any_role(*MODERATION_ROLES)
async def modpings_group(self, ctx: Context) -> None:
"""Allow the removal and re-addition of the pingable moderators role."""
await ctx.send_help(ctx.command)
- @modpings_group.command(name='off')
+ @modpings_group.command(name="off")
@has_any_role(*MODERATION_ROLES)
async def off_command(self, ctx: Context, duration: Expiry) -> None:
"""
@@ -159,7 +159,7 @@ class ModPings(Cog):
Alternatively, an ISO 8601 timestamp can be provided for the duration.
The duration cannot be longer than 30 days.
- """
+ """ # noqa: RUF002
delta = duration - arrow.utcnow()
if delta > datetime.timedelta(days=30):
await ctx.send(":x: Cannot remove the role for longer than 30 days.")
@@ -182,7 +182,7 @@ class ModPings(Cog):
f"until {discord_timestamp(duration, format=TimestampFormats.DAY_TIME)}."
)
- @modpings_group.command(name='on')
+ @modpings_group.command(name="on")
@has_any_role(*MODERATION_ROLES)
async def on_command(self, ctx: Context) -> None:
"""Re-apply the pingable moderators role."""
@@ -201,8 +201,8 @@ class ModPings(Cog):
await ctx.send(f"{Emojis.check_mark} Moderators role has been re-applied.")
@modpings_group.group(
- name='schedule',
- aliases=('s',),
+ name="schedule",
+ aliases=("s",),
invoke_without_command=True
)
@has_any_role(*MODERATION_ROLES)
@@ -220,7 +220,7 @@ class ModPings(Cog):
)
return
- if start < datetime.datetime.utcnow():
+ if start < datetime.datetime.now(datetime.UTC):
# The datetime has already gone for the day, so make it tomorrow
# otherwise the scheduler would schedule it immediately
start += datetime.timedelta(days=1)
@@ -244,7 +244,7 @@ class ModPings(Cog):
f"{discord_timestamp(end, TimestampFormats.TIME)}!"
)
- @schedule_modpings.command(name='delete', aliases=('del', 'd'))
+ @schedule_modpings.command(name="delete", aliases=("del", "d"))
async def modpings_schedule_delete(self, ctx: Context) -> None:
"""Delete your modpings schedule."""
self._modpings_scheduler.cancel(ctx.author.id)
diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py
index 682791593..2852598ca 100644
--- a/bot/exts/moderation/silence.py
+++ b/bot/exts/moderation/silence.py
@@ -1,8 +1,7 @@
import json
-import typing
+from collections import OrderedDict
from contextlib import suppress
-from datetime import datetime, timedelta, timezone
-from typing import Optional, OrderedDict, Union
+from datetime import UTC, datetime, timedelta
from async_rediscache import RedisCache
from discord import Guild, PermissionOverwrite, TextChannel, Thread, VoiceChannel
@@ -33,7 +32,7 @@ MSG_UNSILENCE_MANUAL = (
)
MSG_UNSILENCE_SUCCESS = f"{constants.Emojis.check_mark} unsilenced {{channel}}."
-TextOrVoiceChannel = Union[TextChannel, VoiceChannel]
+TextOrVoiceChannel = TextChannel | VoiceChannel
VOICE_CHANNELS = {
constants.Channels.code_help_voice_0: constants.Channels.code_help_chat_0,
@@ -83,7 +82,7 @@ class SilenceNotifier(tasks.Loop):
f"Sending notice with channels: "
f"{', '.join(f'#{channel} ({channel.id})' for channel in self._silenced_channels)}."
)
- channels_text = ', '.join(
+ channels_text = ", ".join(
f"{channel.mention} for {(self._current_loop-start)//60} min"
for channel, start in self._silenced_channels.items()
)
@@ -159,7 +158,7 @@ class Silence(commands.Cog):
async def silence(
self,
ctx: Context,
- duration_or_channel: typing.Union[TextOrVoiceChannel, HushDurationConverter] = None,
+ duration_or_channel: TextOrVoiceChannel | HushDurationConverter = None,
duration: HushDurationConverter = 10,
*,
kick: bool = False
@@ -210,12 +209,12 @@ class Silence(commands.Cog):
@staticmethod
def parse_silence_args(
ctx: Context,
- duration_or_channel: typing.Union[TextOrVoiceChannel, int],
+ duration_or_channel: TextOrVoiceChannel | int,
duration: HushDurationConverter
- ) -> typing.Tuple[TextOrVoiceChannel, Optional[int]]:
+ ) -> tuple[TextOrVoiceChannel, int | None]:
"""Helper method to parse the arguments of the silence command."""
if duration_or_channel:
- if isinstance(duration_or_channel, (TextChannel, VoiceChannel)):
+ if isinstance(duration_or_channel, TextChannel | VoiceChannel):
channel = duration_or_channel
else:
channel = ctx.channel
@@ -260,13 +259,13 @@ class Silence(commands.Cog):
return True
- async def _schedule_unsilence(self, ctx: Context, channel: TextOrVoiceChannel, duration: Optional[int]) -> None:
+ async def _schedule_unsilence(self, ctx: Context, channel: TextOrVoiceChannel, duration: int | None) -> None:
"""Schedule `ctx.channel` to be unsilenced if `duration` is not None."""
if duration is None:
await self.unsilence_timestamps.set(channel.id, -1)
else:
self.scheduler.schedule_later(duration * 60, channel.id, ctx.invoke(self.unsilence, channel=channel))
- unsilence_time = datetime.now(tz=timezone.utc) + timedelta(minutes=duration)
+ unsilence_time = datetime.now(tz=UTC) + timedelta(minutes=duration)
await self.unsilence_timestamps.set(channel.id, unsilence_time.timestamp())
@commands.command(aliases=("unhush",))
@@ -282,7 +281,7 @@ class Silence(commands.Cog):
await self._unsilence_wrapper(channel, ctx)
@lock_arg(LOCK_NAMESPACE, "channel", raise_error=True)
- async def _unsilence_wrapper(self, channel: TextOrVoiceChannel, ctx: Optional[Context] = None) -> None:
+ async def _unsilence_wrapper(self, channel: TextOrVoiceChannel, ctx: Context | None = None) -> None:
"""
Unsilence `channel` and send a success/failure message to ctx.channel.
@@ -451,8 +450,8 @@ class Silence(commands.Cog):
self.notifier.add_channel(channel)
continue
- dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
- delta = (dt - datetime.now(tz=timezone.utc)).total_seconds()
+ dt = datetime.fromtimestamp(timestamp, tz=UTC)
+ delta = (dt - datetime.now(tz=UTC)).total_seconds()
if delta <= 0:
# Suppress the error since it's not being invoked by a user via the command.
with suppress(LockedResourceError):
diff --git a/bot/exts/moderation/slowmode.py b/bot/exts/moderation/slowmode.py
index c43ae8b0c..3ddb666f3 100644
--- a/bot/exts/moderation/slowmode.py
+++ b/bot/exts/moderation/slowmode.py
@@ -1,4 +1,4 @@
-from typing import Literal, Optional, Union
+from typing import Literal, Optional
from dateutil.relativedelta import relativedelta
from discord import TextChannel, Thread
@@ -20,7 +20,7 @@ COMMONLY_SLOWMODED_CHANNELS = {
Channels.off_topic_0: "ot0",
}
-MessageHolder = Optional[Union[TextChannel, Thread]]
+MessageHolder = Optional[TextChannel | Thread]
class Slowmode(Cog):
@@ -29,12 +29,12 @@ class Slowmode(Cog):
def __init__(self, bot: Bot) -> None:
self.bot = bot
- @group(name='slowmode', aliases=['sm'], invoke_without_command=True)
+ @group(name="slowmode", aliases=["sm"], invoke_without_command=True)
async def slowmode_group(self, ctx: Context) -> None:
"""Get or set the slowmode delay for the text channel this was invoked in or a given text channel."""
await ctx.send_help(ctx.command)
- @slowmode_group.command(name='get', aliases=['g'])
+ @slowmode_group.command(name="get", aliases=["g"])
async def get_slowmode(self, ctx: Context, channel: MessageHolder) -> None:
"""Get the slowmode delay for a text channel."""
# Use the channel this command was invoked in if one was not given
@@ -43,14 +43,14 @@ class Slowmode(Cog):
humanized_delay = time.humanize_delta(seconds=channel.slowmode_delay)
- await ctx.send(f'The slowmode delay for {channel.mention} is {humanized_delay}.')
+ await ctx.send(f"The slowmode delay for {channel.mention} is {humanized_delay}.")
- @slowmode_group.command(name='set', aliases=['s'])
+ @slowmode_group.command(name="set", aliases=["s"])
async def set_slowmode(
self,
ctx: Context,
channel: MessageHolder,
- delay: Union[DurationDelta, Literal["0s", "0seconds"]],
+ delay: DurationDelta | Literal["0s", "0seconds"],
) -> None:
"""Set the slowmode delay for a text channel."""
# Use the channel this command was invoked in if one was not given
@@ -67,28 +67,28 @@ class Slowmode(Cog):
# Ensure the delay is within discord's limits
if slowmode_delay <= SLOWMODE_MAX_DELAY:
- log.info(f'{ctx.author} set the slowmode delay for #{channel} to {humanized_delay}.')
+ log.info(f"{ctx.author} set the slowmode delay for #{channel} to {humanized_delay}.")
await channel.edit(slowmode_delay=slowmode_delay)
if channel.id in COMMONLY_SLOWMODED_CHANNELS:
- log.info(f'Recording slowmode change in stats for {channel.name}.')
+ log.info(f"Recording slowmode change in stats for {channel.name}.")
self.bot.stats.gauge(f"slowmode.{COMMONLY_SLOWMODED_CHANNELS[channel.id]}", slowmode_delay)
await ctx.send(
- f'{Emojis.check_mark} The slowmode delay for {channel.mention} is now {humanized_delay}.'
+ f"{Emojis.check_mark} The slowmode delay for {channel.mention} is now {humanized_delay}."
)
else:
log.info(
- f'{ctx.author} tried to set the slowmode delay of #{channel} to {humanized_delay}, '
- 'which is not between 0 and 6 hours.'
+ f"{ctx.author} tried to set the slowmode delay of #{channel} to {humanized_delay}, "
+ "which is not between 0 and 6 hours."
)
await ctx.send(
- f'{Emojis.cross_mark} The slowmode delay must be between 0 and 6 hours.'
+ f"{Emojis.cross_mark} The slowmode delay must be between 0 and 6 hours."
)
- @slowmode_group.command(name='reset', aliases=['r'])
+ @slowmode_group.command(name="reset", aliases=["r"])
async def reset_slowmode(self, ctx: Context, channel: MessageHolder) -> None:
"""Reset the slowmode delay for a text channel to 0 seconds."""
await self.set_slowmode(ctx, channel, relativedelta(seconds=0))
diff --git a/bot/exts/moderation/stream.py b/bot/exts/moderation/stream.py
index f0d8c23b8..cb03c00e1 100644
--- a/bot/exts/moderation/stream.py
+++ b/bot/exts/moderation/stream.py
@@ -1,4 +1,4 @@
-from datetime import timedelta, timezone
+from datetime import UTC, timedelta
from operator import itemgetter
import arrow
@@ -100,7 +100,7 @@ class Stream(commands.Cog):
\u2003`s` - seconds
Alternatively, an ISO 8601 timestamp can be provided for the duration.
- """
+ """ # noqa: RUF002
log.trace(f"Attempting to give temporary streaming permission to {member} ({member.id}).")
if duration is None:
@@ -110,7 +110,7 @@ class Stream(commands.Cog):
elif duration.tzinfo is None:
# Make duration tz-aware.
# ISODateTime could already include tzinfo, this check is so it isn't overwritten.
- duration.replace(tzinfo=timezone.utc)
+ duration.replace(tzinfo=UTC)
# Check if the member already has streaming permission
already_allowed = any(Roles.video == role.id for role in member.roles)
@@ -185,7 +185,7 @@ class Stream(commands.Cog):
await self._suspend_stream(ctx, member)
- @commands.command(aliases=('lstream',))
+ @commands.command(aliases=("lstream",))
@commands.has_any_role(*MODERATION_ROLES)
async def liststream(self, ctx: commands.Context) -> None:
"""Lists all users who aren't staff, partners or members of the python community and have stream permissions."""
diff --git a/bot/exts/moderation/verification.py b/bot/exts/moderation/verification.py
index 306c27e06..d9308b177 100644
--- a/bot/exts/moderation/verification.py
+++ b/bot/exts/moderation/verification.py
@@ -106,23 +106,23 @@ class Verification(Cog):
# endregion
# region: miscellaneous
- @command(name='verify')
+ @command(name="verify")
@has_any_role(*constants.MODERATION_ROLES)
async def perform_manual_verification(self, ctx: Context, user: discord.Member) -> None:
"""Command for moderators to verify any user."""
- log.trace(f'verify command called by {ctx.author} for {user.id}.')
+ log.trace(f"verify command called by {ctx.author} for {user.id}.")
if not user.pending:
- log.trace(f'{user.id} is already verified, aborting.')
- await ctx.send(f'{constants.Emojis.cross_mark} {user.mention} is already verified.')
+ log.trace(f"{user.id} is already verified, aborting.")
+ await ctx.send(f"{constants.Emojis.cross_mark} {user.mention} is already verified.")
return
# Adding a role automatically verifies the user, so we add and remove the Announcements role.
temporary_role = self.bot.get_guild(constants.Guild.id).get_role(constants.Roles.announcements)
await user.add_roles(temporary_role)
await user.remove_roles(temporary_role)
- log.trace(f'{user.id} manually verified.')
- await ctx.send(f'{constants.Emojis.check_mark} {user.mention} is now verified.')
+ log.trace(f"{user.id} manually verified.")
+ await ctx.send(f"{constants.Emojis.check_mark} {user.mention} is now verified.")
# endregion
diff --git a/bot/exts/moderation/voice_gate.py b/bot/exts/moderation/voice_gate.py
index 1901d1c57..50d3188bd 100644
--- a/bot/exts/moderation/voice_gate.py
+++ b/bot/exts/moderation/voice_gate.py
@@ -51,7 +51,7 @@ VOICE_PING_DM = (
class VoiceGate(Cog):
"""Voice channels verification management."""
- # RedisCache[t.Union[discord.User.id, discord.Member.id], t.Union[discord.Message.id, int]]
+ # RedisCache[discord.User.id | discord.Member.id, discord.Message.id | int]
# The cache's keys are the IDs of members who are verified or have joined a voice channel
# The cache's values are either the message ID of the ping message or 0 (NO_MSG) if no message is present
redis_cache = RedisCache()
@@ -181,7 +181,7 @@ class VoiceGate(Cog):
if failed:
embed = discord.Embed(
title="Voice Gate failed",
- description=FAILED_MESSAGE.format(reasons="\n".join(f'• You {reason}.' for reason in failed_reasons)),
+ description=FAILED_MESSAGE.format(reasons="\n".join(f"• You {reason}." for reason in failed_reasons)),
color=Colour.red()
)
try:
diff --git a/bot/exts/moderation/watchchannels/_watchchannel.py b/bot/exts/moderation/watchchannels/_watchchannel.py
index 7566021c5..c3f823982 100644
--- a/bot/exts/moderation/watchchannels/_watchchannel.py
+++ b/bot/exts/moderation/watchchannels/_watchchannel.py
@@ -4,7 +4,7 @@ import textwrap
from abc import abstractmethod
from collections import defaultdict, deque
from dataclasses import dataclass
-from typing import Any, Dict, Optional
+from typing import Any
import discord
from discord import Color, DMChannel, Embed, HTTPException, Message, errors
@@ -31,8 +31,8 @@ URL_RE = re.compile(r"(https?://[^\s]+)")
class MessageHistory:
"""Represents a watch channel's message history."""
- last_author: Optional[int] = None
- last_channel: Optional[int] = None
+ last_author: int | None = None
+ last_channel: int | None = None
message_count: int = 0
@@ -158,7 +158,7 @@ class WatchChannel(metaclass=CogABCMeta):
self.watched_users = defaultdict(dict)
for entry in data:
- user_id = entry.pop('user')
+ user_id = entry.pop("user")
self.watched_users[user_id] = entry
return True
@@ -207,10 +207,10 @@ class WatchChannel(metaclass=CogABCMeta):
async def webhook_send(
self,
- content: Optional[str] = None,
- username: Optional[str] = None,
- avatar_url: Optional[str] = None,
- embed: Optional[Embed] = None,
+ content: str | None = None,
+ username: str | None = None,
+ avatar_url: str | None = None,
+ embed: Embed | None = None,
) -> None:
"""Sends a message to the webhook with the specified kwargs."""
username = messages.sub_clyde(username)
@@ -280,13 +280,13 @@ class WatchChannel(metaclass=CogABCMeta):
user_id = msg.author.id
guild = self.bot.get_guild(GuildConfig.id)
- actor = await get_or_fetch_member(guild, self.watched_users[user_id]['actor'])
- actor = actor.display_name if actor else self.watched_users[user_id]['actor']
+ actor = await get_or_fetch_member(guild, self.watched_users[user_id]["actor"])
+ actor = actor.display_name if actor else self.watched_users[user_id]["actor"]
- inserted_at = self.watched_users[user_id]['inserted_at']
+ inserted_at = self.watched_users[user_id]["inserted_at"]
time_delta = time.format_relative(inserted_at)
- reason = self.watched_users[user_id]['reason']
+ reason = self.watched_users[user_id]["reason"]
if isinstance(msg.channel, DMChannel):
# If a watched user DMs the bot there won't be a channel name or jump URL
@@ -326,7 +326,7 @@ class WatchChannel(metaclass=CogABCMeta):
async def prepare_watched_users_data(
self, ctx: Context, oldest_first: bool = False, update_cache: bool = True
- ) -> Dict[str, Any]:
+ ) -> dict[str, Any]:
"""
Prepare overview information of watched users to list.
@@ -357,7 +357,7 @@ class WatchChannel(metaclass=CogABCMeta):
line = f"• `{user_id}`"
if member:
line += f" ({member.name}#{member.discriminator})"
- inserted_at = user_data['inserted_at']
+ inserted_at = user_data["inserted_at"]
line += f", added {time.format_relative(inserted_at)}"
if not member: # Cross off users who left the server.
line = f"~~{line}~~"
diff --git a/bot/exts/moderation/watchchannels/bigbrother.py b/bot/exts/moderation/watchchannels/bigbrother.py
index fe652cc5b..7af8c7152 100644
--- a/bot/exts/moderation/watchchannels/bigbrother.py
+++ b/bot/exts/moderation/watchchannels/bigbrother.py
@@ -21,18 +21,18 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"):
bot,
destination=Channels.big_brother,
webhook_id=Webhooks.big_brother.id,
- api_endpoint='bot/infractions',
- api_default_params={'active': 'true', 'type': 'watch', 'ordering': '-inserted_at', 'limit': 10_000},
+ api_endpoint="bot/infractions",
+ api_default_params={"active": "true", "type": "watch", "ordering": "-inserted_at", "limit": 10_000},
logger=log
)
- @group(name='bigbrother', aliases=('bb',), invoke_without_command=True)
+ @group(name="bigbrother", aliases=("bb",), invoke_without_command=True)
@has_any_role(*MODERATION_ROLES)
async def bigbrother_group(self, ctx: Context) -> None:
"""Monitors users by relaying their messages to the Big Brother watch channel."""
await ctx.send_help(ctx.command)
- @bigbrother_group.command(name='watched', aliases=('all', 'list'))
+ @bigbrother_group.command(name="watched", aliases=("all", "list"))
@has_any_role(*MODERATION_ROLES)
async def watched_command(
self, ctx: Context, oldest_first: bool = False, update_cache: bool = True
@@ -47,7 +47,7 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"):
"""
await self.list_watched_users(ctx, oldest_first=oldest_first, update_cache=update_cache)
- @bigbrother_group.command(name='oldest')
+ @bigbrother_group.command(name="oldest")
@has_any_role(*MODERATION_ROLES)
async def oldest_command(self, ctx: Context, update_cache: bool = True) -> None:
"""
@@ -58,7 +58,7 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"):
"""
await ctx.invoke(self.watched_command, oldest_first=True, update_cache=update_cache)
- @bigbrother_group.command(name='watch', aliases=('w',), root_aliases=('watch',))
+ @bigbrother_group.command(name="watch", aliases=("w",), root_aliases=("watch",))
@has_any_role(*MODERATION_ROLES)
async def watch_command(self, ctx: Context, user: MemberOrUser, *, reason: str) -> None:
"""
@@ -69,7 +69,7 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"):
"""
await self.apply_watch(ctx, user, reason)
- @bigbrother_group.command(name='unwatch', aliases=('uw',), root_aliases=('unwatch',))
+ @bigbrother_group.command(name="unwatch", aliases=("uw",), root_aliases=("unwatch",))
@has_any_role(*MODERATION_ROLES)
async def unwatch_command(self, ctx: Context, user: MemberOrUser, *, reason: str) -> None:
"""Stop relaying messages by the given `user`."""
@@ -99,7 +99,7 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"):
await ctx.send(f":x: I'm sorry {ctx.author}, I'm afraid I can't do that. I must be kind to my masters.")
return
- response = await post_infraction(ctx, user, 'watch', reason, hidden=True, active=True)
+ response = await post_infraction(ctx, user, "watch", reason, hidden=True, active=True)
if response is not None:
self.watched_users[user.id] = response
@@ -110,8 +110,8 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"):
params={
"user__id": str(user.id),
"active": "false",
- 'type': 'watch',
- 'ordering': '-inserted_at'
+ "type": "watch",
+ "ordering": "-inserted_at"
}
)
@@ -145,10 +145,10 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"):
await self.bot.api_client.patch(
f"{self.api_endpoint}/{infraction['id']}",
- json={'active': False}
+ json={"active": False}
)
- await post_infraction(ctx, user, 'watch', f"Unwatched: {reason}", hidden=True, active=False)
+ await post_infraction(ctx, user, "watch", f"Unwatched: {reason}", hidden=True, active=False)
self._remove_user(user.id)
diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py
index f3716140a..452ae595d 100644
--- a/bot/exts/recruitment/talentpool/_cog.py
+++ b/bot/exts/recruitment/talentpool/_cog.py
@@ -1,8 +1,7 @@
import asyncio
import textwrap
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from io import StringIO
-from typing import Optional, Union
import discord
from async_rediscache import RedisCache
@@ -57,7 +56,7 @@ class TalentPool(Cog, name="Talentpool"):
"""Return whether automatic posting of nomination reviews is enabled."""
return await self.talentpool_settings.get(AUTOREVIEW_ENABLED_KEY, True)
- @group(name='talentpool', aliases=('tp', 'talent', 'nomination', 'n'), invoke_without_command=True)
+ @group(name="talentpool", aliases=("tp", "talent", "nomination", "n"), invoke_without_command=True)
@has_any_role(*STAFF_ROLES)
async def nomination_group(self, ctx: Context) -> None:
"""Highlights the activity of helper nominees by relaying their messages to the talent pool channel."""
@@ -195,7 +194,7 @@ class TalentPool(Cog, name="Talentpool"):
"""Shows the users that are currently in the talent pool, ordered by oldest nomination."""
await self.show_nominations_list(ctx, oldest_first=True)
- @list_group.command(name='newest')
+ @list_group.command(name="newest")
async def list_newest(self, ctx: Context) -> None:
"""Shows the users that are currently in the talent pool, ordered by newest nomination."""
await self.show_nominations_list(ctx, oldest_first=False)
@@ -216,7 +215,7 @@ class TalentPool(Cog, name="Talentpool"):
Otherwise, nominations will be sorted by age
(ordered based on the value of `oldest_first`).
"""
- now = datetime.now(tz=timezone.utc)
+ now = datetime.now(tz=UTC)
nominations = await self.api.get_nominations(active=True)
messages_per_user = await self.api.get_activity(
[nomination.user_id for nomination in nominations],
@@ -306,7 +305,7 @@ class TalentPool(Cog, name="Talentpool"):
root_aliases=("forcenominate",)
)
@has_any_role(*MODERATION_ROLES)
- async def force_nominate_command(self, ctx: Context, user: MemberOrUser, *, reason: str = '') -> None:
+ async def force_nominate_command(self, ctx: Context, user: MemberOrUser, *, reason: str = "") -> None:
"""
Adds the given `user` to the talent pool, from any channel.
@@ -314,9 +313,9 @@ class TalentPool(Cog, name="Talentpool"):
"""
await self._nominate_user(ctx, user, reason)
- @nomination_group.command(name='nominate', aliases=("w", "add", "a", "watch"), root_aliases=("nominate",))
+ @nomination_group.command(name="nominate", aliases=("w", "add", "a", "watch"), root_aliases=("nominate",))
@has_any_role(*STAFF_ROLES)
- async def nominate_command(self, ctx: Context, user: MemberOrUser, *, reason: str = '') -> None:
+ async def nominate_command(self, ctx: Context, user: MemberOrUser, *, reason: str = "") -> None:
"""
Adds the given `user` to the talent pool.
@@ -363,7 +362,7 @@ class TalentPool(Cog, name="Talentpool"):
await ctx.send(f"✅ The nomination for {user.mention} has been added to the talent pool.")
- @nomination_group.command(name='history', aliases=('info', 'search'))
+ @nomination_group.command(name="history", aliases=("info", "search"))
@has_any_role(*MODERATION_ROLES)
async def history_command(self, ctx: Context, user: MemberOrUser) -> None:
"""Shows the specified user's nomination history."""
@@ -404,19 +403,19 @@ class TalentPool(Cog, name="Talentpool"):
else:
await ctx.send(f":x: {user.mention} doesn't have an active nomination.")
- @nomination_group.group(name='edit', aliases=('e',), invoke_without_command=True)
+ @nomination_group.group(name="edit", aliases=("e",), invoke_without_command=True)
@has_any_role(*STAFF_ROLES)
async def nomination_edit_group(self, ctx: Context) -> None:
"""Commands to edit nominations."""
await ctx.send_help(ctx.command)
- @nomination_edit_group.command(name='reason')
+ @nomination_edit_group.command(name="reason")
@has_any_role(*STAFF_ROLES)
async def edit_reason_command(
self,
ctx: Context,
- nominee_or_nomination_id: Union[UnambiguousMemberOrUser, int],
- nominator: Optional[UnambiguousMemberOrUser] = None,
+ nominee_or_nomination_id: UnambiguousMemberOrUser | int,
+ nominator: UnambiguousMemberOrUser | None = None,
*,
reason: str
) -> None:
@@ -458,7 +457,7 @@ class TalentPool(Cog, name="Talentpool"):
self,
ctx: Context,
*,
- target: Union[int, Member, User],
+ target: int | Member | User,
actor: MemberOrUser,
reason: str,
) -> None:
@@ -493,7 +492,7 @@ class TalentPool(Cog, name="Talentpool"):
await ctx.send(f":white_check_mark: Updated the nomination reason for <@{nomination.user_id}>.")
- @nomination_edit_group.command(name='end_reason')
+ @nomination_edit_group.command(name="end_reason")
@has_any_role(*MODERATION_ROLES)
async def edit_end_reason_command(self, ctx: Context, nomination_id: int, *, reason: str) -> None:
"""Edits the unnominate reason for the nomination with the given `id`."""
@@ -516,7 +515,7 @@ class TalentPool(Cog, name="Talentpool"):
await ctx.send(f":white_check_mark: Updated the nomination end reason for <@{nomination.user_id}>.")
- @nomination_group.command(aliases=('gr',))
+ @nomination_group.command(aliases=("gr",))
@has_any_role(*MODERATION_ROLES)
async def get_review(self, ctx: Context, user_id: int) -> None:
"""Get the user's review as a markdown file."""
@@ -529,7 +528,7 @@ class TalentPool(Cog, name="Talentpool"):
file = discord.File(StringIO(review), f"{user_id}_review.md")
await ctx.send(file=file)
- @nomination_group.command(aliases=('review',))
+ @nomination_group.command(aliases=("review",))
@has_any_role(*MODERATION_ROLES)
async def post_review(self, ctx: Context, user_id: int) -> None:
"""Post the automatic review for the user ahead of time."""
@@ -618,7 +617,7 @@ class TalentPool(Cog, name="Talentpool"):
except discord.HTTPException:
thread_jump_url = "*Not found*"
else:
- thread_jump_url = f'[Jump to thread!]({thread.jump_url})'
+ thread_jump_url = f"[Jump to thread!]({thread.jump_url})"
if nomination.active:
lines = textwrap.dedent(
diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py
index f46e565ed..be9456c37 100644
--- a/bot/exts/recruitment/talentpool/_review.py
+++ b/bot/exts/recruitment/talentpool/_review.py
@@ -5,8 +5,7 @@ import re
import textwrap
import typing
from collections import Counter
-from datetime import datetime, timedelta, timezone
-from typing import List, Optional, Union
+from datetime import UTC, datetime, timedelta
import discord
from async_rediscache import RedisCache
@@ -92,8 +91,8 @@ class Reviewer:
last_vote_timestamp = await self.status_cache.get("last_vote_date")
if last_vote_timestamp:
- last_vote_date = datetime.fromtimestamp(last_vote_timestamp, tz=timezone.utc)
- time_since_last_vote = datetime.now(timezone.utc) - last_vote_date
+ last_vote_date = datetime.fromtimestamp(last_vote_timestamp, tz=UTC)
+ time_since_last_vote = datetime.now(UTC) - last_vote_date
if time_since_last_vote < MIN_REVIEW_INTERVAL:
log.debug("Most recent review was less than %s ago, cancelling check", MIN_REVIEW_INTERVAL)
@@ -193,7 +192,7 @@ class Reviewer:
See `is_ready_for_review` for the criteria for a user to be ready for review.
See `sort_nominations_to_review` for the criteria for a user to be prioritised for review.
"""
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
nominations = await self.api.get_nominations(active=True)
if not nominations:
return None
@@ -237,7 +236,7 @@ class Reviewer:
)
message = await thread.send(f"<@&{Roles.mod_team}> <@&{Roles.admins}>")
- now = datetime.now(tz=timezone.utc)
+ now = datetime.now(tz=UTC)
await self.status_cache.set("last_vote_date", now.timestamp())
await self.api.edit_nomination(nomination.id, reviewed=True, thread_id=thread.id)
@@ -247,7 +246,7 @@ class Reviewer:
context = await self.bot.get_context(message)
await bump_cog.add_thread_to_bump_list(context, thread)
- async def make_review(self, nomination: Nomination) -> typing.Tuple[str, Optional[Emoji], Optional[Member]]:
+ async def make_review(self, nomination: Nomination) -> tuple[str, Emoji | None, Member | None]:
"""Format a generic review of a user and return it with the reviewed emoji and the user themselves."""
log.trace(f"Formatting the review of {nomination.user_id}")
@@ -326,7 +325,7 @@ class Reviewer:
result = f"**Passed** {Emojis.incident_actioned}" if passed else f"**Rejected** {Emojis.incident_unactioned}"
colour = Colours.soft_green if passed else Colours.soft_red
- timestamp = datetime.utcnow().strftime("%Y/%m/%d")
+ timestamp = datetime.now(tz=UTC).strftime("%Y/%m/%d")
embed_content = (
f"{result} on {timestamp}\n"
@@ -424,8 +423,8 @@ class Reviewer:
"""
log.trace(f"Fetching the infraction data for {member.id}'s review")
infraction_list = await self.bot.api_client.get(
- 'bot/infractions/expanded',
- params={'user__id': str(member.id), 'ordering': '-inserted_at'}
+ "bot/infractions/expanded",
+ params={"user__id": str(member.id), "ordering": "-inserted_at"}
)
log.trace(f"{len(infraction_list)} infractions found for {member.id}, formatting review.")
@@ -456,7 +455,7 @@ class Reviewer:
infractions += ", with the last infraction issued "
# Infractions were ordered by time since insertion descending.
- infractions += time.format_relative(infraction_list[0]['inserted_at'])
+ infractions += time.format_relative(infraction_list[0]["inserted_at"])
return f"They have {infractions}."
@@ -470,13 +469,13 @@ class Reviewer:
"""
formatted = infr_type.replace("_", " ")
if count > 1:
- if infr_type.endswith(('ch', 'sh')):
+ if infr_type.endswith(("ch", "sh")):
formatted += "e"
formatted += "s"
return formatted
- async def _previous_nominations_review(self, member: Member) -> Optional[str]:
+ async def _previous_nominations_review(self, member: Member) -> str | None:
"""
Formats the review of the nominee's previous nominations.
@@ -487,7 +486,7 @@ class Reviewer:
log.trace(f"{len(history)} previous nominations found for {member.id}, formatting review.")
if not history:
- return
+ return None
num_entries = sum(len(nomination.entries) for nomination in history)
@@ -523,7 +522,7 @@ class Reviewer:
return review
@staticmethod
- def _random_ducky(guild: Guild) -> Union[Emoji, str]:
+ def _random_ducky(guild: Guild) -> Emoji | str:
"""Picks a random ducky emoji. If no duckies found returns 👀."""
duckies = [emoji for emoji in guild.emojis if emoji.name.startswith("ducky")]
if not duckies:
@@ -531,7 +530,7 @@ class Reviewer:
return random.choice(duckies)
@staticmethod
- async def _bulk_send(channel: TextChannel, text: str) -> List[Message]:
+ async def _bulk_send(channel: TextChannel, text: str) -> list[Message]:
"""
Split a text into several if necessary, and post them to the channel.
diff --git a/bot/exts/utils/bot.py b/bot/exts/utils/bot.py
index a312e0584..4b24ef242 100644
--- a/bot/exts/utils/bot.py
+++ b/bot/exts/utils/bot.py
@@ -1,4 +1,3 @@
-from typing import Optional
from discord import Embed, TextChannel
from discord.ext.commands import Cog, Context, command, group, has_any_role
@@ -21,7 +20,7 @@ class BotCog(Cog, name="Bot"):
"""Bot informational commands."""
await ctx.send_help(ctx.command)
- @botinfo_group.command(name='about', aliases=('info',), hidden=True)
+ @botinfo_group.command(name="about", aliases=("info",), hidden=True)
async def about_command(self, ctx: Context) -> None:
"""Get information about the bot."""
embed = Embed(
@@ -41,9 +40,9 @@ class BotCog(Cog, name="Bot"):
await ctx.send(embed=embed)
- @command(name='echo', aliases=('print',))
+ @command(name="echo", aliases=("print",))
@has_any_role(*MODERATION_ROLES)
- async def echo_command(self, ctx: Context, channel: Optional[TextChannel], *, text: str) -> None:
+ async def echo_command(self, ctx: Context, channel: TextChannel | None, *, text: str) -> None:
"""Repeat the given message in either a specified channel or the current channel."""
if channel is None:
await ctx.send(text)
@@ -52,9 +51,9 @@ class BotCog(Cog, name="Bot"):
else:
await channel.send(text)
- @command(name='embed')
+ @command(name="embed")
@has_any_role(*MODERATION_ROLES)
- async def embed_command(self, ctx: Context, channel: Optional[TextChannel], *, text: str) -> None:
+ async def embed_command(self, ctx: Context, channel: TextChannel | None, *, text: str) -> None:
"""Send the input within an embed to either a specified channel or the current channel."""
embed = Embed(description=text)
diff --git a/bot/exts/utils/extensions.py b/bot/exts/utils/extensions.py
index 90249867f..57c62bfee 100644
--- a/bot/exts/utils/extensions.py
+++ b/bot/exts/utils/extensions.py
@@ -47,7 +47,7 @@ class Extensions(commands.Cog):
Load extensions given their fully qualified or unqualified names.
If '\*' or '\*\*' is given as the name, all unloaded extensions will be loaded.
- """ # noqa: W605
+ """
if not extensions:
await ctx.send_help(ctx.command)
return
@@ -63,7 +63,7 @@ class Extensions(commands.Cog):
Unload currently loaded extensions given their fully qualified or unqualified names.
If '\*' or '\*\*' is given as the name, all loaded extensions will be unloaded.
- """ # noqa: W605
+ """
if not extensions:
await ctx.send_help(ctx.command)
return
@@ -87,7 +87,7 @@ class Extensions(commands.Cog):
If '\*' is given as the name, all currently loaded extensions will be reloaded.
If '\*\*' is given as the name, all extensions, including unloaded ones, will be reloaded.
- """ # noqa: W605
+ """
if not extensions:
await ctx.send_help(ctx.command)
return
@@ -187,7 +187,7 @@ class Extensions(commands.Cog):
await loading_message.edit(content=msg)
self.action_in_progress = False
- async def manage(self, action: Action, ext: str) -> t.Tuple[str, t.Optional[str]]:
+ async def manage(self, action: Action, ext: str) -> tuple[str, str | None]:
"""Apply an action to an extension and return the status message and any error message."""
verb = action.name.lower()
error_msg = None
diff --git a/bot/exts/utils/internal.py b/bot/exts/utils/internal.py
index 3125cee75..cdaa9a61b 100644
--- a/bot/exts/utils/internal.py
+++ b/bot/exts/utils/internal.py
@@ -6,7 +6,7 @@ import textwrap
import traceback
from collections import Counter
from io import StringIO
-from typing import Any, Optional, Tuple
+from typing import Any
import arrow
import discord
@@ -43,7 +43,7 @@ class Internal(Cog):
self.socket_event_total += 1
self.socket_events[event_type] += 1
- def _format(self, inp: str, out: Any) -> Tuple[str, Optional[discord.Embed]]:
+ def _format(self, inp: str, out: Any) -> tuple[str, discord.Embed | None]:
"""Format the eval output into a string & attempt to format it into an Embed."""
self._ = out
@@ -137,7 +137,7 @@ class Internal(Cog):
return res # Return (text, embed)
- async def _eval(self, ctx: Context, code: str) -> Optional[discord.Message]:
+ async def _eval(self, ctx: Context, code: str) -> discord.Message | None:
"""Eval the input code string & send an embed to the invoking context."""
self.ln += 1
@@ -166,18 +166,18 @@ class Internal(Cog):
async def func(): # (None,) -> Any
try:
with contextlib.redirect_stdout(self.stdout):
-{0}
+{}
if '_' in locals():
if inspect.isawaitable(_):
_ = await _
return _
finally:
self.env.update(locals())
-""".format(textwrap.indent(code, ' '))
+""".format(textwrap.indent(code, " "))
try:
- exec(code_, self.env) # noqa: B102,S102
- func = self.env['func']
+ exec(code_, self.env) # noqa: S102
+ func = self.env["func"]
res = await func()
except Exception:
@@ -209,23 +209,24 @@ async def func(): # (None,) -> Any
f"... response truncated; {paste_text}",
embed=embed
)
- return
+ return None
await ctx.send(f"```py\n{out}```", embed=embed)
+ return None
- @group(name='internal', aliases=('int',))
+ @group(name="internal", aliases=("int",))
@has_any_role(Roles.owners, Roles.admins, Roles.core_developers)
async def internal_group(self, ctx: Context) -> None:
"""Internal commands. Top secret!"""
if not ctx.invoked_subcommand:
await ctx.send_help(ctx.command)
- @internal_group.command(name='eval', aliases=('e',))
+ @internal_group.command(name="eval", aliases=("e",))
@has_any_role(Roles.admins, Roles.owners)
async def eval(self, ctx: Context, *, code: str) -> None:
"""Run eval in a REPL-like format."""
code = code.strip("`")
- if re.match('py(thon)?\n', code):
+ if re.match("py(thon)?\n", code):
code = "\n".join(code.split("\n")[1:])
if not re.search( # Check if it's an expression
@@ -236,7 +237,7 @@ async def func(): # (None,) -> Any
await self._eval(ctx, code)
- @internal_group.command(name='socketstats', aliases=('socket', 'stats'))
+ @internal_group.command(name="socketstats", aliases=("socket", "stats"))
@has_any_role(Roles.admins, Roles.owners, Roles.core_developers)
async def socketstats(self, ctx: Context) -> None:
"""Fetch information on the socket events received from Discord."""
diff --git a/bot/exts/utils/ping.py b/bot/exts/utils/ping.py
index edac4ebca..184a382e3 100644
--- a/bot/exts/utils/ping.py
+++ b/bot/exts/utils/ping.py
@@ -54,7 +54,7 @@ class Latency(commands.Cog):
embed = Embed(title="Pong!")
- for desc, latency in zip(DESCRIPTIONS, [bot_ping, site_status, discord_ping]):
+ for desc, latency in zip(DESCRIPTIONS, [bot_ping, site_status, discord_ping], strict=True):
embed.add_field(name=desc, value=latency, inline=False)
await ctx.send(embed=embed)
diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py
index 368f08510..b6dae0b51 100644
--- a/bot/exts/utils/reminders.py
+++ b/bot/exts/utils/reminders.py
@@ -1,7 +1,7 @@
import random
import textwrap
import typing as t
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from operator import itemgetter
import discord
@@ -31,8 +31,8 @@ LOCK_NAMESPACE = "reminder"
WHITELISTED_CHANNELS = Guild.reminder_whitelist
MAXIMUM_REMINDERS = 5
-Mentionable = t.Union[discord.Member, discord.Role]
-ReminderMention = t.Union[UnambiguousUser, discord.Role]
+Mentionable = discord.Member | discord.Role
+ReminderMention = UnambiguousUser | discord.Role
class Reminders(Cog):
@@ -50,18 +50,18 @@ class Reminders(Cog):
"""Get all current reminders from the API and reschedule them."""
await self.bot.wait_until_guild_available()
response = await self.bot.api_client.get(
- 'bot/reminders',
- params={'active': 'true'}
+ "bot/reminders",
+ params={"active": "true"}
)
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
for reminder in response:
is_valid, *_ = self.ensure_valid_reminder(reminder)
if not is_valid:
continue
- remind_at = isoparse(reminder['expiration'])
+ remind_at = isoparse(reminder["expiration"])
# If the reminder is already overdue ...
if remind_at < now:
@@ -69,9 +69,9 @@ class Reminders(Cog):
else:
self.schedule_reminder(reminder)
- def ensure_valid_reminder(self, reminder: dict) -> t.Tuple[bool, discord.TextChannel]:
+ def ensure_valid_reminder(self, reminder: dict) -> tuple[bool, discord.TextChannel]:
"""Ensure reminder channel can be fetched otherwise delete the reminder."""
- channel = self.bot.get_channel(reminder['channel_id'])
+ channel = self.bot.get_channel(reminder["channel_id"])
is_valid = True
if not channel:
is_valid = False
@@ -87,7 +87,7 @@ class Reminders(Cog):
async def _send_confirmation(
ctx: Context,
on_success: str,
- reminder_id: t.Union[str, int]
+ reminder_id: str | int
) -> None:
"""Send an embed confirming the reminder change was made successfully."""
embed = discord.Embed(
@@ -103,7 +103,7 @@ class Reminders(Cog):
await ctx.send(embed=embed)
@staticmethod
- async def _check_mentions(ctx: Context, mentions: t.Iterable[Mentionable]) -> t.Tuple[bool, str]:
+ async def _check_mentions(ctx: Context, mentions: t.Iterable[Mentionable]) -> tuple[bool, str]:
"""
Returns whether or not the list of mentions is allowed.
@@ -115,10 +115,9 @@ class Reminders(Cog):
"""
if await has_no_roles_check(ctx, *STAFF_PARTNERS_COMMUNITY_ROLES):
return False, "members/roles"
- elif await has_no_roles_check(ctx, *MODERATION_ROLES):
- return all(isinstance(mention, (discord.User, discord.Member)) for mention in mentions), "roles"
- else:
- return True, ""
+ if await has_no_roles_check(ctx, *MODERATION_ROLES):
+ return all(isinstance(mention, discord.User | discord.Member) for mention in mentions), "roles"
+ return True, ""
@staticmethod
async def validate_mentions(ctx: Context, mentions: t.Iterable[Mentionable]) -> bool:
@@ -131,11 +130,10 @@ class Reminders(Cog):
if not mentions or mentions_allowed:
return True
- else:
- await send_denial(ctx, f"You can't mention other {disallowed_mentions} in your reminder!")
- return False
+ await send_denial(ctx, f"You can't mention other {disallowed_mentions} in your reminder!")
+ return False
- async def get_mentionables(self, mention_ids: t.List[int]) -> t.Iterator[Mentionable]:
+ async def get_mentionables(self, mention_ids: list[int]) -> t.Iterator[Mentionable]:
"""Converts Role and Member ids to their corresponding objects if possible."""
guild = self.bot.get_guild(Guild.id)
for mention_id in mention_ids:
@@ -145,7 +143,7 @@ class Reminders(Cog):
def schedule_reminder(self, reminder: dict) -> None:
"""A coroutine which sends the reminder once the time is reached, and cancels the running task."""
- reminder_datetime = isoparse(reminder['expiration'])
+ reminder_datetime = isoparse(reminder["expiration"])
self.scheduler.schedule_at(reminder_datetime, reminder["id"], self.send_reminder(reminder))
async def _edit_reminder(self, reminder_id: int, payload: dict) -> dict:
@@ -156,7 +154,7 @@ class Reminders(Cog):
"""
# Send the request to update the reminder in the database
reminder = await self.bot.api_client.patch(
- 'bot/reminders/' + str(reminder_id),
+ "bot/reminders/" + str(reminder_id),
json=payload
)
return reminder
@@ -170,7 +168,7 @@ class Reminders(Cog):
self.schedule_reminder(reminder)
@lock_arg(LOCK_NAMESPACE, "reminder", itemgetter("id"), raise_error=True)
- async def send_reminder(self, reminder: dict, expected_time: t.Optional[time.Timestamp] = None) -> None:
+ async def send_reminder(self, reminder: dict, expected_time: time.Timestamp | None = None) -> None:
"""Send the reminder."""
is_valid, channel = self.ensure_valid_reminder(reminder)
if not is_valid:
@@ -194,7 +192,7 @@ class Reminders(Cog):
embed.description = f"Here's your reminder: {reminder['content']}"
# Here the jump URL is in the format of base_url/guild_id/channel_id/message_id
- additional_mentions = ' '.join([
+ additional_mentions = " ".join([
mentionable.mention async for mentionable in self.get_mentionables(reminder["mentions"])
])
@@ -214,7 +212,7 @@ class Reminders(Cog):
await self.bot.api_client.delete(f"bot/reminders/{reminder['id']}")
@staticmethod
- async def try_get_content_from_reply(ctx: Context) -> t.Optional[str]:
+ async def try_get_content_from_reply(ctx: Context) -> str | None:
"""
Attempts to get content from the referenced message, if applicable.
@@ -228,7 +226,7 @@ class Reminders(Cog):
# If we weren't able to get the content of a replied message
if content is None:
await send_denial(ctx, "Your reminder must have a content and/or reply to a message.")
- return
+ return None
# If the replied message has no content (e.g. only attachments/embeds)
if content == "":
@@ -238,7 +236,7 @@ class Reminders(Cog):
@group(name="remind", aliases=("reminder", "reminders", "remindme"), invoke_without_command=True)
async def remind_group(
- self, ctx: Context, mentions: Greedy[ReminderMention], expiration: Duration, *, content: t.Optional[str] = None
+ self, ctx: Context, mentions: Greedy[ReminderMention], expiration: Duration, *, content: str | None = None
) -> None:
"""
Commands for managing your reminders.
@@ -258,7 +256,7 @@ class Reminders(Cog):
@remind_group.command(name="new", aliases=("add", "create"))
async def new_reminder(
- self, ctx: Context, mentions: Greedy[ReminderMention], expiration: Duration, *, content: t.Optional[str] = None
+ self, ctx: Context, mentions: Greedy[ReminderMention], expiration: Duration, *, content: str | None = None
) -> None:
"""
Set yourself a simple reminder.
@@ -286,9 +284,9 @@ class Reminders(Cog):
# Get their current active reminders
active_reminders = await self.bot.api_client.get(
- 'bot/reminders',
+ "bot/reminders",
params={
- 'author__id': str(ctx.author.id)
+ "author__id": str(ctx.author.id)
}
)
@@ -317,14 +315,14 @@ class Reminders(Cog):
# Now we can attempt to actually set the reminder.
reminder = await self.bot.api_client.post(
- 'bot/reminders',
+ "bot/reminders",
json={
- 'author': ctx.author.id,
- 'channel_id': ctx.message.channel.id,
- 'jump_url': ctx.message.jump_url,
- 'content': content,
- 'expiration': expiration.isoformat(),
- 'mentions': mention_ids,
+ "author": ctx.author.id,
+ "channel_id": ctx.message.channel.id,
+ "jump_url": ctx.message.jump_url,
+ "content": content,
+ "expiration": expiration.isoformat(),
+ "mentions": mention_ids,
}
)
@@ -349,14 +347,14 @@ class Reminders(Cog):
"""View a paginated embed of all reminders for your user."""
# Get all the user's reminders from the database.
data = await self.bot.api_client.get(
- 'bot/reminders',
- params={'author__id': str(ctx.author.id)}
+ "bot/reminders",
+ params={"author__id": str(ctx.author.id)}
)
# Make a list of tuples so it can be sorted by time.
reminders = sorted(
(
- (rem['content'], rem['expiration'], rem['id'], rem['mentions'])
+ (rem["content"], rem["expiration"], rem["id"], rem["mentions"])
for rem in data
),
key=itemgetter(1)
@@ -421,10 +419,10 @@ class Reminders(Cog):
For example, to edit a reminder to expire in 3 days and 1 minute, you can do `!remind edit duration 1234 3d1M`.
"""
- await self.edit_reminder(ctx, id_, {'expiration': expiration.isoformat()})
+ await self.edit_reminder(ctx, id_, {"expiration": expiration.isoformat()})
@edit_reminder_group.command(name="content", aliases=("reason",))
- async def edit_reminder_content(self, ctx: Context, id_: int, *, content: t.Optional[str] = None) -> None:
+ async def edit_reminder_content(self, ctx: Context, id_: int, *, content: str | None = None) -> None:
"""
Edit one of your reminder's content.
@@ -518,7 +516,7 @@ class Reminders(Cog):
)
await ctx.send(embed=embed)
- async def _can_modify(self, ctx: Context, reminder_id: t.Union[str, int], send_on_denial: bool = True) -> bool:
+ async def _can_modify(self, ctx: Context, reminder_id: str | int, send_on_denial: bool = True) -> bool:
"""
Check whether the reminder can be modified by the ctx author.
@@ -536,7 +534,7 @@ class Reminders(Cog):
if await has_any_role_check(ctx, Roles.admins):
return True
- if not api_response["author"] == ctx.author.id:
+ if api_response["author"] != ctx.author.id:
log.debug(f"{ctx.author} is not the reminder author and does not pass the check.")
if send_on_denial:
await send_denial(ctx, "You can't modify reminders of other users!")
diff --git a/bot/exts/utils/snekbox/_cog.py b/bot/exts/utils/snekbox/_cog.py
index d7e8bc93c..5ab48ad43 100644
--- a/bot/exts/utils/snekbox/_cog.py
+++ b/bot/exts/utils/snekbox/_cog.py
@@ -6,7 +6,7 @@ import re
from functools import partial
from operator import attrgetter
from textwrap import dedent
-from typing import Literal, NamedTuple, Optional, TYPE_CHECKING
+from typing import Literal, NamedTuple, TYPE_CHECKING
from discord import AllowedMentions, HTTPException, Interaction, Message, NotFound, Reaction, User, enums, ui
from discord.ext.commands import Cog, Command, Context, Converter, command, guild_only
@@ -85,12 +85,14 @@ NO_SNEKBOX_CHANNELS = (Channels.python_general,)
NO_SNEKBOX_CATEGORIES = ()
SNEKBOX_ROLES = (Roles.helpers, Roles.moderators, Roles.admins, Roles.owners, Roles.python_community, Roles.partners)
-REDO_EMOJI = '\U0001f501' # :repeat:
+REDO_EMOJI = "\U0001f501" # :repeat:
REDO_TIMEOUT = 30
PythonVersion = Literal["3.10", "3.11"]
-FilteredFiles = NamedTuple("FilteredFiles", [("allowed", list[FileAttachment]), ("blocked", list[FileAttachment])])
+class FilteredFiles(NamedTuple):
+ allowed: list[FileAttachment]
+ blocked: list[FileAttachment]
class CodeblockConverter(Converter):
@@ -207,7 +209,7 @@ class Snekbox(Cog):
return EvalResult.from_dict(await resp.json())
@staticmethod
- async def upload_output(output: str) -> Optional[str]:
+ async def upload_output(output: str) -> str | None:
"""Upload the job's output to a paste service and return a URL to it if successful."""
log.trace("Uploading full output to paste service...")
@@ -433,13 +435,13 @@ class Snekbox(Cog):
with contextlib.suppress(NotFound):
try:
_, new_message = await self.bot.wait_for(
- 'message_edit',
+ "message_edit",
check=_predicate_message_edit,
timeout=REDO_TIMEOUT
)
await ctx.message.add_reaction(REDO_EMOJI)
await self.bot.wait_for(
- 'reaction_add',
+ "reaction_add",
check=_predicate_emoji_reaction,
timeout=10
)
@@ -466,12 +468,11 @@ class Snekbox(Cog):
if job_name == "timeit":
return EvalJob(self.prepare_timeit_input(codeblocks))
- else:
- return EvalJob.from_code("\n".join(codeblocks))
+ return EvalJob.from_code("\n".join(codeblocks))
return None
- async def get_code(self, message: Message, command: Command) -> Optional[str]:
+ async def get_code(self, message: Message, command: Command) -> str | None:
"""
Return the code from `message` to be evaluated.
diff --git a/bot/exts/utils/snekbox/_eval.py b/bot/exts/utils/snekbox/_eval.py
index 2f61b5924..b5c481e6a 100644
--- a/bot/exts/utils/snekbox/_eval.py
+++ b/bot/exts/utils/snekbox/_eval.py
@@ -77,10 +77,10 @@ class EvalResult:
"""Return an emoji corresponding to the status code or lack of output in result."""
if not self.has_output:
return ":warning:"
- elif self.returncode == 0: # No error
+ if self.returncode == 0: # No error
return ":white_check_mark:"
- else: # Exception
- return ":x:"
+ # Exception
+ return ":x:"
@property
def error_message(self) -> str:
diff --git a/bot/exts/utils/thread_bumper.py b/bot/exts/utils/thread_bumper.py
index 0384119f5..e6c47c1ba 100644
--- a/bot/exts/utils/thread_bumper.py
+++ b/bot/exts/utils/thread_bumper.py
@@ -1,4 +1,3 @@
-import typing as t
import discord
from discord.ext import commands
@@ -31,11 +30,10 @@ class ThreadBumper(commands.Cog):
) as response:
if response.status == 204:
return True
- elif response.status == 404:
+ if response.status == 404:
return False
- else:
- # A status other than 204/404 is undefined behaviour from site. Raise error for investigation.
- raise ResponseCodeError(response, response.text())
+ # A status other than 204/404 is undefined behaviour from site. Raise error for investigation.
+ raise ResponseCodeError(response, response.text())
async def unarchive_threads_not_manually_archived(self, threads: list[discord.Thread]) -> None:
"""
@@ -94,7 +92,7 @@ class ThreadBumper(commands.Cog):
await ctx.send_help(ctx.command)
@thread_bump_group.command(name="add", aliases=("a",))
- async def add_thread_to_bump_list(self, ctx: commands.Context, thread: t.Optional[discord.Thread]) -> None:
+ async def add_thread_to_bump_list(self, ctx: commands.Context, thread: discord.Thread | None) -> None:
"""Add a thread to the bump list."""
if not thread:
if isinstance(ctx.channel, discord.Thread):
@@ -109,7 +107,7 @@ class ThreadBumper(commands.Cog):
await ctx.send(f":ok_hand:{thread.mention} has been added to the bump list.")
@thread_bump_group.command(name="remove", aliases=("r", "rem", "d", "del", "delete"))
- async def remove_thread_from_bump_list(self, ctx: commands.Context, thread: t.Optional[discord.Thread]) -> None:
+ async def remove_thread_from_bump_list(self, ctx: commands.Context, thread: discord.Thread | None) -> None:
"""Remove a thread from the bump list."""
if not thread:
if isinstance(ctx.channel, discord.Thread):
diff --git a/bot/exts/utils/utils.py b/bot/exts/utils/utils.py
index 9fe3b46e4..dd9daef1d 100644
--- a/bot/exts/utils/utils.py
+++ b/bot/exts/utils/utils.py
@@ -1,7 +1,6 @@
import difflib
import re
import unicodedata
-from typing import Tuple
from discord import Colour, Embed, utils
from discord.ext.commands import BadArgument, Cog, Context, clean_content, command, has_any_role
@@ -65,7 +64,7 @@ class Utils(Cog):
await messages.send_denial(ctx, f"Too many characters ({len(characters)}/50)")
return
- def get_info(char: str) -> Tuple[str, str]:
+ def get_info(char: str) -> tuple[str, str]:
digit = f"{ord(char):x}"
if len(digit) <= 4:
u_code = f"\\u{digit:>04}"
@@ -76,12 +75,12 @@ class Utils(Cog):
info = f"`{u_code.ljust(10)}`: {name} - {utils.escape_markdown(char)}"
return info, u_code
- char_list, raw_list = zip(*(get_info(c) for c in characters))
+ char_list, raw_list = zip(*(get_info(c) for c in characters), strict=True)
embed = Embed().set_author(name="Character Info")
if len(characters) > 1:
# Maximum length possible is 502 out of 1024, so there's no need to truncate.
- embed.add_field(name='Full Raw Text', value=f"`{''.join(raw_list)}`", inline=False)
+ embed.add_field(name="Full Raw Text", value=f"`{''.join(raw_list)}`", inline=False)
await LinePaginator.paginate(char_list, ctx, embed, max_lines=10, max_size=2000, empty=False)
diff --git a/bot/log.py b/bot/log.py
index 236031f25..8b18df70a 100644
--- a/bot/log.py
+++ b/bot/log.py
@@ -3,7 +3,7 @@ import os
import sys
from logging import Logger, handlers
from pathlib import Path
-from typing import Optional, TYPE_CHECKING, cast
+from typing import TYPE_CHECKING, cast
import coloredlogs
import sentry_sdk
@@ -37,7 +37,7 @@ class CustomLogger(LoggerClass):
self.log(TRACE_LEVEL, msg, *args, **kwargs)
-def get_logger(name: Optional[str] = None) -> CustomLogger:
+def get_logger(name: str | None = None) -> CustomLogger:
"""Utility to make mypy recognise that logger is of type `CustomLogger`."""
return cast(CustomLogger, logging.getLogger(name))
diff --git a/bot/pagination.py b/bot/pagination.py
index 679108933..d96e1369d 100644
--- a/bot/pagination.py
+++ b/bot/pagination.py
@@ -1,5 +1,4 @@
import asyncio
-import typing as t
from contextlib import suppress
from functools import partial
@@ -25,7 +24,6 @@ log = get_logger(__name__)
class EmptyPaginatorEmbedError(Exception):
"""Raised when attempting to paginate with empty contents."""
- pass
class LinePaginator(Paginator):
@@ -47,11 +45,11 @@ class LinePaginator(Paginator):
def __init__(
self,
- prefix: str = '```',
- suffix: str = '```',
+ prefix: str = "```",
+ suffix: str = "```",
max_size: int = 4000,
scale_to_size: int = 4000,
- max_lines: t.Optional[int] = None,
+ max_lines: int | None = None,
linesep: str = "\n"
) -> None:
"""
@@ -84,7 +82,7 @@ class LinePaginator(Paginator):
self._count = len(prefix) + 1 # prefix + newline
self._pages = []
- def add_line(self, line: str = '', *, empty: bool = False) -> None:
+ def add_line(self, line: str = "", *, empty: bool = False) -> None:
"""
Adds a line to the current page.
@@ -125,7 +123,7 @@ class LinePaginator(Paginator):
self._current_page.append(line)
if empty:
- self._current_page.append('')
+ self._current_page.append("")
self._count += 1
# Start a new page if there were any overflow words
@@ -144,7 +142,7 @@ class LinePaginator(Paginator):
self._count = len(self.prefix) + 1
self.close_page()
- def _split_remaining_words(self, line: str, max_chars: int) -> t.Tuple[str, t.Optional[str]]:
+ def _split_remaining_words(self, line: str, max_chars: int) -> tuple[str, str | None]:
"""
Internal: split a line into two strings -- reduced_words and remaining_words.
@@ -195,7 +193,7 @@ class LinePaginator(Paginator):
embed: discord.Embed,
prefix: str = "",
suffix: str = "",
- max_lines: t.Optional[int] = None,
+ max_lines: int | None = None,
max_size: int = 500,
scale_to_size: int = 4000,
empty: bool = True,
@@ -205,7 +203,7 @@ class LinePaginator(Paginator):
url: str = None,
exception_on_empty_embed: bool = False,
reply: bool = False,
- ) -> t.Optional[discord.Message]:
+ ) -> discord.Message | None:
"""
Use a paginator and set of reactions to provide pagination over a set of lines.
@@ -271,24 +269,24 @@ class LinePaginator(Paginator):
if isinstance(ctx, discord.Interaction):
return await ctx.response.send_message(embed=embed)
return await ctx.send(embed=embed, reference=reference)
+
+ if footer_text:
+ embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
else:
- if footer_text:
- embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
- else:
- embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
- log.trace(f"Setting embed footer to '{embed.footer.text}'")
+ embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
+ log.trace(f"Setting embed footer to '{embed.footer.text}'")
- if url:
- embed.url = url
- log.trace(f"Setting embed url to '{url}'")
+ if url:
+ embed.url = url
+ log.trace(f"Setting embed url to '{url}'")
- log.debug("Sending first page to channel...")
+ log.debug("Sending first page to channel...")
- if isinstance(ctx, discord.Interaction):
- await ctx.response.send_message(embed=embed)
- message = await ctx.original_response()
- else:
- message = await ctx.send(embed=embed, reference=reference)
+ if isinstance(ctx, discord.Interaction):
+ await ctx.response.send_message(embed=embed)
+ message = await ctx.original_response()
+ else:
+ message = await ctx.send(embed=embed, reference=reference)
log.debug("Adding emoji reactions to message...")
@@ -318,7 +316,7 @@ class LinePaginator(Paginator):
if str(reaction.emoji) == DELETE_EMOJI:
log.debug("Got delete reaction")
return await message.delete()
- elif reaction.emoji in PAGINATION_EMOJI:
+ if reaction.emoji in PAGINATION_EMOJI:
total_pages = len(paginator.pages)
try:
await message.remove_reaction(reaction.emoji, user)
@@ -361,8 +359,7 @@ class LinePaginator(Paginator):
if e.code == 50083:
# Trying to act on an archived thread, just ignore and abort
break
- else:
- raise e
+ raise e
log.debug("Ending pagination and clearing reactions.")
with suppress(discord.NotFound):
diff --git a/bot/rules/mentions.py b/bot/rules/mentions.py
deleted file mode 100644
index ca1d0c01c..000000000
--- a/bot/rules/mentions.py
+++ /dev/null
@@ -1,70 +0,0 @@
-from typing import Dict, Iterable, List, Optional, Tuple
-
-from discord import DeletedReferencedMessage, Member, Message, MessageType, NotFound
-
-import bot
-from bot.log import get_logger
-
-log = get_logger(__name__)
-
-
-async def apply(
- last_message: Message, recent_messages: List[Message], config: Dict[str, int]
-) -> Optional[Tuple[str, Iterable[Member], Iterable[Message]]]:
- """
- Detects total mentions exceeding the limit sent by a single user.
-
- Excludes mentions that are bots, themselves, or replied users.
-
- In very rare cases, may not be able to determine a
- mention was to a reply, in which case it is not ignored.
- """
- relevant_messages = tuple(
- msg
- for msg in recent_messages
- if msg.author == last_message.author
- )
- # We use `msg.mentions` here as that is supplied by the api itself, to determine who was mentioned.
- # Additionally, `msg.mentions` includes the user replied to, even if the mention doesn't occur in the body.
- # In order to exclude users who are mentioned as a reply, we check if the msg has a reference
- #
- # While we could use regex to parse the message content, and get a list of
- # the mentions, that solution is very prone to breaking.
- # We would need to deal with codeblocks, escaping markdown, and any discrepancies between
- # our implementation and discord's markdown parser which would cause false positives or false negatives.
- total_recent_mentions = 0
- for msg in relevant_messages:
- # We check if the message is a reply, and if it is try to get the author
- # since we ignore mentions of a user that we're replying to
- reply_author = None
-
- if msg.type == MessageType.reply:
- ref = msg.reference
-
- if not (resolved := ref.resolved):
- # It is possible, in a very unusual situation, for a message to have a reference
- # that is both not in the cache, and deleted while running this function.
- # In such a situation, this will throw an error which we catch.
- try:
- resolved = await bot.instance.get_partial_messageable(resolved.channel_id).fetch_message(
- resolved.message_id
- )
- except NotFound:
- log.info('Could not fetch the reference message as it has been deleted.')
-
- if resolved and not isinstance(resolved, DeletedReferencedMessage):
- reply_author = resolved.author
-
- for user in msg.mentions:
- # Don't count bot or self mentions, or the user being replied to (if applicable)
- if user.bot or user in {msg.author, reply_author}:
- continue
- total_recent_mentions += 1
-
- if total_recent_mentions > config['max']:
- return (
- f"sent {total_recent_mentions} mentions in {config['interval']}s",
- (last_message.author,),
- relevant_messages
- )
- return None
diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py
index 567821126..f576b7f11 100644
--- a/bot/utils/__init__.py
+++ b/bot/utils/__init__.py
@@ -2,11 +2,11 @@ from bot.utils.helpers import CogABCMeta, find_nth_occurrence, has_lines, pad_ba
from bot.utils.services import PasteTooLongError, PasteUploadError, send_to_paste_service
__all__ = [
- 'CogABCMeta',
- 'find_nth_occurrence',
- 'has_lines',
- 'pad_base64',
- 'send_to_paste_service',
- 'PasteUploadError',
- 'PasteTooLongError',
+ "CogABCMeta",
+ "find_nth_occurrence",
+ "has_lines",
+ "pad_base64",
+ "send_to_paste_service",
+ "PasteUploadError",
+ "PasteTooLongError",
]
diff --git a/bot/utils/caching.py b/bot/utils/caching.py
index 68ce15607..2d0e077ec 100644
--- a/bot/utils/caching.py
+++ b/bot/utils/caching.py
@@ -1,6 +1,7 @@
import functools
from collections import OrderedDict
-from typing import Any, Callable
+from collections.abc import Callable
+from typing import Any
class AsyncCache:
diff --git a/bot/utils/channel.py b/bot/utils/channel.py
index 20f433a3f..05afbe009 100644
--- a/bot/utils/channel.py
+++ b/bot/utils/channel.py
@@ -1,4 +1,3 @@
-from typing import Union
import discord
@@ -9,7 +8,7 @@ from bot.log import get_logger
log = get_logger(__name__)
-def is_mod_channel(channel: Union[discord.TextChannel, discord.Thread]) -> bool:
+def is_mod_channel(channel: discord.TextChannel | discord.Thread) -> bool:
"""True if channel, or channel.parent for threads, is considered a mod channel."""
if isinstance(channel, discord.Thread):
channel = channel.parent
@@ -18,13 +17,12 @@ def is_mod_channel(channel: Union[discord.TextChannel, discord.Thread]) -> bool:
log.trace(f"Channel #{channel} is a configured mod channel")
return True
- elif any(is_in_category(channel, category) for category in constants.MODERATION_CATEGORIES):
+ if any(is_in_category(channel, category) for category in constants.MODERATION_CATEGORIES):
log.trace(f"Channel #{channel} is in a configured mod category")
return True
- else:
- log.trace(f"Channel #{channel} is not a mod channel")
- return False
+ log.trace(f"Channel #{channel} is not a mod channel")
+ return False
def is_staff_channel(channel: discord.TextChannel) -> bool:
diff --git a/bot/utils/checks.py b/bot/utils/checks.py
index 188285684..d4f9c3a27 100644
--- a/bot/utils/checks.py
+++ b/bot/utils/checks.py
@@ -1,4 +1,4 @@
-from typing import Callable, Container, Iterable, Optional, Union
+from collections.abc import Callable, Container, Iterable
from discord.ext.commands import (
BucketType, CheckFailure, Cog, Command, CommandOnCooldown, Context, Cooldown, CooldownMapping, NoPrivateMessage,
@@ -14,7 +14,7 @@ log = get_logger(__name__)
class ContextCheckFailure(CheckFailure):
"""Raised when a context-specific check fails."""
- def __init__(self, redirect_channel: Optional[int]) -> None:
+ def __init__(self, redirect_channel: int | None) -> None:
self.redirect_channel = redirect_channel
if redirect_channel:
@@ -36,7 +36,7 @@ def in_whitelist_check(
channels: Container[int] = (),
categories: Container[int] = (),
roles: Container[int] = (),
- redirect: Optional[int] = constants.Channels.bot_commands,
+ redirect: int | None = constants.Channels.bot_commands,
fail_silently: bool = False,
) -> bool:
"""
@@ -86,7 +86,7 @@ def in_whitelist_check(
return False
-async def has_any_role_check(ctx: Context, *roles: Union[str, int]) -> bool:
+async def has_any_role_check(ctx: Context, *roles: str | int) -> bool:
"""
Returns True if the context's author has any of the specified roles.
@@ -99,7 +99,7 @@ async def has_any_role_check(ctx: Context, *roles: Union[str, int]) -> bool:
return False
-async def has_no_roles_check(ctx: Context, *roles: Union[str, int]) -> bool:
+async def has_no_roles_check(ctx: Context, *roles: str | int) -> bool:
"""
Returns True if the context's author doesn't have any of the specified roles.
@@ -114,8 +114,13 @@ async def has_no_roles_check(ctx: Context, *roles: Union[str, int]) -> bool:
return True
-def cooldown_with_role_bypass(rate: int, per: float, type: BucketType = BucketType.default, *,
- bypass_roles: Iterable[int]) -> Callable:
+def cooldown_with_role_bypass(
+ rate: int,
+ per: float,
+ type: BucketType = BucketType.default,
+ *,
+ bypass_roles: Iterable[int]
+) -> Callable:
"""
Applies a cooldown to a command, but allows members with certain roles to be ignored.
@@ -148,8 +153,10 @@ def cooldown_with_role_bypass(rate: int, per: float, type: BucketType = BucketTy
#
# if the `before_invoke` detail is ever a problem then I can quickly just swap over.
if not isinstance(command, Command):
- raise TypeError('Decorator `cooldown_with_role_bypass` must be applied after the command decorator. '
- 'This means it has to be above the command decorator in the code.')
+ raise TypeError(
+ "Decorator `cooldown_with_role_bypass` must be applied after the command decorator. "
+ "This means it has to be above the command decorator in the code."
+ )
command._before_invoke = predicate
diff --git a/bot/utils/function.py b/bot/utils/function.py
index 55115d7d3..912dfb711 100644
--- a/bot/utils/function.py
+++ b/bot/utils/function.py
@@ -9,7 +9,7 @@ from bot.log import get_logger
log = get_logger(__name__)
-Argument = t.Union[int, str]
+Argument = int | str
BoundArgs = t.OrderedDict[str, t.Any]
Decorator = t.Callable[[t.Callable], t.Callable]
ArgValGetter = t.Callable[[BoundArgs], t.Any]
@@ -72,7 +72,7 @@ def get_arg_value_wrapper(
return decorator_func(wrapper)
-def get_bound_args(func: t.Callable, args: t.Tuple, kwargs: t.Dict[str, t.Any]) -> BoundArgs:
+def get_bound_args(func: t.Callable, args: tuple, kwargs: dict[str, t.Any]) -> BoundArgs:
"""
Bind `args` and `kwargs` to `func` and return a mapping of parameter names to argument values.
@@ -89,7 +89,7 @@ def update_wrapper_globals(
wrapper: types.FunctionType,
wrapped: types.FunctionType,
*,
- ignored_conflict_names: t.Set[str] = frozenset(),
+ ignored_conflict_names: set[str] = frozenset(),
) -> types.FunctionType:
"""
Update globals of `wrapper` with the globals from `wrapped`.
@@ -134,7 +134,7 @@ def command_wraps(
assigned: t.Sequence[str] = functools.WRAPPER_ASSIGNMENTS,
updated: t.Sequence[str] = functools.WRAPPER_UPDATES,
*,
- ignored_conflict_names: t.Set[str] = frozenset(),
+ ignored_conflict_names: set[str] = frozenset(),
) -> t.Callable[[types.FunctionType], types.FunctionType]:
"""Update the decorated function to look like `wrapped` and update globals for discordpy forwardref evaluation."""
def decorator(wrapper: types.FunctionType) -> types.FunctionType:
diff --git a/bot/utils/helpers.py b/bot/utils/helpers.py
index 2a81fbed6..7b5474a2f 100644
--- a/bot/utils/helpers.py
+++ b/bot/utils/helpers.py
@@ -1,16 +1,15 @@
from abc import ABCMeta
-from typing import Optional
from urllib.parse import urlparse
from discord.ext.commands import CogMeta
from tldextract import extract
-class CogABCMeta(CogMeta, ABCMeta): # noqa: B024 (Ignore abstract class with no abstract methods.)
+class CogABCMeta(CogMeta, ABCMeta):
"""Metaclass for ABCs meant to be implemented as Cogs."""
-def find_nth_occurrence(string: str, substring: str, n: int) -> Optional[int]:
+def find_nth_occurrence(string: str, substring: str, n: int) -> int | None:
"""Return index of `n`th occurrence of `substring` in `string`, or None if not found."""
index = 0
for _ in range(n):
diff --git a/bot/utils/lock.py b/bot/utils/lock.py
index c039a4f25..a5f442b78 100644
--- a/bot/utils/lock.py
+++ b/bot/utils/lock.py
@@ -2,8 +2,9 @@ import asyncio
import inspect
import types
from collections import defaultdict
+from collections.abc import Awaitable, Callable, Hashable
from functools import partial
-from typing import Any, Awaitable, Callable, Hashable, Union
+from typing import Any
from weakref import WeakValueDictionary
from bot.errors import LockedResourceError
@@ -14,9 +15,9 @@ from bot.utils.function import command_wraps
log = get_logger(__name__)
__lock_dicts = defaultdict(WeakValueDictionary)
-_IdCallableReturn = Union[Hashable, Awaitable[Hashable]]
+_IdCallableReturn = Hashable | Awaitable[Hashable]
_IdCallable = Callable[[function.BoundArgs], _IdCallableReturn]
-ResourceId = Union[Hashable, _IdCallable]
+ResourceId = Hashable | _IdCallable
class SharedEvent:
@@ -110,6 +111,7 @@ def lock(
log.info(f"{name}: aborted because resource {namespace!r}:{id_!r} is locked")
if raise_error:
raise LockedResourceError(str(namespace), id_)
+ return None
return wrapper
return decorator
diff --git a/bot/utils/members.py b/bot/utils/members.py
index 693286045..08ee78504 100644
--- a/bot/utils/members.py
+++ b/bot/utils/members.py
@@ -7,7 +7,7 @@ from bot.log import get_logger
log = get_logger(__name__)
-async def get_or_fetch_member(guild: discord.Guild, member_id: int) -> t.Optional[discord.Member]:
+async def get_or_fetch_member(guild: discord.Guild, member_id: int) -> discord.Member | None:
"""
Attempt to get a member from cache; on failure fetch from the API.
diff --git a/bot/utils/message_cache.py b/bot/utils/message_cache.py
index 5deb2376b..17667e8ea 100644
--- a/bot/utils/message_cache.py
+++ b/bot/utils/message_cache.py
@@ -143,7 +143,7 @@ class MessageCache:
raise IndexError("cache index out of range")
return self._messages[(item + self._start) % self.maxlen]
- elif isinstance(item, slice):
+ if isinstance(item, slice):
length = len(self)
start, stop, step = item.indices(length)
@@ -176,12 +176,10 @@ class MessageCache:
if step > 0:
offset = ceil((self.maxlen - start) / step) * step + start - self.maxlen
return self._messages[start::step] + self._messages[offset:stop:step]
- else:
- offset = ceil((start + 1) / -step) * -step - start - 1
- return self._messages[start::step] + self._messages[self.maxlen - 1 - offset:stop:step]
+ offset = ceil((start + 1) / -step) * -step - start - 1
+ return self._messages[start::step] + self._messages[self.maxlen - 1 - offset:stop:step]
- else:
- raise TypeError(f"cache indices must be integers or slices, not {type(item)}")
+ raise TypeError(f"cache indices must be integers or slices, not {type(item)}")
def __iter__(self) -> t.Iterator[Message]:
if self._is_empty():
diff --git a/bot/utils/messages.py b/bot/utils/messages.py
index a8c2b95f1..da4a5a300 100644
--- a/bot/utils/messages.py
+++ b/bot/utils/messages.py
@@ -1,12 +1,11 @@
import asyncio
import random
import re
-from collections.abc import Iterable
-from datetime import datetime, timezone
+from collections.abc import Callable, Iterable, Sequence
+from datetime import UTC, datetime
from functools import partial
from io import BytesIO
from itertools import zip_longest
-from typing import Callable, List, Optional, Sequence, Union
import discord
from discord import Message
@@ -53,14 +52,14 @@ def reaction_check(
if user.id in allowed_users or is_moderator:
log.trace(f"Allowed reaction {reaction} by {user} on {reaction.message.id}.")
return True
- else:
- log.trace(f"Removing reaction {reaction} by {user} on {reaction.message.id}: disallowed user.")
- scheduling.create_task(
- reaction.message.remove_reaction(reaction.emoji, user),
- suppressed_exceptions=(discord.HTTPException,),
- name=f"remove_reaction-{reaction}-{reaction.message.id}-{user}"
- )
- return False
+
+ log.trace(f"Removing reaction {reaction} by {user} on {reaction.message.id}: disallowed user.")
+ scheduling.create_task(
+ reaction.message.remove_reaction(reaction.emoji, user),
+ suppressed_exceptions=(discord.HTTPException,),
+ name=f"remove_reaction-{reaction}-{reaction.message.id}-{user}"
+ )
+ return False
async def wait_for_deletion(
@@ -102,7 +101,7 @@ async def wait_for_deletion(
try:
try:
- await bot.instance.wait_for('reaction_add', check=check, timeout=timeout)
+ await bot.instance.wait_for("reaction_add", check=check, timeout=timeout)
except asyncio.TimeoutError:
await message.clear_reactions()
else:
@@ -119,11 +118,11 @@ async def wait_for_deletion(
async def send_attachments(
message: discord.Message,
- destination: Union[discord.TextChannel, discord.Webhook],
+ destination: discord.TextChannel | discord.Webhook,
link_large: bool = True,
use_cached: bool = False,
**kwargs
-) -> List[str]:
+) -> list[str]:
"""
Re-upload the message's attachments to the destination and return a list of their new URLs.
@@ -132,11 +131,11 @@ async def send_attachments(
embed which links to them. Extra kwargs will be passed to send() when sending the attachment.
"""
webhook_send_kwargs = {
- 'username': message.author.display_name,
- 'avatar_url': message.author.display_avatar.url,
+ "username": message.author.display_name,
+ "avatar_url": message.author.display_avatar.url,
}
webhook_send_kwargs.update(kwargs)
- webhook_send_kwargs['username'] = sub_clyde(webhook_send_kwargs['username'])
+ webhook_send_kwargs["username"] = sub_clyde(webhook_send_kwargs["username"])
large = []
urls = []
@@ -220,21 +219,20 @@ async def pin_no_system_message(message: discord.Message) -> bool:
return False
-def sub_clyde(username: Optional[str]) -> Optional[str]:
+def sub_clyde(username: str | None) -> str | None:
"""
- Replace "e"/"E" in any "clyde" in `username` with a Cyrillic "е"/"E" and return the new string.
+ Replace "e"/"E" in any "clyde" in `username` with a Cyrillic "е"/"Е" and return the new string.
Discord disallows "clyde" anywhere in the username for webhooks. It will return a 400.
Return None only if `username` is None.
- """
+ """ # noqa: RUF002
def replace_e(match: re.Match) -> str:
- char = "е" if match[2] == "e" else "Е"
+ char = "е" if match[2] == "e" else "Е" # noqa: RUF001
return match[1] + char
if username:
return re.sub(r"(clyd)(e)", replace_e, username, flags=re.I)
- else:
- return username # Empty string or None
+ return username # Empty string or None
async def send_denial(ctx: Context, reason: str) -> discord.Message:
@@ -285,7 +283,7 @@ async def upload_log(messages: Iterable[Message], actor_id: int, attachments: di
"bot/deleted-messages",
json={
"actor": actor_id,
- "creation": datetime.now(timezone.utc).isoformat(),
+ "creation": datetime.now(UTC).isoformat(),
"deletedmessage_set": deletedmessage_set,
}
)
diff --git a/bot/utils/services.py b/bot/utils/services.py
index a752ac0ec..a25548510 100644
--- a/bot/utils/services.py
+++ b/bot/utils/services.py
@@ -65,12 +65,12 @@ async def send_to_paste_service(contents: str, *, extension: str = "", max_lengt
f"trying again ({attempt}/{FAILED_REQUEST_ATTEMPTS})."
)
continue
- elif "key" in response_json:
+ if "key" in response_json:
log.info(f"Successfully uploaded contents to paste service behind key {response_json['key']}.")
- paste_link = URLs.paste_service.format(key=response_json['key']) + extension
+ paste_link = URLs.paste_service.format(key=response_json["key"]) + extension
- if extension == '.py':
+ if extension == ".py":
return paste_link
return paste_link + "?noredirect"
diff --git a/bot/utils/time.py b/bot/utils/time.py
index 820ac2929..78b46c0c4 100644
--- a/bot/utils/time.py
+++ b/bot/utils/time.py
@@ -5,7 +5,7 @@ import re
from copy import copy
from enum import Enum
from time import struct_time
-from typing import Literal, Optional, TYPE_CHECKING, Union, overload
+from typing import Literal, TYPE_CHECKING, overload
import arrow
from dateutil.relativedelta import relativedelta
@@ -25,16 +25,16 @@ _DURATION_REGEX = re.compile(
# All supported types for the single-argument overload of arrow.get(). tzinfo is excluded because
# it's too implicit of a way for the caller to specify that they want the current time.
-Timestamp = Union[
- arrow.Arrow,
- datetime.datetime,
- datetime.date,
- struct_time,
- int, # POSIX timestamp
- float, # POSIX timestamp
- str, # ISO 8601-formatted string
- tuple[int, int, int], # ISO calendar tuple
-]
+Timestamp = (
+ arrow.Arrow
+ | datetime.datetime
+ | datetime.date
+ | struct_time
+ | int # POSIX timestamp
+ | float # POSIX timestamp
+ | str # ISO 8601-formatted string
+ | tuple[int, int, int] # ISO calendar tuple
+)
_Precision = Literal["years", "months", "days", "hours", "minutes", "seconds"]
@@ -67,12 +67,11 @@ def _stringify_time_unit(value: int, unit: str) -> str:
"""
if unit == "seconds" and value == 0:
return "0 seconds"
- elif value == 1:
+ if value == 1:
return f"{value} {unit[:-1]}"
- elif value == 0:
+ if value == 0:
return f"less than a {unit[:-1]}"
- else:
- return f"{value} {unit}"
+ return f"{value} {unit}"
def discord_timestamp(timestamp: Timestamp, format: TimestampFormats = TimestampFormats.DATE_TIME) -> str:
@@ -88,7 +87,7 @@ def discord_timestamp(timestamp: Timestamp, format: TimestampFormats = Timestamp
# region humanize_delta overloads
@overload
def humanize_delta(
- arg1: Union[relativedelta, Timestamp],
+ arg1: relativedelta | Timestamp,
/,
*,
precision: _Precision = "seconds",
@@ -244,7 +243,7 @@ def humanize_delta(
return humanized
-def parse_duration_string(duration: str) -> Optional[relativedelta]:
+def parse_duration_string(duration: str) -> relativedelta | None:
"""
Convert a `duration` string to a relativedelta object.
@@ -290,10 +289,10 @@ def format_relative(timestamp: Timestamp) -> str:
def format_with_duration(
- timestamp: Optional[Timestamp],
- other_timestamp: Optional[Timestamp] = None,
+ timestamp: Timestamp | None,
+ other_timestamp: Timestamp | None = None,
max_units: int = 2,
-) -> Optional[str]:
+) -> str | None:
"""
Return `timestamp` formatted as a discord timestamp with the timestamp duration since `other_timestamp`.
@@ -316,7 +315,7 @@ def format_with_duration(
return f"{formatted_timestamp} ({duration})"
-def until_expiration(expiry: Optional[Timestamp]) -> str:
+def until_expiration(expiry: Timestamp | None) -> str:
"""
Get the remaining time until an infraction's expiration as a Discord timestamp.
@@ -336,7 +335,7 @@ def until_expiration(expiry: Optional[Timestamp]) -> str:
def unpack_duration(
duration_or_expiry: DurationOrExpiry,
- origin: Optional[Union[datetime.datetime, arrow.Arrow]] = None
+ origin: datetime.datetime | arrow.Arrow | None = None
) -> tuple[datetime.datetime, datetime.datetime]:
"""
Unpacks a DurationOrExpiry into a tuple of (origin, expiry).
@@ -344,15 +343,14 @@ def unpack_duration(
The `origin` defaults to the current UTC time at function call.
"""
if origin is None:
- origin = datetime.datetime.now(tz=datetime.timezone.utc)
+ origin = datetime.datetime.now(tz=datetime.UTC)
if isinstance(origin, arrow.Arrow):
origin = origin.datetime
if isinstance(duration_or_expiry, relativedelta):
return origin, origin + duration_or_expiry
- else:
- return origin, duration_or_expiry
+ return origin, duration_or_expiry
def round_delta(delta: relativedelta) -> relativedelta:
diff --git a/bot/utils/webhooks.py b/bot/utils/webhooks.py
index 9c916b63a..1d144638d 100644
--- a/bot/utils/webhooks.py
+++ b/bot/utils/webhooks.py
@@ -1,4 +1,3 @@
-from typing import Optional
import discord
from discord import Embed
@@ -11,11 +10,11 @@ log = get_logger(__name__)
async def send_webhook(
webhook: discord.Webhook,
- content: Optional[str] = None,
- username: Optional[str] = None,
- avatar_url: Optional[str] = None,
- embed: Optional[Embed] = None,
- wait: Optional[bool] = False
+ content: str | None = None,
+ username: str | None = None,
+ avatar_url: str | None = None,
+ embed: Embed | None = None,
+ wait: bool | None = False
) -> discord.Message:
"""
Send a message using the provided webhook.
diff --git a/botstrap.py b/botstrap.py
index ccf6993f5..c57a254a3 100644
--- a/botstrap.py
+++ b/botstrap.py
@@ -204,7 +204,7 @@ with DiscordClient(guild_id=GUILD_ID) as discord_client:
create_help_channel = False
if create_help_channel:
- python_help_channel_name = PYTHON_HELP_CHANNEL_NAME.replace('_', '-')
+ python_help_channel_name = PYTHON_HELP_CHANNEL_NAME.replace("_", "-")
python_help_category_id = all_categories[PYTHON_HELP_CATEGORY_NAME]
python_help_channel_id = discord_client.create_forum_channel(python_help_channel_name, python_help_category_id)
all_channels[PYTHON_HELP_CHANNEL_NAME] = python_help_channel_id
diff --git a/poetry.lock b/poetry.lock
index 4c0d616af..757d5219c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand.
[[package]]
name = "aiodns"
@@ -607,19 +607,19 @@ testing = ["pre-commit"]
[[package]]
name = "fakeredis"
-version = "2.10.2"
+version = "2.10.3"
description = "Fake implementation of redis API for testing purposes."
category = "main"
optional = false
-python-versions = ">=3.8,<4.0"
+python-versions = ">=3.7,<4.0"
files = [
- {file = "fakeredis-2.10.2-py3-none-any.whl", hash = "sha256:6377c27bc557be46089381d43fd670aece46672d091a494f73ab4c96c34022b3"},
- {file = "fakeredis-2.10.2.tar.gz", hash = "sha256:e2a95fbda7b11188c117d68b0f9eecc00600cb449ccf3362a15fc03cf9e2477d"},
+ {file = "fakeredis-2.10.3-py3-none-any.whl", hash = "sha256:078ad729fe7cbcc84c9ff6f25c0e503fd4e19db6956f78049f9991b10c5271ba"},
+ {file = "fakeredis-2.10.3.tar.gz", hash = "sha256:c5dcb070ef3219226e1d6db8836ddad47da1fc821270f6e89cfeb5da1f7f2e38"},
]
[package.dependencies]
lupa = {version = ">=1.14,<2.0", optional = true, markers = "extra == \"lua\""}
-redis = ">=4,<5"
+redis = ">=4"
sortedcontainers = ">=2.4,<3.0"
[package.extras]
@@ -643,152 +643,21 @@ sgmllib3k = "*"
[[package]]
name = "filelock"
-version = "3.10.7"
+version = "3.11.0"
description = "A platform independent file lock."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "filelock-3.10.7-py3-none-any.whl", hash = "sha256:bde48477b15fde2c7e5a0713cbe72721cb5a5ad32ee0b8f419907960b9d75536"},
- {file = "filelock-3.10.7.tar.gz", hash = "sha256:892be14aa8efc01673b5ed6589dbccb95f9a8596f0507e232626155495c18105"},
+ {file = "filelock-3.11.0-py3-none-any.whl", hash = "sha256:f08a52314748335c6460fc8fe40cd5638b85001225db78c2aa01c8c0db83b318"},
+ {file = "filelock-3.11.0.tar.gz", hash = "sha256:3618c0da67adcc0506b015fd11ef7faf1b493f0b40d87728e19986b536890c37"},
]
[package.extras]
-docs = ["furo (>=2022.12.7)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
+docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.2.2)", "diff-cover (>=7.5)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
[[package]]
-name = "flake8"
-version = "6.0.0"
-description = "the modular source code checker: pep8 pyflakes and co"
-category = "dev"
-optional = false
-python-versions = ">=3.8.1"
-files = [
- {file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"},
- {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"},
-]
-
-[package.dependencies]
-mccabe = ">=0.7.0,<0.8.0"
-pycodestyle = ">=2.10.0,<2.11.0"
-pyflakes = ">=3.0.0,<3.1.0"
-
-[[package]]
-name = "flake8-annotations"
-version = "3.0.0"
-description = "Flake8 Type Annotation Checks"
-category = "dev"
-optional = false
-python-versions = ">=3.8.1,<4.0.0"
-files = [
- {file = "flake8_annotations-3.0.0-py3-none-any.whl", hash = "sha256:ea927d31016515e9aa6e256651d74baeeee6fa4ad3f8383715ec5c0460a4c225"},
- {file = "flake8_annotations-3.0.0.tar.gz", hash = "sha256:88c8b35a0db10b9a92be69ed3f81494509a18db1c3162551e57bc0fc35fab065"},
-]
-
-[package.dependencies]
-attrs = ">=21.4"
-flake8 = ">=5.0"
-
-[[package]]
-name = "flake8-bugbear"
-version = "23.3.23"
-description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle."
-category = "dev"
-optional = false
-python-versions = ">=3.8.1"
-files = [
- {file = "flake8-bugbear-23.3.23.tar.gz", hash = "sha256:ea565bdb87b96b56dc499edd6cc3ba7f695373d902a5f56c989b74fad7c7719d"},
- {file = "flake8_bugbear-23.3.23-py3-none-any.whl", hash = "sha256:8a218d13abd6904800970381057ce8e72cde8eea743240c4ef3ede4dd0bc9cfb"},
-]
-
-[package.dependencies]
-attrs = ">=19.2.0"
-flake8 = ">=6.0.0"
-
-[package.extras]
-dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "pytest", "tox"]
-
-[[package]]
-name = "flake8-docstrings"
-version = "1.7.0"
-description = "Extension for flake8 which uses pydocstyle to check docstrings"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"},
- {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"},
-]
-
-[package.dependencies]
-flake8 = ">=3"
-pydocstyle = ">=2.1"
-
-[[package]]
-name = "flake8-isort"
-version = "6.0.0"
-description = "flake8 plugin that integrates isort ."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "flake8-isort-6.0.0.tar.gz", hash = "sha256:537f453a660d7e903f602ecfa36136b140de279df58d02eb1b6a0c84e83c528c"},
- {file = "flake8_isort-6.0.0-py3-none-any.whl", hash = "sha256:aa0cac02a62c7739e370ce6b9c31743edac904bae4b157274511fc8a19c75bbc"},
-]
-
-[package.dependencies]
-flake8 = "*"
-isort = ">=5.0.0,<6"
-
-[package.extras]
-test = ["pytest"]
-
-[[package]]
-name = "flake8-string-format"
-version = "0.3.0"
-description = "string format checker, plugin for flake8"
-category = "dev"
-optional = false
-python-versions = "*"
-files = [
- {file = "flake8-string-format-0.3.0.tar.gz", hash = "sha256:65f3da786a1461ef77fca3780b314edb2853c377f2e35069723348c8917deaa2"},
- {file = "flake8_string_format-0.3.0-py2.py3-none-any.whl", hash = "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af"},
-]
-
-[package.dependencies]
-flake8 = "*"
-
-[[package]]
-name = "flake8-tidy-imports"
-version = "4.8.0"
-description = "A flake8 plugin that helps you write tidier imports."
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "flake8-tidy-imports-4.8.0.tar.gz", hash = "sha256:df44f9c841b5dfb3a7a1f0da8546b319d772c2a816a1afefcce43e167a593d83"},
- {file = "flake8_tidy_imports-4.8.0-py3-none-any.whl", hash = "sha256:25bd9799358edefa0e010ce2c587b093c3aba942e96aeaa99b6d0500ae1bf09c"},
-]
-
-[package.dependencies]
-flake8 = ">=3.8.0"
-
-[[package]]
-name = "flake8-todo"
-version = "0.7"
-description = "TODO notes checker, plugin for flake8"
-category = "dev"
-optional = false
-python-versions = "*"
-files = [
- {file = "flake8-todo-0.7.tar.gz", hash = "sha256:6e4c5491ff838c06fe5a771b0e95ee15fc005ca57196011011280fc834a85915"},
-]
-
-[package.dependencies]
-pycodestyle = ">=2.0.0,<3.0.0"
-
-[[package]]
name = "frozenlist"
version = "1.3.3"
description = "A list-like structure which implements collections.abc.MutableSequence"
@@ -886,14 +755,14 @@ files = [
[[package]]
name = "httpcore"
-version = "0.16.3"
+version = "0.17.0"
description = "A minimal low-level HTTP client."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
- {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"},
- {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"},
+ {file = "httpcore-0.17.0-py3-none-any.whl", hash = "sha256:0fdfea45e94f0c9fd96eab9286077f9ff788dd186635ae61b312693e4d943599"},
+ {file = "httpcore-0.17.0.tar.gz", hash = "sha256:cc045a3241afbf60ce056202301b4d8b6af08845e3294055eb26b09913ef903c"},
]
[package.dependencies]
@@ -908,25 +777,25 @@ socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "httpx"
-version = "0.23.3"
+version = "0.24.0"
description = "The next generation HTTP client."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
- {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"},
- {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"},
+ {file = "httpx-0.24.0-py3-none-any.whl", hash = "sha256:447556b50c1921c351ea54b4fe79d91b724ed2b027462ab9a329465d147d5a4e"},
+ {file = "httpx-0.24.0.tar.gz", hash = "sha256:507d676fc3e26110d41df7d35ebd8b3b8585052450f4097401c9be59d928c63e"},
]
[package.dependencies]
certifi = "*"
-httpcore = ">=0.15.0,<0.17.0"
-rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
+httpcore = ">=0.15.0,<0.18.0"
+idna = "*"
sniffio = "*"
[package.extras]
brotli = ["brotli", "brotlicffi"]
-cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"]
+cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (>=1.0.0,<2.0.0)"]
@@ -1197,18 +1066,6 @@ beautifulsoup4 = ">=4.9,<5"
six = ">=1.15,<2"
[[package]]
-name = "mccabe"
-version = "0.7.0"
-description = "McCabe checker, plugin for flake8"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
- {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
-]
-
-[[package]]
name = "more-itertools"
version = "9.1.0"
description = "More routines for operating on iterables, beyond itertools"
@@ -1359,21 +1216,6 @@ files = [
]
[[package]]
-name = "pep8-naming"
-version = "0.13.3"
-description = "Check PEP-8 naming conventions, plugin for flake8"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "pep8-naming-0.13.3.tar.gz", hash = "sha256:1705f046dfcd851378aac3be1cd1551c7c1e5ff363bacad707d43007877fa971"},
- {file = "pep8_naming-0.13.3-py3-none-any.whl", hash = "sha256:1a86b8c71a03337c97181917e2b472f0f5e4ccb06844a0d6f0a33522549e7a80"},
-]
-
-[package.dependencies]
-flake8 = ">=5.0.0"
-
-[[package]]
name = "pip-licenses"
version = "4.1.0"
description = "Dump the software license list of Python packages installed with pip."
@@ -1444,14 +1286,14 @@ virtualenv = ">=20.10.0"
[[package]]
name = "prettytable"
-version = "3.6.0"
+version = "3.7.0"
description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
- {file = "prettytable-3.6.0-py3-none-any.whl", hash = "sha256:3b767129491767a3a5108e6f305cbaa650f8020a7db5dfe994a2df7ef7bad0fe"},
- {file = "prettytable-3.6.0.tar.gz", hash = "sha256:2e0026af955b4ea67b22122f310b90eae890738c08cb0458693a49b6221530ac"},
+ {file = "prettytable-3.7.0-py3-none-any.whl", hash = "sha256:f4aaf2ed6e6062a82fd2e6e5289bbbe705ec2788fe401a3a1f62a1cea55526d2"},
+ {file = "prettytable-3.7.0.tar.gz", hash = "sha256:ef8334ee40b7ec721651fc4d37ecc7bb2ef55fde5098d994438f0dfdaa385c0c"},
]
[package.dependencies]
@@ -1556,18 +1398,6 @@ cffi = ">=1.5.0"
idna = ["idna (>=2.1)"]
[[package]]
-name = "pycodestyle"
-version = "2.10.0"
-description = "Python style guide checker"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"},
- {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"},
-]
-
-[[package]]
name = "pycparser"
version = "2.21"
description = "C parser in Python"
@@ -1655,36 +1485,6 @@ statsd = "4.0.1"
async-rediscache = ["async-rediscache[fakeredis] (==1.0.0rc2)"]
[[package]]
-name = "pydocstyle"
-version = "6.3.0"
-description = "Python docstring style checker"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"},
- {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"},
-]
-
-[package.dependencies]
-snowballstemmer = ">=2.2.0"
-
-[package.extras]
-toml = ["tomli (>=1.2.3)"]
-
-[[package]]
-name = "pyflakes"
-version = "3.0.1"
-description = "passive checker of Python programs"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-files = [
- {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"},
- {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"},
-]
-
-[[package]]
name = "pyreadline3"
version = "3.4.1"
description = "A python implementation of GNU readline."
@@ -1698,25 +1498,24 @@ files = [
[[package]]
name = "pytest"
-version = "7.2.2"
+version = "7.3.0"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
- {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"},
- {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"},
+ {file = "pytest-7.3.0-py3-none-any.whl", hash = "sha256:933051fa1bfbd38a21e73c3960cebdad4cf59483ddba7696c48509727e17f201"},
+ {file = "pytest-7.3.0.tar.gz", hash = "sha256:58ecc27ebf0ea643ebfdf7fb1249335da761a00c9f955bcd922349bcb68ee57d"},
]
[package.dependencies]
-attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
[package.extras]
-testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
+testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
[[package]]
name = "pytest-cov"
@@ -1875,101 +1674,104 @@ files = [
[[package]]
name = "rapidfuzz"
-version = "2.15.0"
+version = "2.15.1"
description = "rapid fuzzy string matching"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "rapidfuzz-2.15.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3a610c7c1711a382b330c0e0910c981dd1cd398b135bc2e29219e685685d7afd"},
- {file = "rapidfuzz-2.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e51d41689f551b4d1f678be2b6dd6e1cf87c961b8899bdb96a048491234354dc"},
- {file = "rapidfuzz-2.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:824cf194bb50863f1ff6de6f1aa04693bbb9743981dcdc35a98549c6bf829d01"},
- {file = "rapidfuzz-2.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb7a110b439ba3ee4986d19234e6ef00b36a5f8e9747896c24498fa23e684514"},
- {file = "rapidfuzz-2.15.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b8dcf09453b0b0f4dc64efb35148cab8a0fb6c466d34e5cefd96ca6ab7fdb4e"},
- {file = "rapidfuzz-2.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2f24708f6906b0883669c9eb4e67d0f65519f03530bae82b2b277ef62ec46ac7"},
- {file = "rapidfuzz-2.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9681f9e4d19307666a84f6c0f3706b22d35eeaeeab07ac356b1393b00f97cac"},
- {file = "rapidfuzz-2.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2892b50be613d1458a85106e0c1a21a9e8fd317e24028e8fae61be022870c9cd"},
- {file = "rapidfuzz-2.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1b49947c5f0c7543fdab825e9375edcd407f85250d077e0a404844961d888c9b"},
- {file = "rapidfuzz-2.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ec5324f6fda41e72f49830cb0b8d124b9431c2e3d4928fb0bd28d461dd6657d4"},
- {file = "rapidfuzz-2.15.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e23cec8f517f8dcd3fc7f13d2793616b92b1b2fd36c3759c92758f8166ecd154"},
- {file = "rapidfuzz-2.15.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:420f4544bf230835e39786f55542d75108015a27dfd94779440cffb08d3762c8"},
- {file = "rapidfuzz-2.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:550515f4a2e7051bbae343851a9ec3adc7edb1656b181b2a8ee571ae7fe8a21e"},
- {file = "rapidfuzz-2.15.0-cp310-cp310-win32.whl", hash = "sha256:632d5473ba52da7fa71573c460d5fef470b3ec6d560348a07f97f2860b16f791"},
- {file = "rapidfuzz-2.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:ae44dc7a350deaf92d313369b46280b787e52b99103437c46002ce29b3ba85eb"},
- {file = "rapidfuzz-2.15.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f4107e92744bd1fd83fd38058aee3858893a4ab5b4eab76a758eb13804c38692"},
- {file = "rapidfuzz-2.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:666bad2454b2fabd14e397fd467edc95e57f6324eb8bfc8c13f962732a4cba4e"},
- {file = "rapidfuzz-2.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:03aa67c2eaf33391317598ea688a6cb522a9823c8d8a8eee9c85dc60b6fcbbc8"},
- {file = "rapidfuzz-2.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f22a9f8d97766e18179ddc3251027ef346177335518826592d6e2862c811a4c7"},
- {file = "rapidfuzz-2.15.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:313f3609fe37153d50436884d852aee3a56ac41734c807718d8453bd3c243565"},
- {file = "rapidfuzz-2.15.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2fb5bac25bb5b0d6833f7b740292651759dab870f0487911def46214526f5dc9"},
- {file = "rapidfuzz-2.15.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bc26a506c758bed4bf5b43b90a8c79460e76e28db97330cb6640287468d575d"},
- {file = "rapidfuzz-2.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c5029ddb65ae980bcb87b9790df226105266f7c794b20cb32793b4865564e01"},
- {file = "rapidfuzz-2.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f393b18d411b590309693e4106fab09dc692f564e6fea4a744a33754f7b6a37"},
- {file = "rapidfuzz-2.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8d7e73e36fac74877b8a4700e60c9d699eabd48f7fd37419eb5f8124ed023273"},
- {file = "rapidfuzz-2.15.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b3a125e32831a4370096903f0c2cc1314cf2ceae3af4431ac4885e53984201a5"},
- {file = "rapidfuzz-2.15.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:c32d41dce29d0d14a393de443a1980001bf341b2dc977fab73cbb46be3beb10f"},
- {file = "rapidfuzz-2.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fb4b8e3a52f2f28944499ab6b95817419e482b48cd11d776034ea4c98ea9e33f"},
- {file = "rapidfuzz-2.15.0-cp311-cp311-win32.whl", hash = "sha256:4cd63b1debe72535d7b72de98c50571859954ffb3e5ffc5b0869feb29c407013"},
- {file = "rapidfuzz-2.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:06b65e724cdca4eda6a639d491339445f140d88a598bc0eb98be80147003dc26"},
- {file = "rapidfuzz-2.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:12b1d47eeb04b261f686bf29e4443807f8f953a0918aa5cc3ff1f4d3d48c64a5"},
- {file = "rapidfuzz-2.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ad14814b68719f1e31d03751fa0dae5b30012c56a5155959443030134616eb4"},
- {file = "rapidfuzz-2.15.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2886b2bf740353eace1a942844df2b6bbcfca69717cb4aac033681983e306856"},
- {file = "rapidfuzz-2.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d31c7ba453a811bfdcd802f1a4703a21d7301ccdb91d81d92093eaceafb14a30"},
- {file = "rapidfuzz-2.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24113e4afa1a6ca810f969ab996146bdbdcd338b35f115f935ae63d6b8d2aa75"},
- {file = "rapidfuzz-2.15.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56d63cc530b49ce0afb1aadf3d11bb0f52220a221e799715f63a8b77dea152cf"},
- {file = "rapidfuzz-2.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:210af56001533ed1d8c7d5d0e57081877ba35a9391fb36d0a606693b0bd15d49"},
- {file = "rapidfuzz-2.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9bc3f66189967b1504f617c09b295b6a8ad3a34a63e713c5553068bceb21c351"},
- {file = "rapidfuzz-2.15.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:367f70a0c8bb19684c38973fe54888bd9179e991253547c4ee116a17f3d44319"},
- {file = "rapidfuzz-2.15.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5461e4cb215989c52872a886a1217e08af5de4c565c8bf356ab0f331dcfb8763"},
- {file = "rapidfuzz-2.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b97bb1b3e32700e6bce2036c9435532a6dc45a5df8af8a7b842038b2eeaf3f9d"},
- {file = "rapidfuzz-2.15.0-cp37-cp37m-win32.whl", hash = "sha256:d9ec6429508ab1f2b752163970f26f4a179746c34c4862d3b3c2410be09d4fea"},
- {file = "rapidfuzz-2.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:be0b533a3909c1db82e7a3c03e533374c71441dded616b71f222c4edd0058a52"},
- {file = "rapidfuzz-2.15.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6c446c31468da2a08ec874baaeada06f3af6cede2b3010a2f0fccc5a95c3997d"},
- {file = "rapidfuzz-2.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:577620e28f2b407231f293c58b24b1a7861ddd8092b7e6c6ba34b9945b5aa0a5"},
- {file = "rapidfuzz-2.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:43e397ba21f6a53c6982c8ad0aae1ed5375d3e12089a9ede6c63c0fbffdb5354"},
- {file = "rapidfuzz-2.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6efe5d6b97366dc0c0ec575212441ccaebec5c7669a0a91f4dca5e751a6cace8"},
- {file = "rapidfuzz-2.15.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c63637a74e00913c59bad6988cdf247c7ddc07b4f52cb4a3b15f08ebf90c2556"},
- {file = "rapidfuzz-2.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5103b0be98271131ea992c62544b788f9afb90bbc716e5799b660dbca7b2959d"},
- {file = "rapidfuzz-2.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a98614ad176a7ee719e3f30313f910a79ce17adbeea1f06bd4a1c5c997af762"},
- {file = "rapidfuzz-2.15.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fecb07d87739912153b532bc1b5edbe3ecdf32a20e219d9bb02ef411d4c7638"},
- {file = "rapidfuzz-2.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9662c286e493b7dc8e05363c63284889874097d6a594200702dd5703f76ad310"},
- {file = "rapidfuzz-2.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b7717d489b65199a61e32cc87ad2e0a21b93b11de5f0873c91bcb77bfccda1cd"},
- {file = "rapidfuzz-2.15.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:d10bb6fbf7e1f4f43a3b5a15e4cae28e52ade397704fa0f4566cf651ac23897e"},
- {file = "rapidfuzz-2.15.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:6d0734d368eb6ab131d2644e0119a2d9164be9670de493391694ff647902f4ac"},
- {file = "rapidfuzz-2.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5ef94ed9e3b371f935643cffb967a8090bd7c08e8a4a62523ffbc108bb57b8df"},
- {file = "rapidfuzz-2.15.0-cp38-cp38-win32.whl", hash = "sha256:520865826b038ebf8e099bc7e58e424be14173c8ec72f41944f35c5190761a0c"},
- {file = "rapidfuzz-2.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:bcfcf5ea72fe3bbdc7b7e716a1eca37dd610ab95fb1d8095ec274c9ebe2ebc5a"},
- {file = "rapidfuzz-2.15.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e411ed96be9c4dacfbec3b8bd6873b18fa012da11ab544df32b8855b163a3317"},
- {file = "rapidfuzz-2.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c2d014e8ca0b7a1e67ca9ee68ab04aa3524134dda33454a33b94404a0f67cfc2"},
- {file = "rapidfuzz-2.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6580b5e837cd7776b454608caa62e9745be20c2c5f38e3d25aeca1f4ba7f125e"},
- {file = "rapidfuzz-2.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cdafc456bd327fef05922a73b16ab9ccfdd7108456886456a119517d7c34292"},
- {file = "rapidfuzz-2.15.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e8ef7ec8f3b9bdcf4b436b2bcd11fff5d1157404bc7bb501f51d7bfc85b7171"},
- {file = "rapidfuzz-2.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9790024ef1bde76f62989b59131f17fa6c4bea0f16850aa79774225a079c243f"},
- {file = "rapidfuzz-2.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00a960016fd801956f619f9c926bf72b8b8010e9b12dee2220357d59d9e4116e"},
- {file = "rapidfuzz-2.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d9ddb396914fa807179073d9f8c576376bbda34d52d699c5a41327938d4e1f"},
- {file = "rapidfuzz-2.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2a9daf38dd701ce778cf9f5da7c1abc3a2d327d1106bc0d73fe2a33dbfa846f4"},
- {file = "rapidfuzz-2.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2f6b79bff288d9eb59c6289bd0c92f876e241badfcd205b8087e6140b30a2b36"},
- {file = "rapidfuzz-2.15.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:b48fb352b5d4436878a9b7062a648fc7fde77948ccd6fba454fe16c4ee367feb"},
- {file = "rapidfuzz-2.15.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:830138acb8f57005f37ceec6dc74cd05482c5989c8ca4dba77883dd213039828"},
- {file = "rapidfuzz-2.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:50817a5ce7c8e05434d4a40ff23dfb208a91f622af7fb41325a6dfeffcc0b3a8"},
- {file = "rapidfuzz-2.15.0-cp39-cp39-win32.whl", hash = "sha256:a5c6b502600a3e33b536971989e336d1e1ec19c9acbcdc7ea606ea20061134a4"},
- {file = "rapidfuzz-2.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:c5cacf2f1d1b5e79b091717393a50c9b24b703bca9c84d35c942c188ced67910"},
- {file = "rapidfuzz-2.15.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:76f0248da712ea4abed9e6962873d41cc5fb13777455b4c811b3f9a853fd7e28"},
- {file = "rapidfuzz-2.15.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1f0cb2352b8e64e1804db4c0d91963161d54ce174b1b5575d15da1faf4aace9"},
- {file = "rapidfuzz-2.15.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4f618620253849bb9542a10d23f4c2fc8ac0e06fb485be14312fd494cf48751"},
- {file = "rapidfuzz-2.15.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4fd9e5d7a09a60cb8cd0af2ff4abcbd5d8e9ea17304344a03f06cfbe4909b0"},
- {file = "rapidfuzz-2.15.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d6ca92a5dfd9c97e8d2652ab56ab041d118e0ddf3009f7ae2cb9de7a19688b5d"},
- {file = "rapidfuzz-2.15.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f04ac54dba649d3238295c2ff3a01cb9b5bfeb856d375253fd4f6ae2c0152a39"},
- {file = "rapidfuzz-2.15.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f01d4c96824721e8292182d3270178021ff3d6065598e74fec5cf4b1e794fb2"},
- {file = "rapidfuzz-2.15.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b6822b7be49785664cb72ce9ae8df67578154f6a8cf69e5be5ce7c25580a157"},
- {file = "rapidfuzz-2.15.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c412ede384f3f305a202de4ef5f5b57394b41e213e77df97a33fd45f5837d854"},
- {file = "rapidfuzz-2.15.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0f570d61288b35bf2ff94f6d60c364fe3a78fc7ae71e7b149daeb2bc38dad1c7"},
- {file = "rapidfuzz-2.15.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:62d7fa522c56908cbc9e9ead2a52f19ac818cd525474639ec8ee5eb2adab90df"},
- {file = "rapidfuzz-2.15.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:462852ef50dced87e1e83ad92520b993afba4e7a4e6e93dbdfcdda449124df9e"},
- {file = "rapidfuzz-2.15.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:766846a4df99c473d4426e51934421f767db07fa3a4e4d921bb5258a9f8b9a75"},
- {file = "rapidfuzz-2.15.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fee1e39cdf8361aed89bdcf9bcdcdd47bd3714f16611fcfca46f408d983a661"},
- {file = "rapidfuzz-2.15.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:684ac35c6480561b4a0067319112907e2405a93cb6a228de8214b6a3e86556fd"},
- {file = "rapidfuzz-2.15.0.tar.gz", hash = "sha256:1c7e439d1428882d297bdd0db5626fc4626cdebe50d3fbbf4ed898f775ca56d5"},
+ {file = "rapidfuzz-2.15.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fc0bc259ebe3b93e7ce9df50b3d00e7345335d35acbd735163b7c4b1957074d3"},
+ {file = "rapidfuzz-2.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d59fb3a410d253f50099d7063855c2b95df1ef20ad93ea3a6b84115590899f25"},
+ {file = "rapidfuzz-2.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c525a3da17b6d79d61613096c8683da86e3573e807dfaecf422eea09e82b5ba6"},
+ {file = "rapidfuzz-2.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4deae6a918ecc260d0c4612257be8ba321d8e913ccb43155403842758c46fbe"},
+ {file = "rapidfuzz-2.15.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2577463d10811386e704a3ab58b903eb4e2a31b24dfd9886d789b0084d614b01"},
+ {file = "rapidfuzz-2.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f67d5f56aa48c0da9de4ab81bffb310683cf7815f05ea38e5aa64f3ba4368339"},
+ {file = "rapidfuzz-2.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7927722ff43690e52b3145b5bd3089151d841d350c6f8378c3cfac91f67573a"},
+ {file = "rapidfuzz-2.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6534afc787e32c4104f65cdeb55f6abe4d803a2d0553221d00ef9ce12788dcde"},
+ {file = "rapidfuzz-2.15.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d0ae6ec79a1931929bb9dd57bc173eb5ba4c7197461bf69e3a34b6dd314feed2"},
+ {file = "rapidfuzz-2.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:be7ccc45c4d1a7dfb595f260e8022a90c6cb380c2a346ee5aae93f85c96d362b"},
+ {file = "rapidfuzz-2.15.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:8ba013500a2b68c64b2aecc5fb56a2dad6c2872cf545a0308fd044827b6e5f6a"},
+ {file = "rapidfuzz-2.15.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4d9f7d10065f657f960b48699e7dddfce14ab91af4bab37a215f0722daf0d716"},
+ {file = "rapidfuzz-2.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7e24a1b802cea04160b3fccd75d2d0905065783ebc9de157d83c14fb9e1c6ce2"},
+ {file = "rapidfuzz-2.15.1-cp310-cp310-win32.whl", hash = "sha256:dffdf03499e0a5b3442951bb82b556333b069e0661e80568752786c79c5b32de"},
+ {file = "rapidfuzz-2.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d150d90a7c6caae7962f29f857a4e61d42038cfd82c9df38508daf30c648ae7"},
+ {file = "rapidfuzz-2.15.1-cp310-cp310-win_arm64.whl", hash = "sha256:87c30e9184998ff6eb0fa9221f94282ce7c908fd0da96a1ef66ecadfaaa4cdb7"},
+ {file = "rapidfuzz-2.15.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6986413cb37035eb796e32f049cbc8c13d8630a4ac1e0484e3e268bb3662bd1b"},
+ {file = "rapidfuzz-2.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a72f26e010d4774b676f36e43c0fc8a2c26659efef4b3be3fd7714d3491e9957"},
+ {file = "rapidfuzz-2.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b5cd54c98a387cca111b3b784fc97a4f141244bbc28a92d4bde53f164464112e"},
+ {file = "rapidfuzz-2.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da7fac7c3da39f93e6b2ebe386ed0ffe1cefec91509b91857f6e1204509e931f"},
+ {file = "rapidfuzz-2.15.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f976e76ac72f650790b3a5402431612175b2ac0363179446285cb3c901136ca9"},
+ {file = "rapidfuzz-2.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:abde47e1595902a490ed14d4338d21c3509156abb2042a99e6da51f928e0c117"},
+ {file = "rapidfuzz-2.15.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca8f1747007a3ce919739a60fa95c5325f7667cccf6f1c1ef18ae799af119f5e"},
+ {file = "rapidfuzz-2.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c35da09ab9797b020d0d4f07a66871dfc70ea6566363811090353ea971748b5a"},
+ {file = "rapidfuzz-2.15.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a3a769ca7580686a66046b77df33851b3c2d796dc1eb60c269b68f690f3e1b65"},
+ {file = "rapidfuzz-2.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d50622efefdb03a640a51a6123748cd151d305c1f0431af762e833d6ffef71f0"},
+ {file = "rapidfuzz-2.15.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b7461b0a7651d68bc23f0896bffceea40f62887e5ab8397bf7caa883592ef5cb"},
+ {file = "rapidfuzz-2.15.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:074ee9e17912e025c72a5780ee4c7c413ea35cd26449719cc399b852d4e42533"},
+ {file = "rapidfuzz-2.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7025fb105a11f503943f17718cdb8241ea3bb4d812c710c609e69bead40e2ff0"},
+ {file = "rapidfuzz-2.15.1-cp311-cp311-win32.whl", hash = "sha256:2084d36b95139413cef25e9487257a1cc892b93bd1481acd2a9656f7a1d9930c"},
+ {file = "rapidfuzz-2.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:5a738fcd24e34bce4b19126b92fdae15482d6d3a90bd687fd3d24ce9d28ce82d"},
+ {file = "rapidfuzz-2.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:dc3cafa68cfa54638632bdcadf9aab89a3d182b4a3f04d2cad7585ed58ea8731"},
+ {file = "rapidfuzz-2.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3c53d57ba7a88f7bf304d4ea5a14a0ca112db0e0178fff745d9005acf2879f7d"},
+ {file = "rapidfuzz-2.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6ee758eec4cf2215dc8d8eafafcea0d1f48ad4b0135767db1b0f7c5c40a17dd"},
+ {file = "rapidfuzz-2.15.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d93ba3ae59275e7a3a116dac4ffdb05e9598bf3ee0861fecc5b60fb042d539e"},
+ {file = "rapidfuzz-2.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c3ff75e647908ddbe9aa917fbe39a112d5631171f3fcea5809e2363e525a59d"},
+ {file = "rapidfuzz-2.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d89c421702474c6361245b6b199e6e9783febacdbfb6b002669e6cb3ef17a09"},
+ {file = "rapidfuzz-2.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f69e6199fec0f58f9a89afbbaea78d637c7ce77f656a03a1d6ea6abdc1d44f8"},
+ {file = "rapidfuzz-2.15.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:41dfea282844d0628279b4db2929da0dacb8ac317ddc5dcccc30093cf16357c1"},
+ {file = "rapidfuzz-2.15.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2dd03477feefeccda07b7659dd614f6738cfc4f9b6779dd61b262a73b0a9a178"},
+ {file = "rapidfuzz-2.15.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:5efe035aa76ff37d1b5fa661de3c4b4944de9ff227a6c0b2e390a95c101814c0"},
+ {file = "rapidfuzz-2.15.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ed2cf7c69102c7a0a06926d747ed855bc836f52e8d59a5d1e3adfd980d1bd165"},
+ {file = "rapidfuzz-2.15.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a0e441d4c2025110ec3eba5d54f11f78183269a10152b3a757a739ffd1bb12bf"},
+ {file = "rapidfuzz-2.15.1-cp37-cp37m-win32.whl", hash = "sha256:a4a54efe17cc9f53589c748b53f28776dfdfb9bc83619685740cb7c37985ac2f"},
+ {file = "rapidfuzz-2.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:bb8318116ecac4dfb84841d8b9b461f9bb0c3be5b616418387d104f72d2a16d1"},
+ {file = "rapidfuzz-2.15.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e9296c530e544f68858c3416ad1d982a1854f71e9d2d3dcedb5b216e6d54f067"},
+ {file = "rapidfuzz-2.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:49c4bcdb9238f11f8c4eba1b898937f09b92280d6f900023a8216008f299b41a"},
+ {file = "rapidfuzz-2.15.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb40a279e134bb3fef099a8b58ed5beefb201033d29bdac005bddcdb004ef71"},
+ {file = "rapidfuzz-2.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7381c11cb590bbd4e6f2d8779a0b34fdd2234dfa13d0211f6aee8ca166d9d05"},
+ {file = "rapidfuzz-2.15.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfdcdedfd12a0077193f2cf3626ff6722c5a184adf0d2d51f1ec984bf21c23c3"},
+ {file = "rapidfuzz-2.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85bece1ec59bda8b982bd719507d468d4df746dfb1988df11d916b5e9fe19e8"},
+ {file = "rapidfuzz-2.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1b393f4a1eaa6867ffac6aef58cfb04bab2b3d7d8e40b9fe2cf40dd1d384601"},
+ {file = "rapidfuzz-2.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53de456ef020a77bf9d7c6c54860a48e2e902584d55d3001766140ac45c54bc7"},
+ {file = "rapidfuzz-2.15.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2492330bc38b76ed967eab7bdaea63a89b6ceb254489e2c65c3824efcbf72993"},
+ {file = "rapidfuzz-2.15.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:099e4c6befaa8957a816bdb67ce664871f10aaec9bebf2f61368cf7e0869a7a1"},
+ {file = "rapidfuzz-2.15.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:46599b2ad4045dd3f794a24a6db1e753d23304699d4984462cf1ead02a51ddf3"},
+ {file = "rapidfuzz-2.15.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:591f19d16758a3c55c9d7a0b786b40d95599a5b244d6eaef79c7a74fcf5104d8"},
+ {file = "rapidfuzz-2.15.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ed17359061840eb249f8d833cb213942e8299ffc4f67251a6ed61833a9f2ea20"},
+ {file = "rapidfuzz-2.15.1-cp38-cp38-win32.whl", hash = "sha256:aa1e5aad325168e29bf8e17006479b97024aa9d2fdbe12062bd2f8f09080acf8"},
+ {file = "rapidfuzz-2.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:c2bb68832b140c551dbed691290bef4ee6719d4e8ce1b7226a3736f61a9d1a83"},
+ {file = "rapidfuzz-2.15.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3fac40972cf7b6c14dded88ae2331eb50dfbc278aa9195473ef6fc6bfe49f686"},
+ {file = "rapidfuzz-2.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0e456cbdc0abf39352800309dab82fd3251179fa0ff6573fa117f51f4e84be8"},
+ {file = "rapidfuzz-2.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:22b9d22022b9d09fd4ece15102270ab9b6a5cfea8b6f6d1965c1df7e3783f5ff"},
+ {file = "rapidfuzz-2.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46754fe404a9a6f5cbf7abe02d74af390038d94c9b8c923b3f362467606bfa28"},
+ {file = "rapidfuzz-2.15.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91abb8bf7610efe326394adc1d45e1baca8f360e74187f3fa0ef3df80cdd3ba6"},
+ {file = "rapidfuzz-2.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e40a2f60024f9d3c15401e668f732800114a023f3f8d8c40f1521a62081ff054"},
+ {file = "rapidfuzz-2.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a48ee83916401ac73938526d7bd804e01d2a8fe61809df7f1577b0b3b31049a3"},
+ {file = "rapidfuzz-2.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c71580052f9dbac443c02f60484e5a2e5f72ad4351b84b2009fbe345b1f38422"},
+ {file = "rapidfuzz-2.15.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:82b86d5b8c1b9bcbc65236d75f81023c78d06a721c3e0229889ff4ed5c858169"},
+ {file = "rapidfuzz-2.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fc4528b7736e5c30bc954022c2cf410889abc19504a023abadbc59cdf9f37cae"},
+ {file = "rapidfuzz-2.15.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e1e0e569108a5760d8f01d0f2148dd08cc9a39ead79fbefefca9e7c7723c7e88"},
+ {file = "rapidfuzz-2.15.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:94e1c97f0ad45b05003806f8a13efc1fc78983e52fa2ddb00629003acf4676ef"},
+ {file = "rapidfuzz-2.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47e81767a962e41477a85ad7ac937e34d19a7d2a80be65614f008a5ead671c56"},
+ {file = "rapidfuzz-2.15.1-cp39-cp39-win32.whl", hash = "sha256:79fc574aaf2d7c27ec1022e29c9c18f83cdaf790c71c05779528901e0caad89b"},
+ {file = "rapidfuzz-2.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:f3dd4bcef2d600e0aa121e19e6e62f6f06f22a89f82ef62755e205ce14727874"},
+ {file = "rapidfuzz-2.15.1-cp39-cp39-win_arm64.whl", hash = "sha256:cac095cbdf44bc286339a77214bbca6d4d228c9ebae3da5ff6a80aaeb7c35634"},
+ {file = "rapidfuzz-2.15.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b89d1126be65c85763d56e3b47d75f1a9b7c5529857b4d572079b9a636eaa8a7"},
+ {file = "rapidfuzz-2.15.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19b7460e91168229768be882ea365ba0ac7da43e57f9416e2cfadc396a7df3c2"},
+ {file = "rapidfuzz-2.15.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c33c03e7092642c38f8a15ca2d8fc38da366f2526ec3b46adf19d5c7aa48ba"},
+ {file = "rapidfuzz-2.15.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040faca2e26d9dab5541b45ce72b3f6c0e36786234703fc2ac8c6f53bb576743"},
+ {file = "rapidfuzz-2.15.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:6e2a3b23e1e9aa13474b3c710bba770d0dcc34d517d3dd6f97435a32873e3f28"},
+ {file = "rapidfuzz-2.15.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e597b9dfd6dd180982684840975c458c50d447e46928efe3e0120e4ec6f6686"},
+ {file = "rapidfuzz-2.15.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d14752c9dd2036c5f36ebe8db5f027275fa7d6b3ec6484158f83efb674bab84e"},
+ {file = "rapidfuzz-2.15.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558224b6fc6124d13fa32d57876f626a7d6188ba2a97cbaea33a6ee38a867e31"},
+ {file = "rapidfuzz-2.15.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c89cfa88dc16fd8c9bcc0c7f0b0073f7ef1e27cceb246c9f5a3f7004fa97c4d"},
+ {file = "rapidfuzz-2.15.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:509c5b631cd64df69f0f011893983eb15b8be087a55bad72f3d616b6ae6a0f96"},
+ {file = "rapidfuzz-2.15.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0f73a04135a03a6e40393ecd5d46a7a1049d353fc5c24b82849830d09817991f"},
+ {file = "rapidfuzz-2.15.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c99d53138a2dfe8ada67cb2855719f934af2733d726fbf73247844ce4dd6dd5"},
+ {file = "rapidfuzz-2.15.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f01fa757f0fb332a1f045168d29b0d005de6c39ee5ce5d6c51f2563bb53c601b"},
+ {file = "rapidfuzz-2.15.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60368e1add6e550faae65614844c43f8a96e37bf99404643b648bf2dba92c0fb"},
+ {file = "rapidfuzz-2.15.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:785744f1270828cc632c5a3660409dee9bcaac6931a081bae57542c93e4d46c4"},
+ {file = "rapidfuzz-2.15.1.tar.gz", hash = "sha256:d62137c2ca37aea90a11003ad7dc109c8f1739bfbe5a9a217f3cdb07d7ac00f6"},
]
[package.extras]
@@ -2103,23 +1905,32 @@ requests = ">=1.0.0"
six = "*"
[[package]]
-name = "rfc3986"
-version = "1.5.0"
-description = "Validating URI References per RFC 3986"
+name = "ruff"
+version = "0.0.260"
+description = "An extremely fast Python linter, written in Rust."
category = "dev"
optional = false
-python-versions = "*"
+python-versions = ">=3.7"
files = [
- {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
- {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
+ {file = "ruff-0.0.260-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:c559650b623f3fbdc39c7ed1bcb064765c666a53ee738c53d1461afbf3f23db2"},
+ {file = "ruff-0.0.260-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:90ff1479e292a84c388a8a035d223247ddeea5f6760752a9142b88b6d59ac334"},
+ {file = "ruff-0.0.260-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25584d1b9f445fde72651caab97e7430a4c5bfd2a0ce9af39868753826cba10d"},
+ {file = "ruff-0.0.260-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8032e35357384a29791c75194a71e314031171eb0731fcaa872dfaf4c1f4470a"},
+ {file = "ruff-0.0.260-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e4fa7293f97c021825b3b72f2bf53f0eb4f59625608a889678c1fc6660f412d"},
+ {file = "ruff-0.0.260-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8bec0271e2c8cd36bcf915cb9f6a93e40797a3ff3d2cda4ca87b7bed9e598472"},
+ {file = "ruff-0.0.260-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e075a61aaff8ebe56172217f0ac14c5df9637b289bf161ac697445a9003d5c2"},
+ {file = "ruff-0.0.260-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8678f54eb2696481618902a10c3cb28325f3323799af99997ad6f06005ea4f5"},
+ {file = "ruff-0.0.260-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57d9f0bfdef739b76aa3112b9182a214f0f34589a2659f88353492c7670fe2fe"},
+ {file = "ruff-0.0.260-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ec1f77219ba5adaa194289cb82ba924ff2ed931fd00b8541d66a1724c89fbc9"},
+ {file = "ruff-0.0.260-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aae2170a7ec6f7fc4a73db30aa7aa7fce936176bf66bf85f77f69ddd1dd4a665"},
+ {file = "ruff-0.0.260-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f847b72ef994ab88e9da250c7eb5cbb3f1555b92a9f22c5ed1c27a44b7e98d6"},
+ {file = "ruff-0.0.260-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6dd705d4eff405c2b70513188fbff520f49db6df91f0d5e8258c5d469efa58bc"},
+ {file = "ruff-0.0.260-py3-none-win32.whl", hash = "sha256:3866a96b2ef92c7d837ba6bf8fc9dd125a67886f1c5512ad6fa5d5fefaceff87"},
+ {file = "ruff-0.0.260-py3-none-win_amd64.whl", hash = "sha256:0733d524946decbd4f1e63f7dc26820f5c1e6c31da529ba20fb995057f8e79b1"},
+ {file = "ruff-0.0.260-py3-none-win_arm64.whl", hash = "sha256:12542a26f189a5a10c719bfa14d415d0511ac05e5c9ff5e79cc9d5cc50b81bc8"},
+ {file = "ruff-0.0.260.tar.gz", hash = "sha256:ea8f94262f33b81c47ee9d81f455b144e94776f5c925748cb0c561a12206eae1"},
]
-[package.dependencies]
-idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}
-
-[package.extras]
-idna2008 = ["idna"]
-
[[package]]
name = "sentry-sdk"
version = "1.19.1"
@@ -2215,18 +2026,6 @@ files = [
]
[[package]]
-name = "snowballstemmer"
-version = "2.2.0"
-description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
-category = "dev"
-optional = false
-python-versions = "*"
-files = [
- {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
- {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
-]
-
-[[package]]
name = "sortedcontainers"
version = "2.4.0"
description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
@@ -2463,4 +2262,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "3.11.*"
-content-hash = "40e61bb22bdd3a739eb4d1fbe6b4e375a4b7b905b6b19d7e1e4dfa7268212dee"
+content-hash = "07bee4874189a126a6a6ec92b385a3f316d6713badce548c06d6f0dbdd88a6ec"
diff --git a/pyproject.toml b/pyproject.toml
index 4486bb76b..dc6520391 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -25,7 +25,7 @@ more-itertools = "9.1.0"
python-dateutil = "2.8.2"
python-frontmatter = "1.0.0"
pyyaml = "6.0"
-rapidfuzz = "2.15.0"
+rapidfuzz = "2.15.1"
regex = "2023.3.23"
sentry-sdk = "1.19.1"
tldextract = "3.4.0"
@@ -33,23 +33,16 @@ pydantic = { version = "1.10.7", extras = ["dotenv"]}
[tool.poetry.dev-dependencies]
coverage = "7.2.3"
-flake8 = "6.0.0"
-flake8-annotations = "3.0.0"
-flake8-bugbear = "23.3.23"
-flake8-docstrings = "1.7.0"
-flake8-string-format = "0.3.0"
-flake8-tidy-imports = "4.8.0"
-flake8-todo = "0.7"
-flake8-isort = "6.0.0"
-pep8-naming = "0.13.3"
+httpx = "0.24.0"
+isort = "5.12.0"
pre-commit = "3.2.2"
pip-licenses = "4.1.0"
-pytest = "7.2.2"
+pytest = "7.3.0"
pytest-cov = "4.0.0"
pytest-subtests = "0.10.0"
pytest-xdist = "3.2.1"
+ruff = "0.0.260"
taskipy = "1.10.4"
-httpx = "0.23.3"
[build-system]
@@ -83,6 +76,27 @@ combine_as_imports = true
line_length = 120
atomic = true
+[tool.ruff]
+target-version = "py311"
+extend-exclude = [".cache"]
+ignore = [
+ "ANN002", "ANN003", "ANN101", "ANN102", "ANN204", "ANN206", "ANN401",
+ "B904",
+ "C401", "C408",
+ "D100", "D104", "D105", "D107", "D203", "D212", "D214", "D215", "D301",
+ "D400", "D401", "D402", "D404", "D405", "D406", "D407", "D408", "D409", "D410", "D411", "D412", "D413", "D414", "D416", "D417",
+ "E731",
+ "RET504",
+ "RUF005",
+ "S311",
+ "SIM102", "SIM108",
+]
+line-length = 120
+select = ["ANN", "B", "C4", "D", "DTZ", "E", "F", "ISC", "INT", "N", "PGH", "PIE", "Q", "RET", "RSE", "RUF", "S", "SIM", "T20", "TID", "UP", "W"]
+
+[tool.ruff.per-file-ignores]
+"tests/*" = ["ANN", "D"]
+
[tool.pytest.ini_options]
# We don't use nose style tests so disable them in pytest.
# This stops pytest from running functions named `setup` in test files.
diff --git a/tests/_autospec.py b/tests/_autospec.py
index ecff6bcbe..6f990a580 100644
--- a/tests/_autospec.py
+++ b/tests/_autospec.py
@@ -2,7 +2,7 @@ import contextlib
import functools
import pkgutil
import unittest.mock
-from typing import Callable
+from collections.abc import Callable
@functools.wraps(unittest.mock._patch.decoration_helper)
diff --git a/tests/base.py b/tests/base.py
index 4863a1821..cad187b6a 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -1,7 +1,6 @@
import logging
import unittest
from contextlib import contextmanager
-from typing import Dict
import discord
from async_rediscache import RedisSession
@@ -86,7 +85,7 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
async def assertHasPermissionsCheck( # noqa: N802
self,
cmd: commands.Command,
- permissions: Dict[str, bool],
+ permissions: dict[str, bool],
) -> None:
"""
Test that `cmd` raises a `MissingPermissions` exception if author lacks `permissions`.
diff --git a/tests/bot/exts/events/__init__.py b/tests/bot/exts/events/__init__.py
deleted file mode 100644
index e69de29bb..000000000
--- a/tests/bot/exts/events/__init__.py
+++ /dev/null
diff --git a/tests/bot/exts/events/test_code_jams.py b/tests/bot/exts/events/test_code_jams.py
deleted file mode 100644
index 684f7abcd..000000000
--- a/tests/bot/exts/events/test_code_jams.py
+++ /dev/null
@@ -1,170 +0,0 @@
-import unittest
-from unittest.mock import AsyncMock, MagicMock, create_autospec, patch
-
-from discord import CategoryChannel
-from discord.ext.commands import BadArgument
-
-from bot.constants import Roles
-from bot.exts.events import code_jams
-from bot.exts.events.code_jams import _channels, _cog
-from tests.helpers import (
- MockAttachment, MockBot, MockCategoryChannel, MockContext, MockGuild, MockMember, MockRole, MockTextChannel,
- autospec
-)
-
-TEST_CSV = b"""\
-Team Name,Team Member Discord ID,Team Leader
-Annoyed Alligators,12345,Y
-Annoyed Alligators,54321,N
-Oscillating Otters,12358,Y
-Oscillating Otters,74832,N
-Oscillating Otters,19903,N
-Annoyed Alligators,11111,N
-"""
-
-
-def get_mock_category(channel_count: int, name: str) -> CategoryChannel:
- """Return a mocked code jam category."""
- category = create_autospec(CategoryChannel, spec_set=True, instance=True)
- category.name = name
- category.channels = [MockTextChannel() for _ in range(channel_count)]
-
- return category
-
-
-class JamCodejamCreateTests(unittest.IsolatedAsyncioTestCase):
- """Tests for `codejam create` command."""
-
- def setUp(self):
- self.bot = MockBot()
- self.admin_role = MockRole(name="Admins", id=Roles.admins)
- self.command_user = MockMember([self.admin_role])
- self.guild = MockGuild([self.admin_role])
- self.ctx = MockContext(bot=self.bot, author=self.command_user, guild=self.guild)
- self.cog = _cog.CodeJams(self.bot)
-
- async def test_message_without_attachments(self):
- """If no link or attachments are provided, commands.BadArgument should be raised."""
- self.ctx.message.attachments = []
-
- with self.assertRaises(BadArgument):
- await self.cog.create(self.cog, self.ctx, None)
-
- @patch.object(_channels, "create_team_channel")
- @patch.object(_channels, "create_team_leader_channel")
- async def test_result_sending(self, create_leader_channel, create_team_channel):
- """Should call `ctx.send` when everything goes right."""
- self.ctx.message.attachments = [MockAttachment()]
- self.ctx.message.attachments[0].read = AsyncMock()
- self.ctx.message.attachments[0].read.return_value = TEST_CSV
-
- team_leaders = MockRole()
-
- self.guild.get_member.return_value = MockMember()
-
- self.ctx.guild.create_role = AsyncMock()
- self.ctx.guild.create_role.return_value = team_leaders
- self.cog.add_roles = AsyncMock()
-
- await self.cog.create(self.cog, self.ctx, None)
-
- create_team_channel.assert_awaited()
- create_leader_channel.assert_awaited_once_with(
- self.ctx.guild, team_leaders
- )
- self.ctx.send.assert_awaited_once()
-
- async def test_link_returning_non_200_status(self):
- """When the URL passed returns a non 200 status, it should send a message informing them."""
- self.bot.http_session.get.return_value = mock = MagicMock()
- mock.status = 404
- await self.cog.create(self.cog, self.ctx, "https://not-a-real-link.com")
-
- self.ctx.send.assert_awaited_once()
-
- @patch.object(_channels, "_send_status_update")
- async def test_category_doesnt_exist(self, update):
- """Should create a new code jam category."""
- subtests = (
- [],
- [get_mock_category(_channels.MAX_CHANNELS, _channels.CATEGORY_NAME)],
- [get_mock_category(_channels.MAX_CHANNELS - 2, "other")],
- )
-
- for categories in subtests:
- update.reset_mock()
- self.guild.reset_mock()
- self.guild.categories = categories
-
- with self.subTest(categories=categories):
- actual_category = await _channels._get_category(self.guild)
-
- update.assert_called_once()
- self.guild.create_category_channel.assert_awaited_once()
- category_overwrites = self.guild.create_category_channel.call_args[1]["overwrites"]
-
- self.assertFalse(category_overwrites[self.guild.default_role].read_messages)
- self.assertTrue(category_overwrites[self.guild.me].read_messages)
- self.assertEqual(self.guild.create_category_channel.return_value, actual_category)
-
- async def test_category_channel_exist(self):
- """Should not try to create category channel."""
- expected_category = get_mock_category(_channels.MAX_CHANNELS - 2, _channels.CATEGORY_NAME)
- self.guild.categories = [
- get_mock_category(_channels.MAX_CHANNELS - 2, "other"),
- expected_category,
- get_mock_category(0, _channels.CATEGORY_NAME),
- ]
-
- actual_category = await _channels._get_category(self.guild)
- self.assertEqual(expected_category, actual_category)
-
- async def test_channel_overwrites(self):
- """Should have correct permission overwrites for users and roles."""
- leader = (MockMember(), True)
- members = [leader] + [(MockMember(), False) for _ in range(4)]
- overwrites = _channels._get_overwrites(members, self.guild)
-
- for member, _ in members:
- self.assertTrue(overwrites[member].read_messages)
-
- @patch.object(_channels, "_get_overwrites")
- @patch.object(_channels, "_get_category")
- @autospec(_channels, "_add_team_leader_roles", pass_mocks=False)
- async def test_team_channels_creation(self, get_category, get_overwrites):
- """Should create a text channel for a team."""
- team_leaders = MockRole()
- members = [(MockMember(), True)] + [(MockMember(), False) for _ in range(5)]
- category = MockCategoryChannel()
- category.create_text_channel = AsyncMock()
-
- get_category.return_value = category
- await _channels.create_team_channel(self.guild, "my-team", members, team_leaders)
-
- category.create_text_channel.assert_awaited_once_with(
- "my-team",
- overwrites=get_overwrites.return_value
- )
-
- async def test_jam_roles_adding(self):
- """Should add team leader role to leader and jam role to every team member."""
- leader_role = MockRole(name="Team Leader")
-
- leader = MockMember()
- members = [(leader, True)] + [(MockMember(), False) for _ in range(4)]
- await _channels._add_team_leader_roles(members, leader_role)
-
- leader.add_roles.assert_awaited_once_with(leader_role)
- for member, is_leader in members:
- if not is_leader:
- member.add_roles.assert_not_awaited()
-
-
-class CodeJamSetup(unittest.IsolatedAsyncioTestCase):
- """Test for `setup` function of `CodeJam` cog."""
-
- async def test_setup(self):
- """Should call `bot.add_cog`."""
- bot = MockBot()
- await code_jams.setup(bot)
- bot.add_cog.assert_awaited_once()
diff --git a/tests/bot/exts/filtering/test_discord_token_filter.py b/tests/bot/exts/filtering/test_discord_token_filter.py
index a5cddf8d9..1cb9e16fa 100644
--- a/tests/bot/exts/filtering/test_discord_token_filter.py
+++ b/tests/bot/exts/filtering/test_discord_token_filter.py
@@ -222,8 +222,8 @@ class DiscordTokenFilterTests(unittest.IsolatedAsyncioTestCase):
def test_regex_matches_multiple_valid(self):
"""Should support multiple matches in the middle of a string."""
- token_1 = "NDY3MjIzMjMwNjUwNzc3NjQx.XsyWGg.uFNEQPCc4ePwGh7egG8UicQssz8"
- token_2 = "NDcyMjY1OTQzMDYyNDEzMzMy.XsyWMw.l8XPnDqb0lp-EiQ2g_0xVFT1pyc"
+ token_1 = "NDY3MjIzMjMwNjUwNzc3NjQx.XsyWGg.uFNEQPCc4ePwGh7egG8UicQssz8" # noqa: S105
+ token_2 = "NDcyMjY1OTQzMDYyNDEzMzMy.XsyWMw.l8XPnDqb0lp-EiQ2g_0xVFT1pyc" # noqa: S105
message = f"garbage {token_1} hello {token_2} world"
results = discord_token.TOKEN_RE.finditer(message)
diff --git a/tests/bot/exts/filtering/test_extension_filter.py b/tests/bot/exts/filtering/test_extension_filter.py
index 827d267d2..f71de1e1b 100644
--- a/tests/bot/exts/filtering/test_extension_filter.py
+++ b/tests/bot/exts/filtering/test_extension_filter.py
@@ -25,7 +25,7 @@ class ExtensionsListTests(unittest.IsolatedAsyncioTestCase):
for i, filter_content in enumerate(self.whitelist, start=1):
filters.append({
"id": i, "content": filter_content, "description": None, "settings": {},
- "additional_settings": {}, "created_at": now, "updated_at": now # noqa: P103
+ "additional_settings": {}, "created_at": now, "updated_at": now
})
self.filter_list.add_list({
"id": 1,
diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py
index 65595e959..e90291f62 100644
--- a/tests/bot/exts/info/test_information.py
+++ b/tests/bot/exts/info/test_information.py
@@ -1,7 +1,7 @@
import textwrap
import unittest
import unittest.mock
-from datetime import datetime
+from datetime import UTC, datetime
from textwrap import shorten
import discord
@@ -41,7 +41,7 @@ class InformationCogTests(unittest.IsolatedAsyncioTestCase):
self.ctx.send.assert_called_once()
_, kwargs = self.ctx.send.call_args
- embed = kwargs.pop('embed')
+ embed = kwargs.pop("embed")
self.assertEqual(embed.title, "Role information (Total 1 role)")
self.assertEqual(embed.colour, discord.Colour.og_blurple())
@@ -110,15 +110,15 @@ class UserInfractionHelperMethodTests(unittest.IsolatedAsyncioTestCase):
test_values = (
{
"helper_method": self.cog.basic_user_infraction_counts,
- "expected_args": ("bot/infractions", {'hidden': 'False', 'user__id': str(self.member.id)}),
+ "expected_args": ("bot/infractions", {"hidden": "False", "user__id": str(self.member.id)}),
},
{
"helper_method": self.cog.expanded_user_infraction_counts,
- "expected_args": ("bot/infractions", {'user__id': str(self.member.id)}),
+ "expected_args": ("bot/infractions", {"user__id": str(self.member.id)}),
},
{
"helper_method": self.cog.user_nomination_counts,
- "expected_args": ("bot/nominations", {'user__id': str(self.member.id)}),
+ "expected_args": ("bot/nominations", {"user__id": str(self.member.id)}),
},
)
@@ -241,19 +241,19 @@ class UserInfractionHelperMethodTests(unittest.IsolatedAsyncioTestCase):
"expected_lines": ["No nominations"],
},
{
- "api response": [{'active': True}],
+ "api response": [{"active": True}],
"expected_lines": ["This user is **currently** nominated", "(1 nomination in total)"],
},
{
- "api response": [{'active': True}, {'active': False}],
+ "api response": [{"active": True}, {"active": False}],
"expected_lines": ["This user is **currently** nominated", "(2 nominations in total)"],
},
{
- "api response": [{'active': False}],
+ "api response": [{"active": False}],
"expected_lines": ["This user has 1 historical nomination, but is currently not nominated."],
},
{
- "api response": [{'active': False}, {'active': False}],
+ "api response": [{"active": False}, {"active": False}],
"expected_lines": ["This user has 2 historical nominations, but is currently not nominated."],
},
@@ -290,7 +290,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
user.nick = None
user.__str__ = unittest.mock.Mock(return_value="Mr. Hemlock")
user.colour = 0
- user.created_at = user.joined_at = datetime.utcnow()
+ user.created_at = user.joined_at = datetime.now(UTC)
embed = await self.cog.create_user_embed(ctx, user, False)
@@ -312,7 +312,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
user.nick = "Cat lover"
user.__str__ = unittest.mock.Mock(return_value="Mr. Hemlock")
user.colour = 0
- user.created_at = user.joined_at = datetime.utcnow()
+ user.created_at = user.joined_at = datetime.now(UTC)
embed = await self.cog.create_user_embed(ctx, user, False)
@@ -329,11 +329,11 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
async def test_create_user_embed_ignores_everyone_role(self):
"""Created `!user` embeds should not contain mention of the @everyone-role."""
ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=1))
- admins_role = helpers.MockRole(name='Admins')
+ admins_role = helpers.MockRole(name="Admins")
# A `MockMember` has the @Everyone role by default; we add the Admins to that.
user = helpers.MockMember(roles=[admins_role], colour=100)
- user.created_at = user.joined_at = datetime.utcnow()
+ user.created_at = user.joined_at = datetime.now(UTC)
embed = await self.cog.create_user_embed(ctx, user, False)
@@ -354,13 +354,13 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
"""The embed should contain expanded infractions and nomination info in mod channels."""
ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=50))
- moderators_role = helpers.MockRole(name='Moderators')
+ moderators_role = helpers.MockRole(name="Moderators")
infraction_counts.return_value = ("Infractions", "expanded infractions info")
nomination_counts.return_value = ("Nominations", "nomination info")
user = helpers.MockMember(id=314, roles=[moderators_role], colour=100)
- user.created_at = user.joined_at = datetime.utcfromtimestamp(1)
+ user.created_at = user.joined_at = datetime.fromtimestamp(1, tz=UTC)
embed = await self.cog.create_user_embed(ctx, user, False)
infraction_counts.assert_called_once_with(user)
@@ -394,13 +394,13 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
"""The embed should contain only basic infraction data outside of mod channels."""
ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=100))
- moderators_role = helpers.MockRole(name='Moderators')
+ moderators_role = helpers.MockRole(name="Moderators")
infraction_counts.return_value = ("Infractions", "basic infractions info")
user_messages.return_value = ("Messages", "user message counts")
user = helpers.MockMember(id=314, roles=[moderators_role], colour=100)
- user.created_at = user.joined_at = datetime.utcfromtimestamp(1)
+ user.created_at = user.joined_at = datetime.fromtimestamp(1, tz=UTC)
embed = await self.cog.create_user_embed(ctx, user, False)
infraction_counts.assert_called_once_with(user)
@@ -444,10 +444,10 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
"""The embed should be created with the colour of the top role, if a top role is available."""
ctx = helpers.MockContext()
- moderators_role = helpers.MockRole(name='Moderators')
+ moderators_role = helpers.MockRole(name="Moderators")
user = helpers.MockMember(id=314, roles=[moderators_role], colour=100)
- user.created_at = user.joined_at = datetime.utcnow()
+ user.created_at = user.joined_at = datetime.now(UTC)
embed = await self.cog.create_user_embed(ctx, user, False)
self.assertEqual(embed.colour, discord.Colour(100))
@@ -465,7 +465,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
ctx = helpers.MockContext()
user = helpers.MockMember(id=217, colour=discord.Colour.default())
- user.created_at = user.joined_at = datetime.utcnow()
+ user.created_at = user.joined_at = datetime.now(UTC)
embed = await self.cog.create_user_embed(ctx, user, False)
self.assertEqual(embed.colour, discord.Colour.og_blurple())
@@ -483,7 +483,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
ctx = helpers.MockContext()
user = helpers.MockMember(id=217, colour=0)
- user.created_at = user.joined_at = datetime.utcnow()
+ user.created_at = user.joined_at = datetime.now(UTC)
user.display_avatar.url = "avatar url"
embed = await self.cog.create_user_embed(ctx, user, False)
@@ -644,6 +644,6 @@ class RuleCommandTests(unittest.IsolatedAsyncioTestCase):
for raw_user_input, expected_matched_rule_numbers in test_cases:
with self.subTest(identifier=raw_user_input):
final_rule_numbers = await self.cog.rules(self.cog, self.ctx, args=raw_user_input)
- embed = self.ctx.send.call_args.kwargs['embed']
+ embed = self.ctx.send.call_args.kwargs["embed"]
self.assertEqual(information.DEFAULT_RULES_DESCRIPTION, embed.description)
self.assertEqual(expected_matched_rule_numbers, final_rule_numbers)
diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py
index b78328137..26ba770dc 100644
--- a/tests/bot/exts/moderation/infraction/test_infractions.py
+++ b/tests/bot/exts/moderation/infraction/test_infractions.py
@@ -270,10 +270,9 @@ class CleanBanTests(unittest.IsolatedAsyncioTestCase):
def inner(name):
if name == "ModManagement":
return self.management_cog if enable_manage else None
- elif name == "Clean":
+ if name == "Clean":
return self.clean_cog if enable_clean else None
- else:
- return DEFAULT
+ return DEFAULT
return inner
async def test_cleanban_falls_back_to_native_purge_without_clean_cog(self):
diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py
index 122935e37..25337673e 100644
--- a/tests/bot/exts/moderation/infraction/test_utils.py
+++ b/tests/bot/exts/moderation/infraction/test_utils.py
@@ -1,6 +1,6 @@
import unittest
from collections import namedtuple
-from datetime import datetime
+from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock, patch
from discord import Embed, Forbidden, HTTPException, NotFound
@@ -136,7 +136,10 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):
"""
test_cases = [
{
- "args": (dict(id=0, type="ban", reason=None, expires_at=datetime(2020, 2, 26, 9, 20)), self.user),
+ "args": (
+ dict(id=0, type="ban", reason=None, expires_at=datetime(2020, 2, 26, 9, 20, tzinfo=UTC)),
+ self.user,
+ ),
"expected_output": Embed(
title=utils.INFRACTION_TITLE,
description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format(
@@ -192,7 +195,10 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):
"send_result": False
},
{
- "args": (dict(id=0, type="mute", reason="Test", expires_at=datetime(2020, 2, 26, 9, 20)), self.user),
+ "args": (
+ dict(id=0, type="mute", reason="Test", expires_at=datetime(2020, 2, 26, 9, 20, tzinfo=UTC)),
+ self.user,
+ ),
"expected_output": Embed(
title=utils.INFRACTION_TITLE,
description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format(
@@ -309,7 +315,7 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase):
async def test_normal_post_infraction(self):
"""Should return response from POST request if there are no errors."""
- now = datetime.utcnow()
+ now = datetime.now(UTC)
expected = {
"actor": self.ctx.author.id,
"hidden": True,
diff --git a/tests/bot/exts/moderation/test_incidents.py b/tests/bot/exts/moderation/test_incidents.py
index 53d98360c..1a02339d4 100644
--- a/tests/bot/exts/moderation/test_incidents.py
+++ b/tests/bot/exts/moderation/test_incidents.py
@@ -20,7 +20,7 @@ from tests.helpers import (
MockUser
)
-CURRENT_TIME = datetime.datetime(2022, 1, 1, tzinfo=datetime.timezone.utc)
+CURRENT_TIME = datetime.datetime(2022, 1, 1, tzinfo=datetime.UTC)
class MockAsyncIterable:
@@ -799,7 +799,7 @@ class TestMessageLinkEmbeds(TestIncidents):
"\n".join("Lets make a new line test".split()): "Lets\nmake\na...",
- 'Hello, World!' * 300: (
+ "Hello, World!" * 300: (
"Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!"
"Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!"
"Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!"
diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py
index 2622f46a7..ec0b3bf43 100644
--- a/tests/bot/exts/moderation/test_silence.py
+++ b/tests/bot/exts/moderation/test_silence.py
@@ -1,7 +1,6 @@
import itertools
import unittest
-from datetime import datetime, timezone
-from typing import List, Tuple
+from datetime import UTC, datetime
from unittest import mock
from unittest.mock import AsyncMock, Mock
@@ -97,10 +96,12 @@ class SilenceNotifierTests(SilenceTest):
"""Alert is skipped on first loop or not an increment of 900."""
test_cases = (0, 15, 5000)
for current_loop in test_cases:
- with self.subTest(current_loop=current_loop):
- with mock.patch.object(self.notifier, "_current_loop", new=current_loop):
- await self.notifier._notifier()
- self.alert_channel.send.assert_not_called()
+ with (
+ self.subTest(current_loop=current_loop),
+ mock.patch.object(self.notifier, "_current_loop", new=current_loop),
+ ):
+ await self.notifier._notifier()
+ self.alert_channel.send.assert_not_called()
@autospec(silence.Silence, "previous_overwrites", "unsilence_timestamps", pass_mocks=False)
@@ -203,7 +204,7 @@ class SilenceCogTests(SilenceTest):
self.assertEqual((None,), member.move_to.call_args_list[0].args)
@staticmethod
- def create_erroneous_members() -> Tuple[List[MockMember], List[MockMember]]:
+ def create_erroneous_members() -> tuple[list[MockMember], list[MockMember]]:
"""
Helper method to generate a list of members that error out on move_to call.
@@ -363,7 +364,7 @@ class RescheduleTests(RedisTestCase):
channels = [MockTextChannel(id=123), MockTextChannel(id=456)]
self.bot.get_channel.side_effect = channels
self.cog.unsilence_timestamps.items.return_value = [(123, 2000), (456, 3000)]
- silence.datetime.now.return_value = datetime.fromtimestamp(1000, tz=timezone.utc)
+ silence.datetime.now.return_value = datetime.fromtimestamp(1000, tz=UTC)
self.cog._unsilence_wrapper = mock.MagicMock()
unsilence_return = self.cog._unsilence_wrapper.return_value
@@ -426,17 +427,19 @@ class SilenceTests(SilenceTest):
targets = (MockTextChannel(), MockVoiceChannel(), None)
for (duration, message, was_silenced), target in itertools.product(test_cases, targets):
- with mock.patch.object(self.cog, "_set_silence_overwrites", return_value=was_silenced):
- with self.subTest(was_silenced=was_silenced, target=target, message=message):
- with mock.patch.object(self.cog, "send_message") as send_message:
- ctx = MockContext()
- await self.cog.silence.callback(self.cog, ctx, target, duration)
- send_message.assert_called_once_with(
- message,
- ctx.channel,
- target or ctx.channel,
- alert_target=was_silenced
- )
+ with (
+ mock.patch.object(self.cog, "_set_silence_overwrites", return_value=was_silenced),
+ self.subTest(was_silenced=was_silenced, target=target, message=message),
+ mock.patch.object(self.cog, "send_message") as send_message
+ ):
+ ctx = MockContext()
+ await self.cog.silence.callback(self.cog, ctx, target, duration)
+ send_message.assert_called_once_with(
+ message,
+ ctx.channel,
+ target or ctx.channel,
+ alert_target=was_silenced
+ )
@voice_sync_helper
async def test_sync_called(self, ctx, sync, kick):
@@ -577,10 +580,10 @@ class SilenceTests(SilenceTest):
new_overwrite_dict = dict(self.voice_overwrite)
# Remove 'connect' & 'speak' keys because they were changed by the method.
- del prev_overwrite_dict['connect']
- del prev_overwrite_dict['speak']
- del new_overwrite_dict['connect']
- del new_overwrite_dict['speak']
+ del prev_overwrite_dict["connect"]
+ del prev_overwrite_dict["speak"]
+ del new_overwrite_dict["connect"]
+ del new_overwrite_dict["speak"]
self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict)
@@ -617,13 +620,13 @@ class SilenceTests(SilenceTest):
now_timestamp = 100
duration = 15
timestamp = now_timestamp + duration * 60
- datetime_mock.now.return_value = datetime.fromtimestamp(now_timestamp, tz=timezone.utc)
+ datetime_mock.now.return_value = datetime.fromtimestamp(now_timestamp, tz=UTC)
ctx = MockContext(channel=self.text_channel)
await self.cog.silence.callback(self.cog, ctx, duration)
self.cog.unsilence_timestamps.set.assert_awaited_once_with(ctx.channel.id, timestamp)
- datetime_mock.now.assert_called_once_with(tz=timezone.utc) # Ensure it's using an aware dt.
+ datetime_mock.now.assert_called_once_with(tz=UTC) # Ensure it's using an aware dt.
async def test_cached_indefinite_time(self):
"""A value of -1 was cached for a permanent silence."""
@@ -697,13 +700,15 @@ class UnsilenceTests(SilenceTest):
if target:
target.overwrites_for.return_value = overwrite
- with mock.patch.object(self.cog, "_unsilence", return_value=was_unsilenced):
- with mock.patch.object(self.cog, "send_message") as send_message:
- with self.subTest(was_unsilenced=was_unsilenced, overwrite=overwrite, target=target):
- await self.cog.unsilence.callback(self.cog, ctx, channel=target)
+ with (
+ mock.patch.object(self.cog, "_unsilence", return_value=was_unsilenced),
+ mock.patch.object(self.cog, "send_message") as send_message,
+ self.subTest(was_unsilenced=was_unsilenced, overwrite=overwrite, target=target),
+ ):
+ await self.cog.unsilence.callback(self.cog, ctx, channel=target)
- call_args = (message, ctx.channel, target or ctx.channel)
- send_message.assert_awaited_once_with(*call_args, alert_target=was_unsilenced)
+ call_args = (message, ctx.channel, target or ctx.channel)
+ send_message.assert_awaited_once_with(*call_args, alert_target=was_unsilenced)
async def test_skipped_already_unsilenced(self):
"""Permissions were not set and `False` was returned for an already unsilenced channel."""
@@ -808,10 +813,10 @@ class UnsilenceTests(SilenceTest):
new_overwrite_dict = dict(self.text_overwrite)
# Remove these keys because they were modified by the unsilence.
- del prev_overwrite_dict['send_messages']
- del prev_overwrite_dict['add_reactions']
- del new_overwrite_dict['send_messages']
- del new_overwrite_dict['add_reactions']
+ del prev_overwrite_dict["send_messages"]
+ del prev_overwrite_dict["add_reactions"]
+ del new_overwrite_dict["send_messages"]
+ del new_overwrite_dict["add_reactions"]
self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict)
@@ -826,10 +831,10 @@ class UnsilenceTests(SilenceTest):
new_overwrite_dict = dict(self.voice_overwrite)
# Remove these keys because they were modified by the unsilence.
- del prev_overwrite_dict['connect']
- del prev_overwrite_dict['speak']
- del new_overwrite_dict['connect']
- del new_overwrite_dict['speak']
+ del prev_overwrite_dict["connect"]
+ del prev_overwrite_dict["speak"]
+ del new_overwrite_dict["connect"]
+ del new_overwrite_dict["speak"]
self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict)
diff --git a/tests/bot/exts/moderation/test_slowmode.py b/tests/bot/exts/moderation/test_slowmode.py
index 5483b7a64..cf5101e16 100644
--- a/tests/bot/exts/moderation/test_slowmode.py
+++ b/tests/bot/exts/moderation/test_slowmode.py
@@ -17,24 +17,24 @@ class SlowmodeTests(unittest.IsolatedAsyncioTestCase):
async def test_get_slowmode_no_channel(self) -> None:
"""Get slowmode without a given channel."""
- self.ctx.channel = MockTextChannel(name='python-general', slowmode_delay=5)
+ self.ctx.channel = MockTextChannel(name="python-general", slowmode_delay=5)
await self.cog.get_slowmode(self.cog, self.ctx, None)
self.ctx.send.assert_called_once_with("The slowmode delay for #python-general is 5 seconds.")
async def test_get_slowmode_with_channel(self) -> None:
"""Get slowmode with a given channel."""
- text_channel = MockTextChannel(name='python-language', slowmode_delay=2)
+ text_channel = MockTextChannel(name="python-language", slowmode_delay=2)
await self.cog.get_slowmode(self.cog, self.ctx, text_channel)
- self.ctx.send.assert_called_once_with('The slowmode delay for #python-language is 2 seconds.')
+ self.ctx.send.assert_called_once_with("The slowmode delay for #python-language is 2 seconds.")
async def test_set_slowmode_no_channel(self) -> None:
"""Set slowmode without a given channel."""
test_cases = (
- ('helpers', 23, True, f'{Emojis.check_mark} The slowmode delay for #helpers is now 23 seconds.'),
- ('mods', 76526, False, f'{Emojis.cross_mark} The slowmode delay must be between 0 and 6 hours.'),
- ('admins', 97, True, f'{Emojis.check_mark} The slowmode delay for #admins is now 1 minute and 37 seconds.')
+ ("helpers", 23, True, f"{Emojis.check_mark} The slowmode delay for #helpers is now 23 seconds."),
+ ("mods", 76526, False, f"{Emojis.cross_mark} The slowmode delay must be between 0 and 6 hours."),
+ ("admins", 97, True, f"{Emojis.check_mark} The slowmode delay for #admins is now 1 minute and 37 seconds.")
)
for channel_name, seconds, edited, result_msg in test_cases:
@@ -60,9 +60,9 @@ class SlowmodeTests(unittest.IsolatedAsyncioTestCase):
async def test_set_slowmode_with_channel(self) -> None:
"""Set slowmode with a given channel."""
test_cases = (
- ('bot-commands', 12, True, f'{Emojis.check_mark} The slowmode delay for #bot-commands is now 12 seconds.'),
- ('mod-spam', 21, True, f'{Emojis.check_mark} The slowmode delay for #mod-spam is now 21 seconds.'),
- ('admin-spam', 4323598, False, f'{Emojis.cross_mark} The slowmode delay must be between 0 and 6 hours.')
+ ("bot-commands", 12, True, f"{Emojis.check_mark} The slowmode delay for #bot-commands is now 12 seconds."),
+ ("mod-spam", 21, True, f"{Emojis.check_mark} The slowmode delay for #mod-spam is now 21 seconds."),
+ ("admin-spam", 4323598, False, f"{Emojis.cross_mark} The slowmode delay must be between 0 and 6 hours.")
)
for channel_name, seconds, edited, result_msg in test_cases:
@@ -87,7 +87,7 @@ class SlowmodeTests(unittest.IsolatedAsyncioTestCase):
async def test_reset_slowmode_sets_delay_to_zero(self) -> None:
"""Reset slowmode with a given channel."""
- text_channel = MockTextChannel(name='meta', slowmode_delay=1)
+ text_channel = MockTextChannel(name="meta", slowmode_delay=1)
self.cog.set_slowmode = mock.AsyncMock()
await self.cog.reset_slowmode(self.cog, self.ctx, text_channel)
diff --git a/tests/bot/exts/recruitment/talentpool/test_review.py b/tests/bot/exts/recruitment/talentpool/test_review.py
index f726fccc7..25622e91f 100644
--- a/tests/bot/exts/recruitment/talentpool/test_review.py
+++ b/tests/bot/exts/recruitment/talentpool/test_review.py
@@ -1,5 +1,5 @@
import unittest
-from datetime import datetime, timedelta, timezone
+from datetime import UTC, datetime, timedelta
from unittest.mock import AsyncMock, Mock, patch
from bot.exts.recruitment.talentpool import _review
@@ -61,8 +61,8 @@ class ReviewerTests(unittest.IsolatedAsyncioTestCase):
@patch("bot.exts.recruitment.talentpool._review.MIN_REVIEW_INTERVAL", timedelta(days=1))
async def test_is_ready_for_review(self):
"""Tests for the `is_ready_for_review` function."""
- too_recent = datetime.now(timezone.utc) - timedelta(hours=1)
- not_too_recent = datetime.now(timezone.utc) - timedelta(days=7)
+ too_recent = datetime.now(UTC) - timedelta(hours=1)
+ not_too_recent = datetime.now(UTC) - timedelta(days=7)
cases = (
# Only one review, and not too recent, so ready.
(
@@ -126,7 +126,7 @@ class ReviewerTests(unittest.IsolatedAsyncioTestCase):
@patch("bot.exts.recruitment.talentpool._review.MIN_NOMINATION_TIME", timedelta(days=7))
async def test_get_nomination_to_review(self):
"""Test get_nomination_to_review function."""
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
# Each case contains a list of nominations, followed by the index in that list
# of the one that should be selected, or None if None should be returned
@@ -184,7 +184,7 @@ class ReviewerTests(unittest.IsolatedAsyncioTestCase):
@patch("bot.exts.recruitment.talentpool._review.MIN_NOMINATION_TIME", timedelta(days=0))
async def test_get_nomination_to_review_order(self):
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
# Each case in cases is a list of nominations in the order they should be chosen from first to last
cases = [
diff --git a/tests/bot/exts/test_cogs.py b/tests/bot/exts/test_cogs.py
index f8e120262..99bc87120 100644
--- a/tests/bot/exts/test_cogs.py
+++ b/tests/bot/exts/test_cogs.py
@@ -50,7 +50,7 @@ class CommandNameTests(unittest.TestCase):
yield obj
@staticmethod
- def get_qualified_names(command: commands.Command) -> t.List[str]:
+ def get_qualified_names(command: commands.Command) -> list[str]:
"""Return a list of all qualified names, including aliases, for the `command`."""
names = [f"{command.full_parent_name} {alias}".strip() for alias in command.aliases]
names.append(command.qualified_name)
diff --git a/tests/bot/exts/utils/snekbox/test_io.py b/tests/bot/exts/utils/snekbox/test_io.py
index bcf1162b8..4f7f49a5e 100644
--- a/tests/bot/exts/utils/snekbox/test_io.py
+++ b/tests/bot/exts/utils/snekbox/test_io.py
@@ -19,7 +19,7 @@ class SnekboxIOTests(TestCase):
(r"A\0\tB", "A__B"),
# Any other disallowed chars -> underscore
(r"\\.txt", "_.txt"),
- (r"A!@#$%^&*B, C()[]{}+=D.txt", "A_B_C_D.txt"), # noqa: P103
+ (r"A!@#$%^&*B, C()[]{}+=D.txt", "A_B_C_D.txt"),
(" ", "_"),
# Normal file names should be unchanged
("legal_file-name.txt", "legal_file-name.txt"),
diff --git a/tests/bot/exts/utils/snekbox/test_snekbox.py b/tests/bot/exts/utils/snekbox/test_snekbox.py
index 79ac8ea2c..fa28aade8 100644
--- a/tests/bot/exts/utils/snekbox/test_snekbox.py
+++ b/tests/bot/exts/utils/snekbox/test_snekbox.py
@@ -43,7 +43,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
"files": [
{
"path": "main.py",
- "content": b64encode("import random".encode()).decode()
+ "content": b64encode(b"import random").decode()
}
]
}
@@ -72,45 +72,45 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
async def test_codeblock_converter(self):
ctx = MockContext()
cases = (
- ('print("Hello world!")', 'print("Hello world!")', 'non-formatted'),
- ('`print("Hello world!")`', 'print("Hello world!")', 'one line code block'),
- ('```\nprint("Hello world!")```', 'print("Hello world!")', 'multiline code block'),
- ('```py\nprint("Hello world!")```', 'print("Hello world!")', 'multiline python code block'),
- ('text```print("Hello world!")```text', 'print("Hello world!")', 'code block surrounded by text'),
+ ('print("Hello world!")', 'print("Hello world!")', "non-formatted"),
+ ('`print("Hello world!")`', 'print("Hello world!")', "one line code block"),
+ ('```\nprint("Hello world!")```', 'print("Hello world!")', "multiline code block"),
+ ('```py\nprint("Hello world!")```', 'print("Hello world!")', "multiline python code block"),
+ ('text```print("Hello world!")```text', 'print("Hello world!")', "code block surrounded by text"),
('```print("Hello world!")```\ntext\n```py\nprint("Hello world!")```',
- 'print("Hello world!")\nprint("Hello world!")', 'two code blocks with text in-between'),
+ 'print("Hello world!")\nprint("Hello world!")', "two code blocks with text in-between"),
('`print("Hello world!")`\ntext\n```print("How\'s it going?")```',
- 'print("How\'s it going?")', 'code block preceded by inline code'),
+ 'print("How\'s it going?")', "code block preceded by inline code"),
('`print("Hello world!")`\ntext\n`print("Hello world!")`',
- 'print("Hello world!")', 'one inline code block of two')
+ 'print("Hello world!")', "one inline code block of two")
)
for case, expected, testname in cases:
- with self.subTest(msg=f'Extract code from {testname}.'):
+ with self.subTest(msg=f"Extract code from {testname}."):
self.assertEqual(
- '\n'.join(await snekbox.CodeblockConverter.convert(ctx, case)), expected
+ "\n".join(await snekbox.CodeblockConverter.convert(ctx, case)), expected
)
def test_prepare_timeit_input(self):
"""Test the prepare_timeit_input codeblock detection."""
- base_args = ('-m', 'timeit', '-s')
+ base_args = ("-m", "timeit", "-s")
cases = (
- (['print("Hello World")'], '', 'single block of code'),
- (['x = 1', 'print(x)'], 'x = 1', 'two blocks of code'),
- (['x = 1', 'print(x)', 'print("Some other code.")'], 'x = 1', 'three blocks of code')
+ (['print("Hello World")'], "", "single block of code"),
+ (["x = 1", "print(x)"], "x = 1", "two blocks of code"),
+ (["x = 1", "print(x)", 'print("Some other code.")'], "x = 1", "three blocks of code")
)
for case, setup_code, test_name in cases:
setup = snekbox._cog.TIMEIT_SETUP_WRAPPER.format(setup=setup_code)
- expected = [*base_args, setup, '\n'.join(case[1:] if setup_code else case)]
- with self.subTest(msg=f'Test with {test_name} and expected return {expected}'):
+ expected = [*base_args, setup, "\n".join(case[1:] if setup_code else case)]
+ with self.subTest(msg=f"Test with {test_name} and expected return {expected}"):
self.assertEqual(self.cog.prepare_timeit_input(case), expected)
def test_eval_result_message(self):
"""EvalResult.get_message(), should return message."""
cases = (
- ('ERROR', None, ('Your 3.11 eval job has failed', 'ERROR', '')),
- ('', 128 + snekbox._eval.SIGKILL, ('Your 3.11 eval job timed out or ran out of memory', '', '')),
- ('', 255, ('Your 3.11 eval job has failed', 'A fatal NsJail error occurred', ''))
+ ("ERROR", None, ("Your 3.11 eval job has failed", "ERROR", "")),
+ ("", 128 + snekbox._eval.SIGKILL, ("Your 3.11 eval job timed out or ran out of memory", "", "")),
+ ("", 255, ("Your 3.11 eval job has failed", "A fatal NsJail error occurred", ""))
)
for stdout, returncode, expected in cases:
exp_msg, exp_err, exp_files_err = expected
@@ -167,7 +167,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
msg = result.get_failed_files_str(char_max=10)
self.assertEqual(msg, expected)
- @patch('bot.exts.utils.snekbox._eval.Signals', side_effect=ValueError)
+ @patch("bot.exts.utils.snekbox._eval.Signals", side_effect=ValueError)
def test_eval_result_message_invalid_signal(self, _mock_signals: Mock):
result = EvalResult(stdout="", returncode=127)
self.assertEqual(
@@ -177,7 +177,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(result.error_message, "")
self.assertEqual(result.files_error_message, "")
- @patch('bot.exts.utils.snekbox._eval.Signals')
+ @patch("bot.exts.utils.snekbox._eval.Signals")
def test_eval_result_message_valid_signal(self, mock_signals: Mock):
mock_signals.return_value.name = "SIGTEST"
result = EvalResult(stdout="", returncode=127)
@@ -189,9 +189,9 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
def test_eval_result_status_emoji(self):
"""Return emoji according to the eval result."""
cases = (
- (' ', -1, ':warning:'),
- ('Hello world!', 0, ':white_check_mark:'),
- ('Invalid beard size', -1, ':x:')
+ (" ", -1, ":warning:"),
+ ("Hello world!", 0, ":white_check_mark:"),
+ ("Invalid beard size", -1, ":x:")
)
for stdout, returncode, expected in cases:
with self.subTest(stdout=stdout, returncode=returncode, expected=expected):
@@ -200,48 +200,48 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
async def test_format_output(self):
"""Test output formatting."""
- self.cog.upload_output = AsyncMock(return_value='https://testificate.com/')
+ self.cog.upload_output = AsyncMock(return_value="https://testificate.com/")
too_many_lines = (
- '001 | v\n002 | e\n003 | r\n004 | y\n005 | l\n006 | o\n'
- '007 | n\n008 | g\n009 | b\n010 | e\n011 | a\n... (truncated - too many lines)'
+ "001 | v\n002 | e\n003 | r\n004 | y\n005 | l\n006 | o\n"
+ "007 | n\n008 | g\n009 | b\n010 | e\n011 | a\n... (truncated - too many lines)"
)
too_long_too_many_lines = (
"\n".join(
- f"{i:03d} | {line}" for i, line in enumerate(['verylongbeard' * 10] * 15, 1)
+ f"{i:03d} | {line}" for i, line in enumerate(["verylongbeard" * 10] * 15, 1)
)[:1000] + "\n... (truncated - too long, too many lines)"
)
cases = (
- ('', ('[No output]', None), 'No output'),
- ('My awesome output', ('My awesome output', None), 'One line output'),
- ('<@', ("<@\u200B", None), r'Convert <@ to <@\u200B'),
- ('<!@', ("<!@\u200B", None), r'Convert <!@ to <!@\u200B'),
+ ("", ("[No output]", None), "No output"),
+ ("My awesome output", ("My awesome output", None), "One line output"),
+ ("<@", ("<@\u200B", None), r"Convert <@ to <@\u200B"),
+ ("<!@", ("<!@\u200B", None), r"Convert <!@ to <!@\u200B"),
(
- '\u202E\u202E\u202E',
- ('Code block escape attempt detected; will not output result', 'https://testificate.com/'),
- 'Detect RIGHT-TO-LEFT OVERRIDE'
+ "\u202E\u202E\u202E",
+ ("Code block escape attempt detected; will not output result", "https://testificate.com/"),
+ "Detect RIGHT-TO-LEFT OVERRIDE"
),
(
- '\u200B\u200B\u200B',
- ('Code block escape attempt detected; will not output result', 'https://testificate.com/'),
- 'Detect ZERO WIDTH SPACE'
+ "\u200B\u200B\u200B",
+ ("Code block escape attempt detected; will not output result", "https://testificate.com/"),
+ "Detect ZERO WIDTH SPACE"
),
- ('long\nbeard', ('001 | long\n002 | beard', None), 'Two line output'),
+ ("long\nbeard", ("001 | long\n002 | beard", None), "Two line output"),
(
- 'v\ne\nr\ny\nl\no\nn\ng\nb\ne\na\nr\nd',
- (too_many_lines, 'https://testificate.com/'),
- '12 lines output'
+ "v\ne\nr\ny\nl\no\nn\ng\nb\ne\na\nr\nd",
+ (too_many_lines, "https://testificate.com/"),
+ "12 lines output"
),
(
- 'verylongbeard' * 100,
- ('verylongbeard' * 76 + 'verylongbear\n... (truncated - too long)', 'https://testificate.com/'),
- '1300 characters output'
+ "verylongbeard" * 100,
+ ("verylongbeard" * 76 + "verylongbear\n... (truncated - too long)", "https://testificate.com/"),
+ "1300 characters output"
),
(
- ('verylongbeard' * 10 + '\n') * 15,
- (too_long_too_many_lines, 'https://testificate.com/'),
- '15 lines, 1965 characters output'
+ ("verylongbeard" * 10 + "\n") * 15,
+ (too_long_too_many_lines, "https://testificate.com/"),
+ "15 lines, 1965 characters output"
),
)
for case, expected, testname in cases:
@@ -257,10 +257,10 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
self.cog.send_job = AsyncMock(return_value=response)
self.cog.continue_job = AsyncMock(return_value=None)
- await self.cog.eval_command(self.cog, ctx=ctx, python_version='3.11', code=['MyAwesomeCode'])
+ await self.cog.eval_command(self.cog, ctx=ctx, python_version="3.11", code=["MyAwesomeCode"])
job = EvalJob.from_code("MyAwesomeCode")
self.cog.send_job.assert_called_once_with(ctx, job)
- self.cog.continue_job.assert_called_once_with(ctx, response, 'eval')
+ self.cog.continue_job.assert_called_once_with(ctx, response, "eval")
async def test_eval_command_evaluate_twice(self):
"""Test the eval and re-eval command procedure."""
@@ -269,9 +269,9 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
ctx.command = MagicMock()
self.cog.send_job = AsyncMock(return_value=response)
self.cog.continue_job = AsyncMock()
- self.cog.continue_job.side_effect = (EvalJob.from_code('MyAwesomeFormattedCode'), None)
+ self.cog.continue_job.side_effect = (EvalJob.from_code("MyAwesomeFormattedCode"), None)
- await self.cog.eval_command(self.cog, ctx=ctx, python_version='3.11', code=['MyAwesomeCode'])
+ await self.cog.eval_command(self.cog, ctx=ctx, python_version="3.11", code=["MyAwesomeCode"])
expected_job = EvalJob.from_code("MyAwesomeFormattedCode")
self.cog.send_job.assert_called_with(ctx, expected_job)
@@ -285,7 +285,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
async def delay_with_side_effect(*args, **kwargs) -> dict:
"""Delay the post_job call to ensure the job runs long enough to conflict."""
await asyncio.sleep(1)
- return {'stdout': '', 'returncode': 0}
+ return {"stdout": "", "returncode": 0}
self.cog.post_job = AsyncMock(side_effect=delay_with_side_effect)
with self.assertRaises(LockedResourceError):
@@ -299,32 +299,32 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
ctx = MockContext()
ctx.message = MockMessage()
ctx.send = AsyncMock()
- ctx.author = MockUser(mention='@LemonLemonishBeard#0042')
+ ctx.author = MockUser(mention="@LemonLemonishBeard#0042")
eval_result = EvalResult("", 0)
self.cog.post_job = AsyncMock(return_value=eval_result)
- self.cog.format_output = AsyncMock(return_value=('[No output]', None))
+ self.cog.format_output = AsyncMock(return_value=("[No output]", None))
self.cog.upload_output = AsyncMock() # Should not be called
mocked_filter_cog = MagicMock()
mocked_filter_cog.filter_snekbox_output = AsyncMock(return_value=(False, []))
self.bot.get_cog.return_value = mocked_filter_cog
- job = EvalJob.from_code('MyAwesomeCode')
+ job = EvalJob.from_code("MyAwesomeCode")
await self.cog.send_job(ctx, job),
ctx.send.assert_called_once()
self.assertEqual(
ctx.send.call_args.args[0],
- '@LemonLemonishBeard#0042 :warning: Your 3.11 eval job has completed '
- 'with return code 0.\n\n```\n[No output]\n```'
+ "@LemonLemonishBeard#0042 :warning: Your 3.11 eval job has completed "
+ "with return code 0.\n\n```\n[No output]\n```"
)
- allowed_mentions = ctx.send.call_args.kwargs['allowed_mentions']
+ allowed_mentions = ctx.send.call_args.kwargs["allowed_mentions"]
expected_allowed_mentions = AllowedMentions(everyone=False, roles=False, users=[ctx.author])
self.assertEqual(allowed_mentions.to_dict(), expected_allowed_mentions.to_dict())
self.cog.post_job.assert_called_once_with(job)
- self.cog.format_output.assert_called_once_with('')
+ self.cog.format_output.assert_called_once_with("")
self.cog.upload_output.assert_not_called()
async def test_send_job_with_paste_link(self):
@@ -332,11 +332,11 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
ctx = MockContext()
ctx.message = MockMessage()
ctx.send = AsyncMock()
- ctx.author.mention = '@LemonLemonishBeard#0042'
+ ctx.author.mention = "@LemonLemonishBeard#0042"
eval_result = EvalResult("Way too long beard", 0)
self.cog.post_job = AsyncMock(return_value=eval_result)
- self.cog.format_output = AsyncMock(return_value=('Way too long beard', 'lookatmybeard.com'))
+ self.cog.format_output = AsyncMock(return_value=("Way too long beard", "lookatmybeard.com"))
mocked_filter_cog = MagicMock()
mocked_filter_cog.filter_snekbox_output = AsyncMock(return_value=(False, []))
@@ -348,20 +348,20 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
ctx.send.assert_called_once()
self.assertEqual(
ctx.send.call_args.args[0],
- '@LemonLemonishBeard#0042 :white_check_mark: Your 3.11 eval job '
- 'has completed with return code 0.'
- '\n\n```\nWay too long beard\n```\nFull output: lookatmybeard.com'
+ "@LemonLemonishBeard#0042 :white_check_mark: Your 3.11 eval job "
+ "has completed with return code 0."
+ "\n\n```\nWay too long beard\n```\nFull output: lookatmybeard.com"
)
self.cog.post_job.assert_called_once_with(job)
- self.cog.format_output.assert_called_once_with('Way too long beard')
+ self.cog.format_output.assert_called_once_with("Way too long beard")
async def test_send_job_with_non_zero_eval(self):
"""Test the send_job function with a code returning a non-zero code."""
ctx = MockContext()
ctx.message = MockMessage()
ctx.send = AsyncMock()
- ctx.author.mention = '@LemonLemonishBeard#0042'
+ ctx.author.mention = "@LemonLemonishBeard#0042"
eval_result = EvalResult("ERROR", 127)
self.cog.post_job = AsyncMock(return_value=eval_result)
@@ -377,8 +377,8 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
ctx.send.assert_called_once()
self.assertEqual(
ctx.send.call_args.args[0],
- '@LemonLemonishBeard#0042 :x: Your 3.11 eval job has completed with return code 127.'
- '\n\n```\nERROR\n```'
+ "@LemonLemonishBeard#0042 :x: Your 3.11 eval job has completed with return code 127."
+ "\n\n```\nERROR\n```"
)
self.cog.post_job.assert_called_once_with(job)
@@ -436,11 +436,11 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
self.bot.wait_for.assert_has_awaits(
(
call(
- 'message_edit',
+ "message_edit",
check=partial_mock(snekbox._cog.predicate_message_edit, ctx),
timeout=snekbox._cog.REDO_TIMEOUT,
),
- call('reaction_add', check=partial_mock(snekbox._cog.predicate_emoji_reaction, ctx), timeout=10)
+ call("reaction_add", check=partial_mock(snekbox._cog.predicate_emoji_reaction, ctx), timeout=10)
)
)
ctx.message.add_reaction.assert_called_once_with(snekbox._cog.REDO_EMOJI)
@@ -483,17 +483,17 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
def test_predicate_message_edit(self):
"""Test the predicate_message_edit function."""
- msg0 = MockMessage(id=1, content='abc')
- msg1 = MockMessage(id=2, content='abcdef')
- msg2 = MockMessage(id=1, content='abcdef')
+ msg0 = MockMessage(id=1, content="abc")
+ msg1 = MockMessage(id=2, content="abcdef")
+ msg2 = MockMessage(id=1, content="abcdef")
cases = (
- (msg0, msg0, False, 'same ID, same content'),
- (msg0, msg1, False, 'different ID, different content'),
- (msg0, msg2, True, 'same ID, different content')
+ (msg0, msg0, False, "same ID, same content"),
+ (msg0, msg1, False, "different ID, different content"),
+ (msg0, msg2, True, "same ID, different content")
)
for ctx_msg, new_msg, expected, testname in cases:
- with self.subTest(msg=f'Messages with {testname} return {expected}'):
+ with self.subTest(msg=f"Messages with {testname} return {expected}"):
ctx = MockContext(message=ctx_msg)
actual = snekbox._cog.predicate_message_edit(ctx, ctx_msg, new_msg)
self.assertEqual(actual, expected)
@@ -509,16 +509,16 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
invalid_reaction_id.__str__.return_value = snekbox._cog.REDO_EMOJI
invalid_user_id = MockUser(id=42)
invalid_reaction_str = MockReaction(message=MockMessage(id=1))
- invalid_reaction_str.__str__.return_value = ':longbeard:'
+ invalid_reaction_str.__str__.return_value = ":longbeard:"
cases = (
- (invalid_reaction_id, valid_user, False, 'invalid reaction ID'),
- (valid_reaction, invalid_user_id, False, 'invalid user ID'),
- (invalid_reaction_str, valid_user, False, 'invalid reaction __str__'),
- (valid_reaction, valid_user, True, 'matching attributes')
+ (invalid_reaction_id, valid_user, False, "invalid reaction ID"),
+ (valid_reaction, invalid_user_id, False, "invalid user ID"),
+ (invalid_reaction_str, valid_user, False, "invalid reaction __str__"),
+ (valid_reaction, valid_user, True, "matching attributes")
)
for reaction, user, expected, testname in cases:
- with self.subTest(msg=f'Test with {testname} and expected return {expected}'):
+ with self.subTest(msg=f"Test with {testname} and expected return {expected}"):
actual = snekbox._cog.predicate_emoji_reaction(valid_ctx, reaction, user)
self.assertEqual(actual, expected)
diff --git a/tests/bot/resources/test_resources.py b/tests/bot/resources/test_resources.py
index 73937cfa6..77f92f100 100644
--- a/tests/bot/resources/test_resources.py
+++ b/tests/bot/resources/test_resources.py
@@ -7,7 +7,7 @@ class ResourceValidationTests(unittest.TestCase):
"""Validates resources used by the bot."""
def test_stars_valid(self):
"""The resource `bot/resources/stars.json` should contain a list of strings."""
- path = Path('bot', 'resources', 'stars.json')
+ path = Path("bot", "resources", "stars.json")
content = path.read_text()
data = json.loads(content)
diff --git a/tests/bot/test_constants.py b/tests/bot/test_constants.py
index f10d6fbe8..3492021ce 100644
--- a/tests/bot/test_constants.py
+++ b/tests/bot/test_constants.py
@@ -23,11 +23,7 @@ def is_annotation_instance(value: typing.Any, annotation: typing.Any) -> bool:
def is_any_instance(value: typing.Any, types: typing.Collection) -> bool:
"""Return True if `value` is an instance of any type in `types`."""
- for type_ in types:
- if is_annotation_instance(value, type_):
- return True
-
- return False
+ return any(is_annotation_instance(value, type_) for type_ in types)
class ConstantsTests(unittest.TestCase):
@@ -39,7 +35,7 @@ class ConstantsTests(unittest.TestCase):
sections = (
cls
for (name, cls) in inspect.getmembers(constants)
- if hasattr(cls, 'section') and isinstance(cls, type)
+ if hasattr(cls, "section") and isinstance(cls, type)
)
for section in sections:
for name, annotation in section.__annotations__.items():
diff --git a/tests/bot/test_converters.py b/tests/bot/test_converters.py
index 1bb678db2..e5ccf27f7 100644
--- a/tests/bot/test_converters.py
+++ b/tests/bot/test_converters.py
@@ -1,6 +1,6 @@
import re
import unittest
-from datetime import MAXYEAR, datetime, timezone
+from datetime import MAXYEAR, UTC, datetime
from unittest.mock import MagicMock, patch
from dateutil.relativedelta import relativedelta
@@ -15,13 +15,13 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase):
@classmethod
def setUpClass(cls):
cls.context = MagicMock
- cls.context.author = 'bob'
+ cls.context.author = "bob"
- cls.fixed_utc_now = datetime.fromisoformat('2019-01-01T00:00:00+00:00')
+ cls.fixed_utc_now = datetime.fromisoformat("2019-01-01T00:00:00+00:00")
async def test_package_name_for_valid(self):
"""PackageName returns valid package names unchanged."""
- test_values = ('foo', 'le_mon', 'num83r')
+ test_values = ("foo", "le_mon", "num83r")
for name in test_values:
with self.subTest(identifier=name):
@@ -30,47 +30,46 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase):
async def test_package_name_for_invalid(self):
"""PackageName raises the proper exception for invalid package names."""
- test_values = ('text_with_a_dot.', 'UpperCaseName', 'dashed-name')
+ test_values = ("text_with_a_dot.", "UpperCaseName", "dashed-name")
for name in test_values:
- with self.subTest(identifier=name):
- with self.assertRaises(BadArgument):
- await PackageName.convert(self.context, name)
+ with self.subTest(identifier=name), self.assertRaises(BadArgument):
+ await PackageName.convert(self.context, name)
async def test_duration_converter_for_valid(self):
"""Duration returns the correct `datetime` for valid duration strings."""
test_values = (
# Simple duration strings
- ('1Y', {"years": 1}),
- ('1y', {"years": 1}),
- ('1year', {"years": 1}),
- ('1years', {"years": 1}),
- ('1m', {"months": 1}),
- ('1month', {"months": 1}),
- ('1months', {"months": 1}),
- ('1w', {"weeks": 1}),
- ('1W', {"weeks": 1}),
- ('1week', {"weeks": 1}),
- ('1weeks', {"weeks": 1}),
- ('1d', {"days": 1}),
- ('1D', {"days": 1}),
- ('1day', {"days": 1}),
- ('1days', {"days": 1}),
- ('1h', {"hours": 1}),
- ('1H', {"hours": 1}),
- ('1hour', {"hours": 1}),
- ('1hours', {"hours": 1}),
- ('1M', {"minutes": 1}),
- ('1minute', {"minutes": 1}),
- ('1minutes', {"minutes": 1}),
- ('1s', {"seconds": 1}),
- ('1S', {"seconds": 1}),
- ('1second', {"seconds": 1}),
- ('1seconds', {"seconds": 1}),
+ ("1Y", {"years": 1}),
+ ("1y", {"years": 1}),
+ ("1year", {"years": 1}),
+ ("1years", {"years": 1}),
+ ("1m", {"months": 1}),
+ ("1month", {"months": 1}),
+ ("1months", {"months": 1}),
+ ("1w", {"weeks": 1}),
+ ("1W", {"weeks": 1}),
+ ("1week", {"weeks": 1}),
+ ("1weeks", {"weeks": 1}),
+ ("1d", {"days": 1}),
+ ("1D", {"days": 1}),
+ ("1day", {"days": 1}),
+ ("1days", {"days": 1}),
+ ("1h", {"hours": 1}),
+ ("1H", {"hours": 1}),
+ ("1hour", {"hours": 1}),
+ ("1hours", {"hours": 1}),
+ ("1M", {"minutes": 1}),
+ ("1minute", {"minutes": 1}),
+ ("1minutes", {"minutes": 1}),
+ ("1s", {"seconds": 1}),
+ ("1S", {"seconds": 1}),
+ ("1second", {"seconds": 1}),
+ ("1seconds", {"seconds": 1}),
# Complex duration strings
(
- '1y1m1w1d1H1M1S',
+ "1y1m1w1d1H1M1S",
{
"years": 1,
"months": 1,
@@ -81,13 +80,13 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase):
"seconds": 1
}
),
- ('5y100S', {"years": 5, "seconds": 100}),
- ('2w28H', {"weeks": 2, "hours": 28}),
+ ("5y100S", {"years": 5, "seconds": 100}),
+ ("2w28H", {"weeks": 2, "hours": 28}),
# Duration strings with spaces
- ('1 year 2 months', {"years": 1, "months": 2}),
- ('1d 2H', {"days": 1, "hours": 2}),
- ('1 week2 days', {"weeks": 1, "days": 2}),
+ ("1 year 2 months", {"years": 1, "months": 2}),
+ ("1d 2H", {"days": 1, "hours": 2}),
+ ("1 week2 days", {"weeks": 1, "days": 2}),
)
converter = Duration()
@@ -95,7 +94,7 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase):
for duration, duration_dict in test_values:
expected_datetime = self.fixed_utc_now + relativedelta(**duration_dict)
- with patch('bot.converters.datetime') as mock_datetime:
+ with patch("bot.converters.datetime") as mock_datetime:
mock_datetime.now.return_value = self.fixed_utc_now
with self.subTest(duration=duration, duration_dict=duration_dict):
@@ -106,19 +105,19 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase):
"""Duration raises the right exception for invalid duration strings."""
test_values = (
# Units in wrong order
- '1d1w',
- '1s1y',
+ "1d1w",
+ "1s1y",
# Duplicated units
- '1 year 2 years',
- '1 M 10 minutes',
+ "1 year 2 years",
+ "1 M 10 minutes",
# Unknown substrings
- '1MVes',
- '1y3breads',
+ "1MVes",
+ "1y3breads",
# Missing amount
- 'ym',
+ "ym",
# Incorrect whitespace
" 1y",
@@ -126,15 +125,15 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase):
"1y 1m",
# Garbage
- 'Guido van Rossum',
- 'lemon lemon lemon lemon lemon lemon lemon',
+ "Guido van Rossum",
+ "lemon lemon lemon lemon lemon lemon lemon",
)
converter = Duration()
for invalid_duration in test_values:
with self.subTest(invalid_duration=invalid_duration):
- exception_message = f'`{invalid_duration}` is not a valid duration string.'
+ exception_message = f"`{invalid_duration}` is not a valid duration string."
with self.assertRaisesRegex(BadArgument, re.escape(exception_message)):
await converter.convert(self.context, invalid_duration)
@@ -151,44 +150,43 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase):
async def test_isodatetime_converter_for_valid(self):
"""ISODateTime converter returns correct datetime for valid datetime string."""
- utc = timezone.utc
test_values = (
# `YYYY-mm-ddTHH:MM:SSZ` | `YYYY-mm-dd HH:MM:SSZ`
- ('2019-09-02T02:03:05Z', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
- ('2019-09-02 02:03:05Z', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
+ ("2019-09-02T02:03:05Z", datetime(2019, 9, 2, 2, 3, 5, tzinfo=UTC)),
+ ("2019-09-02 02:03:05Z", datetime(2019, 9, 2, 2, 3, 5, tzinfo=UTC)),
# `YYYY-mm-ddTHH:MM:SS±HH:MM` | `YYYY-mm-dd HH:MM:SS±HH:MM`
- ('2019-09-02T03:18:05+01:15', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
- ('2019-09-02 03:18:05+01:15', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
- ('2019-09-02T00:48:05-01:15', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
- ('2019-09-02 00:48:05-01:15', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
+ ("2019-09-02T03:18:05+01:15", datetime(2019, 9, 2, 2, 3, 5, tzinfo=UTC)),
+ ("2019-09-02 03:18:05+01:15", datetime(2019, 9, 2, 2, 3, 5, tzinfo=UTC)),
+ ("2019-09-02T00:48:05-01:15", datetime(2019, 9, 2, 2, 3, 5, tzinfo=UTC)),
+ ("2019-09-02 00:48:05-01:15", datetime(2019, 9, 2, 2, 3, 5, tzinfo=UTC)),
# `YYYY-mm-ddTHH:MM:SS±HHMM` | `YYYY-mm-dd HH:MM:SS±HHMM`
- ('2019-09-02T03:18:05+0115', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
- ('2019-09-02 03:18:05+0115', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
- ('2019-09-02T00:48:05-0115', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
- ('2019-09-02 00:48:05-0115', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
+ ("2019-09-02T03:18:05+0115", datetime(2019, 9, 2, 2, 3, 5, tzinfo=UTC)),
+ ("2019-09-02 03:18:05+0115", datetime(2019, 9, 2, 2, 3, 5, tzinfo=UTC)),
+ ("2019-09-02T00:48:05-0115", datetime(2019, 9, 2, 2, 3, 5, tzinfo=UTC)),
+ ("2019-09-02 00:48:05-0115", datetime(2019, 9, 2, 2, 3, 5, tzinfo=UTC)),
# `YYYY-mm-ddTHH:MM:SS±HH` | `YYYY-mm-dd HH:MM:SS±HH`
- ('2019-09-02 03:03:05+01', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
- ('2019-09-02T01:03:05-01', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
+ ("2019-09-02 03:03:05+01", datetime(2019, 9, 2, 2, 3, 5, tzinfo=UTC)),
+ ("2019-09-02T01:03:05-01", datetime(2019, 9, 2, 2, 3, 5, tzinfo=UTC)),
# `YYYY-mm-ddTHH:MM:SS` | `YYYY-mm-dd HH:MM:SS`
- ('2019-09-02T02:03:05', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
- ('2019-09-02 02:03:05', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),
+ ("2019-09-02T02:03:05", datetime(2019, 9, 2, 2, 3, 5, tzinfo=UTC)),
+ ("2019-09-02 02:03:05", datetime(2019, 9, 2, 2, 3, 5, tzinfo=UTC)),
# `YYYY-mm-ddTHH:MM` | `YYYY-mm-dd HH:MM`
- ('2019-11-12T09:15', datetime(2019, 11, 12, 9, 15, tzinfo=utc)),
- ('2019-11-12 09:15', datetime(2019, 11, 12, 9, 15, tzinfo=utc)),
+ ("2019-11-12T09:15", datetime(2019, 11, 12, 9, 15, tzinfo=UTC)),
+ ("2019-11-12 09:15", datetime(2019, 11, 12, 9, 15, tzinfo=UTC)),
# `YYYY-mm-dd`
- ('2019-04-01', datetime(2019, 4, 1, tzinfo=utc)),
+ ("2019-04-01", datetime(2019, 4, 1, tzinfo=UTC)),
# `YYYY-mm`
- ('2019-02-01', datetime(2019, 2, 1, tzinfo=utc)),
+ ("2019-02-01", datetime(2019, 2, 1, tzinfo=UTC)),
# `YYYY`
- ('2025', datetime(2025, 1, 1, tzinfo=utc)),
+ ("2025", datetime(2025, 1, 1, tzinfo=UTC)),
)
converter = ISODateTime()
@@ -202,19 +200,19 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase):
"""ISODateTime converter raises the correct exception for invalid datetime strings."""
test_values = (
# Make sure it doesn't interfere with the Duration converter
- '1Y',
- '1d',
- '1H',
+ "1Y",
+ "1d",
+ "1H",
# Check if it fails when only providing the optional time part
- '10:10:10',
- '10:00',
+ "10:10:10",
+ "10:00",
# Invalid date format
- '19-01-01',
+ "19-01-01",
# Other non-valid strings
- 'fisk the tag master',
+ "fisk the tag master",
)
converter = ISODateTime()
@@ -249,6 +247,8 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase):
)
converter = HushDurationConverter()
for invalid_minutes_string, exception_message in test_values:
- with self.subTest(invalid_minutes_string=invalid_minutes_string, exception_message=exception_message):
- with self.assertRaisesRegex(BadArgument, re.escape(exception_message)):
- await converter.convert(self.context, invalid_minutes_string)
+ with (
+ self.subTest(invalid_minutes_string=invalid_minutes_string, exception_message=exception_message),
+ self.assertRaisesRegex(BadArgument, re.escape(exception_message)),
+ ):
+ await converter.convert(self.context, invalid_minutes_string)
diff --git a/tests/bot/test_decorators.py b/tests/bot/test_decorators.py
index 3d450caa0..6a04123a7 100644
--- a/tests/bot/test_decorators.py
+++ b/tests/bot/test_decorators.py
@@ -142,6 +142,8 @@ class InWhitelistTests(unittest.TestCase):
with unittest.mock.patch("bot.decorators.commands.check", new=lambda predicate: predicate):
predicate = in_whitelist(**test_case.kwargs)
- with self.subTest(test_description=test_case.description):
- with self.assertRaisesRegex(InWhitelistCheckFailure, exception_message):
- predicate(test_case.ctx)
+ with (
+ self.subTest(test_description=test_case.description),
+ self.assertRaisesRegex(InWhitelistCheckFailure, exception_message),
+ ):
+ predicate(test_case.ctx)
diff --git a/tests/bot/test_pagination.py b/tests/bot/test_pagination.py
index 630f2516d..cf23f1948 100644
--- a/tests/bot/test_pagination.py
+++ b/tests/bot/test_pagination.py
@@ -8,39 +8,39 @@ class LinePaginatorTests(TestCase):
def setUp(self):
"""Create a paginator for the test method."""
- self.paginator = pagination.LinePaginator(prefix='', suffix='', max_size=30,
+ self.paginator = pagination.LinePaginator(prefix="", suffix="", max_size=30,
scale_to_size=50)
def test_add_line_works_on_small_lines(self):
"""`add_line` should allow small lines to be added."""
- self.paginator.add_line('x' * (self.paginator.max_size - 3))
+ self.paginator.add_line("x" * (self.paginator.max_size - 3))
# Note that the page isn't added to _pages until it's full.
self.assertEqual(len(self.paginator._pages), 0)
def test_add_line_works_on_long_lines(self):
"""After additional lines after `max_size` is exceeded should go on the next page."""
- self.paginator.add_line('x' * self.paginator.max_size)
+ self.paginator.add_line("x" * self.paginator.max_size)
self.assertEqual(len(self.paginator._pages), 0)
# Any additional lines should start a new page after `max_size` is exceeded.
- self.paginator.add_line('x')
+ self.paginator.add_line("x")
self.assertEqual(len(self.paginator._pages), 1)
def test_add_line_continuation(self):
"""When `scale_to_size` is exceeded, remaining words should be split onto the next page."""
- self.paginator.add_line('zyz ' * (self.paginator.scale_to_size//4 + 1))
+ self.paginator.add_line("zyz " * (self.paginator.scale_to_size//4 + 1))
self.assertEqual(len(self.paginator._pages), 1)
def test_add_line_no_continuation(self):
"""If adding a new line to an existing page would exceed `max_size`, it should start a new
page rather than using continuation.
"""
- self.paginator.add_line('z' * (self.paginator.max_size - 3))
- self.paginator.add_line('z')
+ self.paginator.add_line("z" * (self.paginator.max_size - 3))
+ self.paginator.add_line("z")
self.assertEqual(len(self.paginator._pages), 1)
def test_add_line_truncates_very_long_words(self):
"""`add_line` should truncate if a single long word exceeds `scale_to_size`."""
- self.paginator.add_line('x' * (self.paginator.scale_to_size + 1))
+ self.paginator.add_line("x" * (self.paginator.scale_to_size + 1))
# Note: item at index 1 is the truncated line, index 0 is prefix
- self.assertEqual(self.paginator._current_page[1], 'x' * self.paginator.scale_to_size)
+ self.assertEqual(self.paginator._current_page[1], "x" * self.paginator.scale_to_size)
diff --git a/tests/bot/utils/test_message_cache.py b/tests/bot/utils/test_message_cache.py
index 04bfd28d1..ad3f4e8b6 100644
--- a/tests/bot/utils/test_message_cache.py
+++ b/tests/bot/utils/test_message_cache.py
@@ -157,9 +157,8 @@ class TestMessageCache(unittest.TestCase):
cache.append(msg)
for current_loop in test_cases:
- with self.subTest(current_loop=current_loop):
- with self.assertRaises(IndexError):
- cache[current_loop]
+ with self.subTest(current_loop=current_loop), self.assertRaises(IndexError):
+ cache[current_loop]
def test_slicing_with_unfilled_cache(self):
"""Test if slicing returns the correct messages if the cache is not yet fully filled."""
diff --git a/tests/bot/utils/test_time.py b/tests/bot/utils/test_time.py
index 120d65176..6244a3548 100644
--- a/tests/bot/utils/test_time.py
+++ b/tests/bot/utils/test_time.py
@@ -1,5 +1,5 @@
import unittest
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from dateutil.relativedelta import relativedelta
@@ -13,23 +13,23 @@ class TimeTests(unittest.TestCase):
"""humanize_delta should be able to handle unknown units, and will not abort."""
# Does not abort for unknown units, as the unit name is checked
# against the attribute of the relativedelta instance.
- actual = time.humanize_delta(relativedelta(days=2, hours=2), precision='elephants', max_units=2)
- self.assertEqual(actual, '2 days and 2 hours')
+ actual = time.humanize_delta(relativedelta(days=2, hours=2), precision="elephants", max_units=2)
+ self.assertEqual(actual, "2 days and 2 hours")
def test_humanize_delta_handle_high_units(self):
"""humanize_delta should be able to handle very high units."""
# Very high maximum units, but it only ever iterates over
# each value the relativedelta might have.
- actual = time.humanize_delta(relativedelta(days=2, hours=2), precision='hours', max_units=20)
- self.assertEqual(actual, '2 days and 2 hours')
+ actual = time.humanize_delta(relativedelta(days=2, hours=2), precision="hours", max_units=20)
+ self.assertEqual(actual, "2 days and 2 hours")
def test_humanize_delta_should_normal_usage(self):
"""Testing humanize delta."""
test_cases = (
- (relativedelta(days=2), 'seconds', 1, '2 days'),
- (relativedelta(days=2, hours=2), 'seconds', 2, '2 days and 2 hours'),
- (relativedelta(days=2, hours=2), 'seconds', 1, '2 days'),
- (relativedelta(days=2, hours=2), 'days', 2, '2 days'),
+ (relativedelta(days=2), "seconds", 1, "2 days"),
+ (relativedelta(days=2, hours=2), "seconds", 2, "2 days and 2 hours"),
+ (relativedelta(days=2, hours=2), "seconds", 1, "2 days"),
+ (relativedelta(days=2, hours=2), "days", 2, "2 days"),
)
for delta, precision, max_units, expected in test_cases:
@@ -43,8 +43,8 @@ class TimeTests(unittest.TestCase):
for max_units in test_cases:
with self.subTest(max_units=max_units), self.assertRaises(ValueError) as error:
- time.humanize_delta(relativedelta(days=2, hours=2), precision='hours', max_units=max_units)
- self.assertEqual(str(error.exception), 'max_units must be positive.')
+ time.humanize_delta(relativedelta(days=2, hours=2), precision="hours", max_units=max_units)
+ self.assertEqual(str(error.exception), "max_units must be positive.")
def test_format_with_duration_none_expiry(self):
"""format_with_duration should work for None expiry."""
@@ -52,9 +52,9 @@ class TimeTests(unittest.TestCase):
(None, None, None, None),
# To make sure that date_from and max_units are not touched
- (None, 'Why hello there!', None, None),
- (None, None, float('inf'), None),
- (None, 'Why hello there!', float('inf'), None),
+ (None, "Why hello there!", None, None),
+ (None, None, float("inf"), None),
+ (None, "Why hello there!", float("inf"), None),
)
for expiry, date_from, max_units, expected in test_cases:
@@ -64,10 +64,10 @@ class TimeTests(unittest.TestCase):
def test_format_with_duration_custom_units(self):
"""format_with_duration should work for custom max_units."""
test_cases = (
- ('3000-12-12T00:01:00Z', datetime(3000, 12, 11, 12, 5, 5, tzinfo=timezone.utc), 6,
- '<t:32533488060:f> (11 hours, 55 minutes and 55 seconds)'),
- ('3000-11-23T20:09:00Z', datetime(3000, 4, 25, 20, 15, tzinfo=timezone.utc), 20,
- '<t:32531918940:f> (6 months, 28 days, 23 hours and 54 minutes)')
+ ("3000-12-12T00:01:00Z", datetime(3000, 12, 11, 12, 5, 5, tzinfo=UTC), 6,
+ "<t:32533488060:f> (11 hours, 55 minutes and 55 seconds)"),
+ ("3000-11-23T20:09:00Z", datetime(3000, 4, 25, 20, 15, tzinfo=UTC), 20,
+ "<t:32531918940:f> (6 months, 28 days, 23 hours and 54 minutes)")
)
for expiry, date_from, max_units, expected in test_cases:
@@ -76,23 +76,22 @@ class TimeTests(unittest.TestCase):
def test_format_with_duration_normal_usage(self):
"""format_with_duration should work for normal usage, across various durations."""
- utc = timezone.utc
test_cases = (
- ('2019-12-12T00:01:00Z', datetime(2019, 12, 11, 12, 0, 5, tzinfo=utc), 2,
- '<t:1576108860:f> (12 hours and 55 seconds)'),
- ('2019-12-12T00:01:00Z', datetime(2019, 12, 11, 12, 0, 5, tzinfo=utc), 1, '<t:1576108860:f> (12 hours)'),
- ('2019-12-12T00:00:00Z', datetime(2019, 12, 11, 23, 59, tzinfo=utc), 2, '<t:1576108800:f> (1 minute)'),
- ('2019-11-23T20:09:00Z', datetime(2019, 11, 15, 20, 15, tzinfo=utc), 2,
- '<t:1574539740:f> (7 days and 23 hours)'),
- ('2019-11-23T20:09:00Z', datetime(2019, 4, 25, 20, 15, tzinfo=utc), 2,
- '<t:1574539740:f> (6 months and 28 days)'),
- ('2019-11-23T20:58:00Z', datetime(2019, 11, 23, 20, 53, tzinfo=utc), 2, '<t:1574542680:f> (5 minutes)'),
- ('2019-11-24T00:00:00Z', datetime(2019, 11, 23, 23, 59, 0, tzinfo=utc), 2, '<t:1574553600:f> (1 minute)'),
- ('2019-11-23T23:59:00Z', datetime(2017, 7, 21, 23, 0, tzinfo=utc), 2,
- '<t:1574553540:f> (2 years and 4 months)'),
- ('2019-11-23T23:59:00Z', datetime(2019, 11, 23, 23, 49, 5, tzinfo=utc), 2,
- '<t:1574553540:f> (9 minutes and 55 seconds)'),
- (None, datetime(2019, 11, 23, 23, 49, 5), 2, None),
+ ("2019-12-12T00:01:00Z", datetime(2019, 12, 11, 12, 0, 5, tzinfo=UTC), 2,
+ "<t:1576108860:f> (12 hours and 55 seconds)"),
+ ("2019-12-12T00:01:00Z", datetime(2019, 12, 11, 12, 0, 5, tzinfo=UTC), 1, "<t:1576108860:f> (12 hours)"),
+ ("2019-12-12T00:00:00Z", datetime(2019, 12, 11, 23, 59, tzinfo=UTC), 2, "<t:1576108800:f> (1 minute)"),
+ ("2019-11-23T20:09:00Z", datetime(2019, 11, 15, 20, 15, tzinfo=UTC), 2,
+ "<t:1574539740:f> (7 days and 23 hours)"),
+ ("2019-11-23T20:09:00Z", datetime(2019, 4, 25, 20, 15, tzinfo=UTC), 2,
+ "<t:1574539740:f> (6 months and 28 days)"),
+ ("2019-11-23T20:58:00Z", datetime(2019, 11, 23, 20, 53, tzinfo=UTC), 2, "<t:1574542680:f> (5 minutes)"),
+ ("2019-11-24T00:00:00Z", datetime(2019, 11, 23, 23, 59, 0, tzinfo=UTC), 2, "<t:1574553600:f> (1 minute)"),
+ ("2019-11-23T23:59:00Z", datetime(2017, 7, 21, 23, 0, tzinfo=UTC), 2,
+ "<t:1574553540:f> (2 years and 4 months)"),
+ ("2019-11-23T23:59:00Z", datetime(2019, 11, 23, 23, 49, 5, tzinfo=UTC), 2,
+ "<t:1574553540:f> (9 minutes and 55 seconds)"),
+ (None, datetime(2019, 11, 23, 23, 49, 5, tzinfo=UTC), 2, None),
)
for expiry, date_from, max_units, expected in test_cases:
@@ -106,8 +105,8 @@ class TimeTests(unittest.TestCase):
def test_until_expiration_with_duration_custom_units(self):
"""until_expiration should work for custom max_units."""
test_cases = (
- ('3000-12-12T00:01:00Z', '<t:32533488060:R>'),
- ('3000-11-23T20:09:00Z', '<t:32531918940:R>')
+ ("3000-12-12T00:01:00Z", "<t:32533488060:R>"),
+ ("3000-11-23T20:09:00Z", "<t:32531918940:R>")
)
for expiry, expected in test_cases:
@@ -117,11 +116,11 @@ class TimeTests(unittest.TestCase):
def test_until_expiration_normal_usage(self):
"""until_expiration should work for normal usage, across various durations."""
test_cases = (
- ('3000-12-12T00:01:00Z', '<t:32533488060:R>'),
- ('3000-12-12T00:01:00Z', '<t:32533488060:R>'),
- ('3000-12-12T00:00:00Z', '<t:32533488000:R>'),
- ('3000-11-23T20:09:00Z', '<t:32531918940:R>'),
- ('3000-11-23T20:09:00Z', '<t:32531918940:R>'),
+ ("3000-12-12T00:01:00Z", "<t:32533488060:R>"),
+ ("3000-12-12T00:01:00Z", "<t:32533488060:R>"),
+ ("3000-12-12T00:00:00Z", "<t:32533488000:R>"),
+ ("3000-11-23T20:09:00Z", "<t:32531918940:R>"),
+ ("3000-11-23T20:09:00Z", "<t:32531918940:R>"),
)
for expiry, expected in test_cases:
diff --git a/tests/helpers.py b/tests/helpers.py
index 020f1aee5..bb12c4977 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -5,7 +5,7 @@ import itertools
import logging
import unittest.mock
from asyncio import AbstractEventLoop
-from typing import Iterable, Optional
+from collections.abc import Iterable
import discord
from aiohttp import ClientSession
@@ -79,7 +79,7 @@ class CustomMockMixin:
additional_spec_asyncs = None
def __init__(self, **kwargs):
- name = kwargs.pop('name', None) # `name` has special meaning for Mock classes, so we need to set it manually.
+ name = kwargs.pop("name", None) # `name` has special meaning for Mock classes, so we need to set it manually.
super().__init__(spec_set=self.spec_set, **kwargs)
if self.additional_spec_asyncs:
@@ -101,7 +101,7 @@ class CustomMockMixin:
This override will look for an attribute called `child_mock_type` and use that as the type of the child mock.
"""
_new_name = kw.get("_new_name")
- if _new_name in self.__dict__['_spec_asyncs']:
+ if _new_name in self.__dict__["_spec_asyncs"]:
return unittest.mock.AsyncMock(**kw)
_type = type(self)
@@ -121,23 +121,23 @@ class CustomMockMixin:
# Create a guild instance to get a realistic Mock of `discord.Guild`
guild_data = {
- 'id': 1,
- 'name': 'guild',
- 'region': 'Europe',
- 'verification_level': 2,
- 'default_notications': 1,
- 'afk_timeout': 100,
- 'icon': "icon.png",
- 'banner': 'banner.png',
- 'mfa_level': 1,
- 'splash': 'splash.png',
- 'system_channel_id': 464033278631084042,
- 'description': 'mocking is fun',
- 'max_presences': 10_000,
- 'max_members': 100_000,
- 'preferred_locale': 'UTC',
- 'owner_id': 1,
- 'afk_channel_id': 464033278631084042,
+ "id": 1,
+ "name": "guild",
+ "region": "Europe",
+ "verification_level": 2,
+ "default_notications": 1,
+ "afk_timeout": 100,
+ "icon": "icon.png",
+ "banner": "banner.png",
+ "mfa_level": 1,
+ "splash": "splash.png",
+ "system_channel_id": 464033278631084042,
+ "description": "mocking is fun",
+ "max_presences": 10_000,
+ "max_members": 100_000,
+ "preferred_locale": "UTC",
+ "owner_id": 1,
+ "afk_channel_id": 464033278631084042,
}
guild_instance = discord.Guild(data=guild_data, state=unittest.mock.MagicMock())
@@ -170,8 +170,8 @@ class MockGuild(CustomMockMixin, unittest.mock.Mock, HashableMixin):
"""
spec_set = guild_instance
- def __init__(self, roles: Optional[Iterable[MockRole]] = None, **kwargs) -> None:
- default_kwargs = {'id': next(self.discord_id), 'members': [], "chunked": True}
+ def __init__(self, roles: Iterable[MockRole] | None = None, **kwargs) -> None:
+ default_kwargs = {"id": next(self.discord_id), "members": [], "chunked": True}
super().__init__(**collections.ChainMap(kwargs, default_kwargs))
self.roles = [MockRole(name="@everyone", position=1, id=0)]
@@ -180,7 +180,7 @@ class MockGuild(CustomMockMixin, unittest.mock.Mock, HashableMixin):
# Create a Role instance to get a realistic Mock of `discord.Role`
-role_data = {'name': 'role', 'id': 1}
+role_data = {"name": "role", "id": 1}
role_instance = discord.Role(guild=guild_instance, state=unittest.mock.MagicMock(), data=role_data)
@@ -195,11 +195,11 @@ class MockRole(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin):
def __init__(self, **kwargs) -> None:
default_kwargs = {
- 'id': next(self.discord_id),
- 'name': 'role',
- 'position': 1,
- 'colour': discord.Colour(0xdeadbf),
- 'permissions': discord.Permissions(),
+ "id": next(self.discord_id),
+ "name": "role",
+ "position": 1,
+ "colour": discord.Colour(0xdeadbf),
+ "permissions": discord.Permissions(),
}
super().__init__(**collections.ChainMap(kwargs, default_kwargs))
@@ -209,8 +209,8 @@ class MockRole(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin):
if isinstance(self.permissions, int):
self.permissions = discord.Permissions(self.permissions)
- if 'mention' not in kwargs:
- self.mention = f'&{self.name}'
+ if "mention" not in kwargs:
+ self.mention = f"&{self.name}"
def __lt__(self, other):
"""Simplified position-based comparisons similar to those of `discord.Role`."""
@@ -222,7 +222,7 @@ class MockRole(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin):
# Create a Member instance to get a realistic Mock of `discord.Member`
-member_data = {'user': 'lemon', 'roles': [1], 'flags': 2}
+member_data = {"user": "lemon", "roles": [1], "flags": 2}
state_mock = unittest.mock.MagicMock()
member_instance = discord.Member(data=member_data, guild=guild_instance, state=state_mock)
@@ -236,8 +236,8 @@ class MockMember(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin
"""
spec_set = member_instance
- def __init__(self, roles: Optional[Iterable[MockRole]] = None, **kwargs) -> None:
- default_kwargs = {'name': 'member', 'id': next(self.discord_id), 'bot': False, "pending": False}
+ def __init__(self, roles: Iterable[MockRole] | None = None, **kwargs) -> None:
+ default_kwargs = {"name": "member", "id": next(self.discord_id), "bot": False, "pending": False}
super().__init__(**collections.ChainMap(kwargs, default_kwargs))
self.roles = [MockRole(name="@everyone", position=1, id=0)]
@@ -245,7 +245,7 @@ class MockMember(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin
self.roles.extend(roles)
self.top_role = max(self.roles)
- if 'mention' not in kwargs:
+ if "mention" not in kwargs:
self.mention = f"@{self.name}"
@@ -269,10 +269,10 @@ class MockUser(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin):
spec_set = user_instance
def __init__(self, **kwargs) -> None:
- default_kwargs = {'name': 'user', 'id': next(self.discord_id), 'bot': False}
+ default_kwargs = {"name": "user", "id": next(self.discord_id), "bot": False}
super().__init__(**collections.ChainMap(kwargs, default_kwargs))
- if 'mention' not in kwargs:
+ if "mention" not in kwargs:
self.mention = f"@{self.name}"
@@ -331,16 +331,16 @@ class MockBot(CustomMockMixin, unittest.mock.MagicMock):
# Create a TextChannel instance to get a realistic MagicMock of `discord.TextChannel`
channel_data = {
- 'id': 1,
- 'type': 'TextChannel',
- 'name': 'channel',
- 'parent_id': 1234567890,
- 'topic': 'topic',
- 'position': 1,
- 'nsfw': False,
- 'last_message_id': 1,
- 'bitrate': 1337,
- 'user_limit': 25,
+ "id": 1,
+ "type": "TextChannel",
+ "name": "channel",
+ "parent_id": 1234567890,
+ "topic": "topic",
+ "position": 1,
+ "nsfw": False,
+ "last_message_id": 1,
+ "bitrate": 1337,
+ "user_limit": 25,
}
state = unittest.mock.MagicMock()
guild = unittest.mock.MagicMock()
@@ -360,10 +360,10 @@ class MockTextChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin):
spec_set = text_channel_instance
def __init__(self, **kwargs) -> None:
- default_kwargs = {'id': next(self.discord_id), 'name': 'channel', 'guild': MockGuild()}
+ default_kwargs = {"id": next(self.discord_id), "name": "channel", "guild": MockGuild()}
super().__init__(**collections.ChainMap(kwargs, default_kwargs))
- if 'mention' not in kwargs:
+ if "mention" not in kwargs:
self.mention = f"#{self.name}"
@@ -377,10 +377,10 @@ class MockVoiceChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin):
spec_set = voice_channel_instance
def __init__(self, **kwargs) -> None:
- default_kwargs = {'id': next(self.discord_id), 'name': 'channel', 'guild': MockGuild()}
+ default_kwargs = {"id": next(self.discord_id), "name": "channel", "guild": MockGuild()}
super().__init__(**collections.ChainMap(kwargs, default_kwargs))
- if 'mention' not in kwargs:
+ if "mention" not in kwargs:
self.mention = f"#{self.name}"
@@ -401,16 +401,16 @@ class MockDMChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin):
spec_set = dm_channel_instance
def __init__(self, **kwargs) -> None:
- default_kwargs = {'id': next(self.discord_id), 'recipient': MockUser(), "me": MockUser(), 'guild': None}
+ default_kwargs = {"id": next(self.discord_id), "recipient": MockUser(), "me": MockUser(), "guild": None}
super().__init__(**collections.ChainMap(kwargs, default_kwargs))
# Create CategoryChannel instance to get a realistic MagicMock of `discord.CategoryChannel`
category_channel_data = {
- 'id': 1,
- 'type': discord.ChannelType.category,
- 'name': 'category',
- 'position': 1,
+ "id": 1,
+ "type": discord.ChannelType.category,
+ "name": "category",
+ "position": 1,
}
state = unittest.mock.MagicMock()
@@ -422,26 +422,26 @@ category_channel_instance = discord.CategoryChannel(
class MockCategoryChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin):
def __init__(self, **kwargs) -> None:
- default_kwargs = {'id': next(self.discord_id)}
+ default_kwargs = {"id": next(self.discord_id)}
super().__init__(**collections.ChainMap(kwargs, default_kwargs))
# Create a Message instance to get a realistic MagicMock of `discord.Message`
message_data = {
- 'id': 1,
- 'webhook_id': 431341013479718912,
- 'attachments': [],
- 'embeds': [],
- 'application': {"id": 4, "description": "A Python Bot", "name": "Python Discord", "icon": None},
- 'activity': 'mocking',
- 'channel': unittest.mock.MagicMock(),
- 'edited_timestamp': '2019-10-14T15:33:48+00:00',
- 'type': 'message',
- 'pinned': False,
- 'mention_everyone': False,
- 'tts': None,
- 'content': 'content',
- 'nonce': None,
+ "id": 1,
+ "webhook_id": 431341013479718912,
+ "attachments": [],
+ "embeds": [],
+ "application": {"id": 4, "description": "A Python Bot", "name": "Python Discord", "icon": None},
+ "activity": "mocking",
+ "channel": unittest.mock.MagicMock(),
+ "edited_timestamp": "2019-10-14T15:33:48+00:00",
+ "type": "message",
+ "pinned": False,
+ "mention_everyone": False,
+ "tts": None,
+ "content": "content",
+ "nonce": None,
}
state = unittest.mock.MagicMock()
channel = unittest.mock.MagicMock()
@@ -470,13 +470,13 @@ class MockContext(CustomMockMixin, unittest.mock.MagicMock):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
- self.me = kwargs.get('me', MockMember())
- self.bot = kwargs.get('bot', MockBot())
- self.guild = kwargs.get('guild', MockGuild())
- self.author = kwargs.get('author', MockMember())
- self.channel = kwargs.get('channel', MockTextChannel())
- self.message = kwargs.get('message', MockMessage())
- self.invoked_from_error_handler = kwargs.get('invoked_from_error_handler', False)
+ self.me = kwargs.get("me", MockMember())
+ self.bot = kwargs.get("bot", MockBot())
+ self.guild = kwargs.get("guild", MockGuild())
+ self.author = kwargs.get("author", MockMember())
+ self.channel = kwargs.get("channel", MockTextChannel())
+ self.message = kwargs.get("message", MockMessage())
+ self.invoked_from_error_handler = kwargs.get("invoked_from_error_handler", False)
class MockInteraction(CustomMockMixin, unittest.mock.MagicMock):
@@ -489,13 +489,13 @@ class MockInteraction(CustomMockMixin, unittest.mock.MagicMock):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
- self.me = kwargs.get('me', MockMember())
- self.client = kwargs.get('client', MockBot())
- self.guild = kwargs.get('guild', MockGuild())
- self.user = kwargs.get('user', MockMember())
- self.channel = kwargs.get('channel', MockTextChannel())
- self.message = kwargs.get('message', MockMessage())
- self.invoked_from_error_handler = kwargs.get('invoked_from_error_handler', False)
+ self.me = kwargs.get("me", MockMember())
+ self.client = kwargs.get("client", MockBot())
+ self.guild = kwargs.get("guild", MockGuild())
+ self.user = kwargs.get("user", MockMember())
+ self.channel = kwargs.get("channel", MockTextChannel())
+ self.message = kwargs.get("message", MockMessage())
+ self.invoked_from_error_handler = kwargs.get("invoked_from_error_handler", False)
attachment_instance = discord.Attachment(data=unittest.mock.MagicMock(id=1), state=unittest.mock.MagicMock())
@@ -543,10 +543,10 @@ class MockMessage(CustomMockMixin, unittest.mock.MagicMock):
spec_set = message_instance
def __init__(self, **kwargs) -> None:
- default_kwargs = {'attachments': []}
+ default_kwargs = {"attachments": []}
super().__init__(**collections.ChainMap(kwargs, default_kwargs))
- self.author = kwargs.get('author', MockMember())
- self.channel = kwargs.get('channel', MockTextChannel())
+ self.author = kwargs.get("author", MockMember())
+ self.channel = kwargs.get("channel", MockTextChannel())
class MockInteractionMessage(MockMessage):
@@ -556,10 +556,9 @@ class MockInteractionMessage(MockMessage):
Instances of this class will follow the specifications of `discord.InteractionMessage` instances. For more
information, see the `MockGuild` docstring.
"""
- pass
-emoji_data = {'require_colons': True, 'managed': True, 'id': 1, 'name': 'hyperlemon'}
+emoji_data = {"require_colons": True, "managed": True, "id": 1, "name": "hyperlemon"}
emoji_instance = discord.Emoji(guild=MockGuild(), state=unittest.mock.MagicMock(), data=emoji_data)
@@ -574,10 +573,10 @@ class MockEmoji(CustomMockMixin, unittest.mock.MagicMock):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
- self.guild = kwargs.get('guild', MockGuild())
+ self.guild = kwargs.get("guild", MockGuild())
-partial_emoji_instance = discord.PartialEmoji(animated=False, name='guido')
+partial_emoji_instance = discord.PartialEmoji(animated=False, name="guido")
class MockPartialEmoji(CustomMockMixin, unittest.mock.MagicMock):
@@ -590,7 +589,7 @@ class MockPartialEmoji(CustomMockMixin, unittest.mock.MagicMock):
spec_set = partial_emoji_instance
-reaction_instance = discord.Reaction(message=MockMessage(), data={'me': True}, emoji=MockEmoji())
+reaction_instance = discord.Reaction(message=MockMessage(), data={"me": True}, emoji=MockEmoji())
class MockReaction(CustomMockMixin, unittest.mock.MagicMock):
@@ -605,8 +604,8 @@ class MockReaction(CustomMockMixin, unittest.mock.MagicMock):
def __init__(self, **kwargs) -> None:
_users = kwargs.pop("users", [])
super().__init__(**kwargs)
- self.emoji = kwargs.get('emoji', MockEmoji())
- self.message = kwargs.get('message', MockMessage())
+ self.emoji = kwargs.get("emoji", MockEmoji())
+ self.message = kwargs.get("message", MockMessage())
user_iterator = unittest.mock.AsyncMock()
user_iterator.__aiter__.return_value = _users
diff --git a/tests/test_base.py b/tests/test_base.py
index 365805a71..f1fb1a514 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -30,15 +30,19 @@ class LoggingTestCaseTests(unittest.TestCase):
r"1 logs of DEBUG or higher were triggered on root:\n"
r'<LogRecord: tests\.test_base, [\d]+, .+[/\\]tests[/\\]test_base\.py, [\d]+, "Log!">'
)
- with self.assertRaisesRegex(AssertionError, msg_regex):
- with LoggingTestCase.assertNotLogs(self, level=logging.DEBUG):
- self.log.debug("Log!")
+ with (
+ self.assertRaisesRegex(AssertionError, msg_regex),
+ LoggingTestCase.assertNotLogs(self, level=logging.DEBUG),
+ ):
+ self.log.debug("Log!")
def test_assert_not_logs_reraises_unexpected_exception_in_managed_context(self):
"""Test if LoggingTestCase.assertNotLogs reraises an unexpected exception."""
- with self.assertRaises(ValueError, msg="test exception"):
- with LoggingTestCase.assertNotLogs(self, level=logging.DEBUG):
- raise ValueError("test exception")
+ with (
+ self.assertRaises(ValueError, msg="test exception"),
+ LoggingTestCase.assertNotLogs(self, level=logging.DEBUG),
+ ):
+ raise ValueError("test exception")
def test_assert_not_logs_restores_old_logging_settings(self):
"""Test if LoggingTestCase.assertNotLogs reraises an unexpected exception."""
@@ -56,9 +60,8 @@ class LoggingTestCaseTests(unittest.TestCase):
def test_logging_test_case_works_with_logger_instance(self):
"""Test if the LoggingTestCase captures logging for provided logger."""
log = get_logger("new_logger")
- with self.assertRaises(AssertionError):
- with LoggingTestCase.assertNotLogs(self, logger=log):
- log.info("Hello, this should raise an AssertionError")
+ with self.assertRaises(AssertionError), LoggingTestCase.assertNotLogs(self, logger=log):
+ log.info("Hello, this should raise an AssertionError")
def test_logging_test_case_respects_alternative_logger(self):
"""Test if LoggingTestCase only checks the provided logger."""
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index b2686b1d0..fa7d0eb44 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -144,13 +144,13 @@ class DiscordMocksTests(unittest.TestCase):
def test_mocks_allows_access_to_attributes_part_of_spec(self):
"""Accessing attributes that are valid for the objects they mock should succeed."""
mocks = (
- (helpers.MockGuild(), 'name'),
- (helpers.MockRole(), 'hoist'),
- (helpers.MockMember(), 'display_name'),
- (helpers.MockBot(), 'user'),
- (helpers.MockContext(), 'invoked_with'),
- (helpers.MockTextChannel(), 'last_message'),
- (helpers.MockMessage(), 'mention_everyone'),
+ (helpers.MockGuild(), "name"),
+ (helpers.MockRole(), "hoist"),
+ (helpers.MockMember(), "display_name"),
+ (helpers.MockBot(), "user"),
+ (helpers.MockContext(), "invoked_with"),
+ (helpers.MockTextChannel(), "last_message"),
+ (helpers.MockMessage(), "mention_everyone"),
)
for mock, valid_attribute in mocks:
@@ -161,8 +161,8 @@ class DiscordMocksTests(unittest.TestCase):
msg = f"accessing valid attribute `{valid_attribute}` raised an AttributeError"
self.fail(msg)
- @unittest.mock.patch(f'{__name__}.DiscordMocksTests.subTest')
- @unittest.mock.patch(f'{__name__}.getattr')
+ @unittest.mock.patch(f"{__name__}.DiscordMocksTests.subTest")
+ @unittest.mock.patch(f"{__name__}.getattr")
def test_mock_allows_access_to_attributes_test(self, mock_getattr, mock_subtest):
"""The valid attribute test should raise an AssertionError after an AttributeError."""
mock_getattr.side_effect = AttributeError
@@ -184,9 +184,8 @@ class DiscordMocksTests(unittest.TestCase):
)
for mock in mocks:
- with self.subTest(mock=mock):
- with self.assertRaises(AttributeError):
- mock.the_cake_is_a_lie
+ with self.subTest(mock=mock), self.assertRaises(AttributeError):
+ mock.the_cake_is_a_lie # noqa: B018
def test_mocks_use_mention_when_provided_as_kwarg(self):
"""The mock should use the passed `mention` instead of the default one if present."""
@@ -333,9 +332,9 @@ class MockObjectTests(unittest.TestCase):
(helpers.MockBot, "owner_id"),
(helpers.MockContext, "command_failed"),
(helpers.MockMessage, "mention_everyone"),
- (helpers.MockEmoji, 'managed'),
- (helpers.MockPartialEmoji, 'url'),
- (helpers.MockReaction, 'me'),
+ (helpers.MockEmoji, "managed"),
+ (helpers.MockPartialEmoji, "url"),
+ (helpers.MockReaction, "me"),
)
for mock_type, valid_attribute in test_values:
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index 987b7c790..000000000
--- a/tox.ini
+++ /dev/null
@@ -1,19 +0,0 @@
-[flake8]
-max-line-length=120
-docstring-convention=all
-import-order-style=pycharm
-application_import_names=bot,tests
-exclude=.cache,.venv,.git,constants.py
-extend-ignore=
- B311,W503,E226,S311,T000,E731
- # Missing Docstrings
- D100,D104,D105,D107,
- # Docstring Whitespace
- D203,D212,D214,D215,
- # Docstring Quotes
- D301,D302,
- # Docstring Content
- D400,D401,D402,D404,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414,D416,D417
- # Type Annotations
- ANN002,ANN003,ANN101,ANN102,ANN204,ANN206,ANN401
-per-file-ignores=tests/*:D,ANN