From a5b8099a7b4da288058d852a8f303a46154fb6bc Mon Sep 17 00:00:00 2001 From: wookie184 Date: Sun, 9 Jul 2023 09:44:30 +0100 Subject: Fix some test warnings (#2675) * Fix test warnings * Remove unnecessary event_loop parameters This is not necessary as the current event loop will be used which is the same as self.bot.loop. Removing these should slightly improve test speed as the mock for self.bot.loop doesn't need to be created. --- tests/bot/exts/backend/test_logging.py | 5 +++-- tests/bot/exts/moderation/test_incidents.py | 8 +++++--- tests/helpers.py | 10 ++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/backend/test_logging.py b/tests/bot/exts/backend/test_logging.py index 466f207d9..3a41220ad 100644 --- a/tests/bot/exts/backend/test_logging.py +++ b/tests/bot/exts/backend/test_logging.py @@ -3,7 +3,7 @@ from unittest.mock import patch from bot import constants from bot.exts.backend.logging import Logging -from tests.helpers import MockBot, MockTextChannel +from tests.helpers import MockBot, MockTextChannel, no_create_task class LoggingTests(unittest.IsolatedAsyncioTestCase): @@ -11,7 +11,8 @@ class LoggingTests(unittest.IsolatedAsyncioTestCase): def setUp(self): self.bot = MockBot() - self.cog = Logging(self.bot) + with no_create_task(): + self.cog = Logging(self.bot) self.dev_log = MockTextChannel(id=1234, name="dev-log") @patch("bot.exts.backend.logging.DEBUG_MODE", False) diff --git a/tests/bot/exts/moderation/test_incidents.py b/tests/bot/exts/moderation/test_incidents.py index bb337aeba..61f64da44 100644 --- a/tests/bot/exts/moderation/test_incidents.py +++ b/tests/bot/exts/moderation/test_incidents.py @@ -17,7 +17,7 @@ from bot.utils.time import TimestampFormats, discord_timestamp from tests.base import RedisTestCase from tests.helpers import ( MockAsyncWebhook, MockAttachment, MockBot, MockMember, MockMessage, MockReaction, MockRole, MockTextChannel, - MockUser + MockUser, no_create_task ) CURRENT_TIME = datetime.datetime(2022, 1, 1, tzinfo=datetime.UTC) @@ -306,7 +306,8 @@ class TestIncidents(RedisTestCase): Note that this will not schedule `crawl_incidents` in the background, as everything is being mocked. The `crawl_task` attribute will end up being None. """ - self.cog_instance = incidents.Incidents(MockBot()) + with no_create_task(): + self.cog_instance = incidents.Incidents(MockBot()) @patch("asyncio.sleep", AsyncMock()) # Prevent the coro from sleeping to speed up the test @@ -458,7 +459,8 @@ class TestMakeConfirmationTask(TestIncidents): If this function begins to fail, first check that `created_check` is being retrieved correctly. It should be the function that is built locally in the tested method. """ - self.cog_instance.make_confirmation_task(MockMessage(id=123)) + with no_create_task(): + self.cog_instance.make_confirmation_task(MockMessage(id=123)) self.cog_instance.bot.wait_for.assert_called_once() created_check = self.cog_instance.bot.wait_for.call_args.kwargs["check"] diff --git a/tests/helpers.py b/tests/helpers.py index 26ac42697..580848c25 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -6,6 +6,7 @@ import logging import unittest.mock from asyncio import AbstractEventLoop from collections.abc import Iterable +from contextlib import contextmanager from functools import cached_property import discord @@ -664,3 +665,12 @@ class MockAsyncWebhook(CustomMockMixin, unittest.mock.MagicMock): """ spec_set = webhook_instance additional_spec_asyncs = ("send", "edit", "delete", "execute") + +@contextmanager +def no_create_task(): + def side_effect(coro, *_, **__): + coro.close() + + with unittest.mock.patch("pydis_core.utils.scheduling.create_task") as create_task: + create_task.side_effect = side_effect + yield -- cgit v1.2.3 From 0994d88979cbcf8ccf37168f4b4696a302086698 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 18 Jun 2023 20:25:22 +0100 Subject: Use the new pydis_core send_to_paste_service function --- bot/exts/filtering/filtering.py | 9 ++- bot/exts/moderation/dm_relay.py | 9 ++- bot/exts/moderation/metabase.py | 12 ++-- bot/exts/utils/internal.py | 12 ++-- bot/exts/utils/snekbox/_cog.py | 14 +++-- bot/utils/__init__.py | 4 -- bot/utils/services.py | 83 ------------------------- tests/bot/exts/utils/snekbox/test_snekbox.py | 13 +--- tests/bot/utils/test_services.py | 90 ---------------------------- 9 files changed, 40 insertions(+), 206 deletions(-) delete mode 100644 bot/utils/services.py delete mode 100644 tests/bot/utils/test_services.py (limited to 'tests') diff --git a/bot/exts/filtering/filtering.py b/bot/exts/filtering/filtering.py index d69bd9644..629f9471e 100644 --- a/bot/exts/filtering/filtering.py +++ b/bot/exts/filtering/filtering.py @@ -18,6 +18,7 @@ from discord.ext import commands, tasks from discord.ext.commands import BadArgument, Cog, Context, command, has_any_role from pydis_core.site_api import ResponseCodeError from pydis_core.utils import scheduling +from pydis_core.utils.paste_service import PasteTooLongError, PasteUploadError, send_to_paste_service import bot import bot.exts.filtering._ui.filter as filters_ui @@ -47,7 +48,6 @@ from bot.pagination import LinePaginator from bot.utils.channel import is_mod_channel from bot.utils.lock import lock_arg from bot.utils.message_cache import MessageCache -from bot.utils.services import PasteTooLongError, PasteUploadError, send_to_paste_service log = get_logger(__name__) @@ -1452,7 +1452,12 @@ class Filtering(Cog): raise report = discord.utils.remove_markdown(report) try: - paste_resp = await send_to_paste_service(report, extension="txt") + resp = await send_to_paste_service( + contents=report, + http_session=self.bot.http_session, + lexer="text", + ) + paste_resp = resp["link"] except (ValueError, PasteTooLongError, PasteUploadError): paste_resp = ":warning: Failed to upload report to paste service" file_buffer = io.StringIO(report) diff --git a/bot/exts/moderation/dm_relay.py b/bot/exts/moderation/dm_relay.py index bf0b96a58..5bec3b10f 100644 --- a/bot/exts/moderation/dm_relay.py +++ b/bot/exts/moderation/dm_relay.py @@ -1,11 +1,11 @@ import discord from discord.ext.commands import Cog, Context, command, has_any_role +from pydis_core.utils.paste_service import PasteTooLongError, PasteUploadError, send_to_paste_service from bot.bot import Bot from bot.constants import Emojis, MODERATION_ROLES from bot.log import get_logger from bot.utils.channel import is_mod_channel -from bot.utils.services import PasteTooLongError, PasteUploadError, send_to_paste_service log = get_logger(__name__) @@ -54,7 +54,12 @@ class DMRelay(Cog): f"Channel ID: {user.dm_channel.id}\n\n" ) try: - message = await send_to_paste_service(metadata + output, extension="txt") + resp = await send_to_paste_service( + contents=metadata + output, + lexer="text", + http_session=self.bot.http_session, + ) + message = resp["link"] except PasteTooLongError: message = f"{Emojis.cross_mark} Too long to upload to paste service." except PasteUploadError: diff --git a/bot/exts/moderation/metabase.py b/bot/exts/moderation/metabase.py index f5483325f..1c0a2c191 100644 --- a/bot/exts/moderation/metabase.py +++ b/bot/exts/moderation/metabase.py @@ -9,14 +9,13 @@ from aiohttp.client_exceptions import ClientResponseError from arrow import Arrow from async_rediscache import RedisCache from discord.ext.commands import Cog, Context, group, has_any_role +from pydis_core.utils.paste_service import PasteTooLongError, PasteUploadError, send_to_paste_service from pydis_core.utils.scheduling import Scheduler from bot.bot import Bot from bot.constants import Metabase as MetabaseConfig, Roles from bot.log import get_logger -from bot.utils import send_to_paste_service from bot.utils.channel import is_mod_channel -from bot.utils.services import PasteTooLongError, PasteUploadError log = get_logger(__name__) @@ -129,6 +128,7 @@ class Metabase(Cog): async with self.bot.http_session.post(url, headers=self.headers, raise_for_status=True) as resp: if extension == "csv": + extension = "text" # paste site doesn't support csv as a lexer out = await resp.text(encoding="utf-8") # Save the output for use with int e self.exports[question_id] = list(csv.DictReader(StringIO(out))) @@ -142,13 +142,17 @@ class Metabase(Cog): out = json.dumps(out, indent=4, sort_keys=True) try: - paste_link = await send_to_paste_service(out, extension=extension) + resp = await send_to_paste_service( + contents=out, + lexer=extension, + http_session=self.bot.http_session, + ) except PasteTooLongError: message = f":x: {ctx.author.mention} Too long to upload to paste service." except PasteUploadError: message = f":x: {ctx.author.mention} Failed to upload to paste service." else: - message = f":+1: {ctx.author.mention} Here's your link: {paste_link}" + message = f":+1: {ctx.author.mention} Here's your link: {resp['link']}" await ctx.send( f"{message}\nYou can also access this data within internal eval by doing: " diff --git a/bot/exts/utils/internal.py b/bot/exts/utils/internal.py index cdaa9a61b..58140b3c0 100644 --- a/bot/exts/utils/internal.py +++ b/bot/exts/utils/internal.py @@ -11,12 +11,12 @@ from typing import Any import arrow import discord from discord.ext.commands import Cog, Context, group, has_any_role, is_owner +from pydis_core.utils.paste_service import PasteTooLongError, PasteUploadError, send_to_paste_service from bot.bot import Bot from bot.constants import DEBUG_MODE, Roles from bot.log import get_logger -from bot.utils import find_nth_occurrence, send_to_paste_service -from bot.utils.services import PasteTooLongError, PasteUploadError +from bot.utils import find_nth_occurrence log = get_logger(__name__) @@ -196,13 +196,17 @@ async def func(): # (None,) -> Any if len(out) > truncate_index: try: - paste_link = await send_to_paste_service(out, extension="py") + resp = await send_to_paste_service( + contents=out, + lexer="python", + http_session=self.bot.http_session, + ) except PasteTooLongError: paste_text = "too long to upload to paste service." except PasteUploadError: paste_text = "failed to upload contents to paste service." else: - paste_text = f"full contents at {paste_link}" + paste_text = f"full contents at {resp['link']}" await ctx.send( f"```py\n{out[:truncate_index]}\n```" diff --git a/bot/exts/utils/snekbox/_cog.py b/bot/exts/utils/snekbox/_cog.py index 9d0ced4ef..f07bcb7df 100644 --- a/bot/exts/utils/snekbox/_cog.py +++ b/bot/exts/utils/snekbox/_cog.py @@ -11,6 +11,7 @@ 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 from pydis_core.utils import interactions +from pydis_core.utils.paste_service import PasteTooLongError, PasteUploadError, send_to_paste_service from pydis_core.utils.regex import FORMATTED_CODE_REGEX, RAW_CODE_REGEX from bot.bot import Bot @@ -21,9 +22,7 @@ from bot.exts.help_channels._channel import is_help_forum_post from bot.exts.utils.snekbox._eval import EvalJob, EvalResult from bot.exts.utils.snekbox._io import FileAttachment from bot.log import get_logger -from bot.utils import send_to_paste_service from bot.utils.lock import LockedResourceError, lock_arg -from bot.utils.services import PasteTooLongError, PasteUploadError if TYPE_CHECKING: from bot.exts.filtering.filtering import Filtering @@ -74,7 +73,6 @@ if not hasattr(sys, "_setup_finished"): {setup} """ -MAX_PASTE_LENGTH = 10_000 # Max to display in a codeblock before sending to a paste service # This also applies to text files MAX_OUTPUT_BLOCK_LINES = 10 @@ -205,13 +203,17 @@ class Snekbox(Cog): async with self.bot.http_session.post(URLs.snekbox_eval_api, json=data, raise_for_status=True) as resp: return EvalResult.from_dict(await resp.json()) - @staticmethod - async def upload_output(output: str) -> str | None: + async def upload_output(self, 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...") try: - return await send_to_paste_service(output, extension="txt", max_length=MAX_PASTE_LENGTH) + paste_link = await send_to_paste_service( + contents=output, + lexer="text", + http_session=self.bot.http_session, + ) + return paste_link["link"] except PasteTooLongError: return "too long to upload" except PasteUploadError: diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py index f576b7f11..f803add1c 100644 --- a/bot/utils/__init__.py +++ b/bot/utils/__init__.py @@ -1,12 +1,8 @@ from bot.utils.helpers import CogABCMeta, find_nth_occurrence, has_lines, pad_base64 -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", ] diff --git a/bot/utils/services.py b/bot/utils/services.py deleted file mode 100644 index a25548510..000000000 --- a/bot/utils/services.py +++ /dev/null @@ -1,83 +0,0 @@ -from aiohttp import ClientConnectorError - -import bot -from bot.constants import URLs -from bot.log import get_logger - -log = get_logger(__name__) - -FAILED_REQUEST_ATTEMPTS = 3 -MAX_PASTE_LENGTH = 100_000 - - -class PasteUploadError(Exception): - """Raised when an error is encountered uploading to the paste service.""" - - -class PasteTooLongError(Exception): - """Raised when content is too large to upload to the paste service.""" - - -async def send_to_paste_service(contents: str, *, extension: str = "", max_length: int = MAX_PASTE_LENGTH) -> str: - """ - Upload `contents` to the paste service. - - Add `extension` to the output URL. Use `max_length` to limit the allowed contents length - to lower than the maximum allowed by the paste service. - - Raise `ValueError` if `max_length` is greater than the maximum allowed by the paste service. - Raise `PasteTooLongError` if `contents` is too long to upload, and `PasteUploadError` if uploading fails. - - Return the generated URL with the extension. - """ - if max_length > MAX_PASTE_LENGTH: - raise ValueError(f"`max_length` must not be greater than {MAX_PASTE_LENGTH}") - - extension = extension and f".{extension}" - - contents_size = len(contents.encode()) - if contents_size > max_length: - log.info("Contents too large to send to paste service.") - raise PasteTooLongError(f"Contents of size {contents_size} greater than maximum size {max_length}") - - log.debug(f"Sending contents of size {contents_size} bytes to paste service.") - paste_url = URLs.paste_service.format(key="documents") - for attempt in range(1, FAILED_REQUEST_ATTEMPTS + 1): - try: - async with bot.instance.http_session.post(paste_url, data=contents) as response: - response_json = await response.json() - except ClientConnectorError: - log.warning( - f"Failed to connect to paste service at url {paste_url}, " - f"trying again ({attempt}/{FAILED_REQUEST_ATTEMPTS})." - ) - continue - except Exception: - log.exception( - f"An unexpected error has occurred during handling of the request, " - f"trying again ({attempt}/{FAILED_REQUEST_ATTEMPTS})." - ) - continue - - if "message" in response_json: - log.warning( - f"Paste service returned error {response_json['message']} with status code {response.status}, " - f"trying again ({attempt}/{FAILED_REQUEST_ATTEMPTS})." - ) - continue - 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 - - if extension == ".py": - return paste_link - - return paste_link + "?noredirect" - - log.warning( - f"Got unexpected JSON response from paste service: {response_json}\n" - f"trying again ({attempt}/{FAILED_REQUEST_ATTEMPTS})." - ) - - raise PasteUploadError("Failed to upload contents to paste service") diff --git a/tests/bot/exts/utils/snekbox/test_snekbox.py b/tests/bot/exts/utils/snekbox/test_snekbox.py index fa28aade8..6d9b6b1e4 100644 --- a/tests/bot/exts/utils/snekbox/test_snekbox.py +++ b/tests/bot/exts/utils/snekbox/test_snekbox.py @@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, call, create_autospec, pat from discord import AllowedMentions from discord.ext import commands +from pydis_core.utils.paste_service import MAX_PASTE_SIZE from bot import constants from bot.errors import LockedResourceError @@ -56,19 +57,9 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): async def test_upload_output_reject_too_long(self): """Reject output longer than MAX_PASTE_LENGTH.""" - result = await self.cog.upload_output("-" * (snekbox._cog.MAX_PASTE_LENGTH + 1)) + result = await self.cog.upload_output("-" * (MAX_PASTE_SIZE + 1)) self.assertEqual(result, "too long to upload") - @patch("bot.exts.utils.snekbox._cog.send_to_paste_service") - async def test_upload_output(self, mock_paste_util): - """Upload the eval output to the URLs.paste_service.format(key="documents") endpoint.""" - await self.cog.upload_output("Test output.") - mock_paste_util.assert_called_once_with( - "Test output.", - extension="txt", - max_length=snekbox._cog.MAX_PASTE_LENGTH - ) - async def test_codeblock_converter(self): ctx = MockContext() cases = ( diff --git a/tests/bot/utils/test_services.py b/tests/bot/utils/test_services.py deleted file mode 100644 index 3c9e037ce..000000000 --- a/tests/bot/utils/test_services.py +++ /dev/null @@ -1,90 +0,0 @@ -import logging -import unittest -from unittest.mock import AsyncMock, MagicMock, Mock, patch - -from aiohttp import ClientConnectorError - -from bot.utils.services import ( - FAILED_REQUEST_ATTEMPTS, MAX_PASTE_LENGTH, PasteTooLongError, PasteUploadError, send_to_paste_service -) -from tests.helpers import MockBot - - -class PasteTests(unittest.IsolatedAsyncioTestCase): - def setUp(self) -> None: - patcher = patch("bot.instance", new=MockBot()) - self.bot = patcher.start() - self.addCleanup(patcher.stop) - - @patch("bot.utils.services.URLs.paste_service", "https://paste_service.com/{key}") - async def test_url_and_sent_contents(self): - """Correct url was used and post was called with expected data.""" - response = MagicMock( - json=AsyncMock(return_value={"key": ""}) - ) - self.bot.http_session.post.return_value.__aenter__.return_value = response - self.bot.http_session.post.reset_mock() - await send_to_paste_service("Content") - self.bot.http_session.post.assert_called_once_with("https://paste_service.com/documents", data="Content") - - @patch("bot.utils.services.URLs.paste_service", "https://paste_service.com/{key}") - async def test_paste_returns_correct_url_on_success(self): - """Url with specified extension is returned on successful requests.""" - key = "paste_key" - test_cases = ( - (f"https://paste_service.com/{key}.txt?noredirect", "txt"), - (f"https://paste_service.com/{key}.py", "py"), - (f"https://paste_service.com/{key}?noredirect", ""), - ) - response = MagicMock( - json=AsyncMock(return_value={"key": key}) - ) - self.bot.http_session.post.return_value.__aenter__.return_value = response - - for expected_output, extension in test_cases: - with self.subTest(msg=f"Send contents with extension {extension!r}"): - self.assertEqual( - await send_to_paste_service("", extension=extension), - expected_output - ) - - async def test_request_repeated_on_json_errors(self): - """Json with error message and invalid json are handled as errors and requests repeated.""" - test_cases = ({"message": "error"}, {"unexpected_key": None}, {}) - self.bot.http_session.post.return_value.__aenter__.return_value = response = MagicMock() - self.bot.http_session.post.reset_mock() - - for error_json in test_cases: - with self.subTest(error_json=error_json): - response.json = AsyncMock(return_value=error_json) - with self.assertRaises(PasteUploadError): - await send_to_paste_service("") - self.assertEqual(self.bot.http_session.post.call_count, FAILED_REQUEST_ATTEMPTS) - - self.bot.http_session.post.reset_mock() - - async def test_request_repeated_on_connection_errors(self): - """Requests are repeated in the case of connection errors.""" - self.bot.http_session.post = MagicMock(side_effect=ClientConnectorError(Mock(), Mock())) - with self.assertRaises(PasteUploadError): - await send_to_paste_service("") - self.assertEqual(self.bot.http_session.post.call_count, FAILED_REQUEST_ATTEMPTS) - - async def test_general_error_handled_and_request_repeated(self): - """All `Exception`s are handled, logged and request repeated.""" - self.bot.http_session.post = MagicMock(side_effect=Exception) - with self.assertRaises(PasteUploadError): - await send_to_paste_service("") - self.assertEqual(self.bot.http_session.post.call_count, FAILED_REQUEST_ATTEMPTS) - self.assertLogs("bot.utils", logging.ERROR) - - async def test_raises_error_on_too_long_input(self): - """Ensure PasteTooLongError is raised if `contents` is longer than `MAX_PASTE_LENGTH`.""" - contents = "a" * (MAX_PASTE_LENGTH + 1) - with self.assertRaises(PasteTooLongError): - await send_to_paste_service(contents) - - async def test_raises_on_too_large_max_length(self): - """Ensure ValueError is raised if `max_length` passed is greater than `MAX_PASTE_LENGTH`.""" - with self.assertRaises(ValueError): - await send_to_paste_service("Hello World!", max_length=MAX_PASTE_LENGTH + 1) -- cgit v1.2.3 From d961f5e2cc89c6c2a30b923756e3304d65f1ae28 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Thu, 22 Jun 2023 22:33:50 +0100 Subject: Mock _lexers_supported_by_pastebin in test to ensure no call to external service is made --- bot/exts/utils/snekbox/_cog.py | 9 ++++----- tests/bot/exts/utils/snekbox/test_snekbox.py | 4 ++++ 2 files changed, 8 insertions(+), 5 deletions(-) (limited to 'tests') diff --git a/bot/exts/utils/snekbox/_cog.py b/bot/exts/utils/snekbox/_cog.py index f07bcb7df..875357fb3 100644 --- a/bot/exts/utils/snekbox/_cog.py +++ b/bot/exts/utils/snekbox/_cog.py @@ -10,8 +10,7 @@ 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 -from pydis_core.utils import interactions -from pydis_core.utils.paste_service import PasteTooLongError, PasteUploadError, send_to_paste_service +from pydis_core.utils import interactions, paste_service from pydis_core.utils.regex import FORMATTED_CODE_REGEX, RAW_CODE_REGEX from bot.bot import Bot @@ -208,15 +207,15 @@ class Snekbox(Cog): log.trace("Uploading full output to paste service...") try: - paste_link = await send_to_paste_service( + paste_link = await paste_service.send_to_paste_service( contents=output, lexer="text", http_session=self.bot.http_session, ) return paste_link["link"] - except PasteTooLongError: + except paste_service.PasteTooLongError: return "too long to upload" - except PasteUploadError: + except paste_service.PasteUploadError: return "unable to upload" @staticmethod diff --git a/tests/bot/exts/utils/snekbox/test_snekbox.py b/tests/bot/exts/utils/snekbox/test_snekbox.py index 6d9b6b1e4..200aa5a20 100644 --- a/tests/bot/exts/utils/snekbox/test_snekbox.py +++ b/tests/bot/exts/utils/snekbox/test_snekbox.py @@ -55,6 +55,10 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): ) resp.json.assert_awaited_once() + @patch( + "bot.exts.utils.snekbox._cog.paste_service._lexers_supported_by_pastebin", + {"https://paste.pythondiscord.com": ["text"]}, + ) async def test_upload_output_reject_too_long(self): """Reject output longer than MAX_PASTE_LENGTH.""" result = await self.cog.upload_output("-" * (MAX_PASTE_SIZE + 1)) -- cgit v1.2.3 From e56cd0c666d27a018c8e09dea144e3a1907c73ef Mon Sep 17 00:00:00 2001 From: Steele Farnsworth Date: Sat, 5 Aug 2023 00:58:44 -0400 Subject: Change DM users get when warned (#2696) --- bot/exts/moderation/infraction/_utils.py | 34 +++++++++++++++++----- tests/bot/exts/moderation/infraction/test_utils.py | 10 +++---- 2 files changed, 31 insertions(+), 13 deletions(-) (limited to 'tests') diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 1bd5c5d5f..baeb971e4 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -39,17 +39,26 @@ 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}>)." ) +INFRACTION_MODMAIL_FOOTER = ( + "\nIf you would like to discuss this infraction, " + f"send a message to the ModMail bot (<@{MODMAIL_ACCOUNT_ID}>)." +) INFRACTION_AUTHOR_NAME = "Infraction information" LONGEST_EXTRAS = max(len(INFRACTION_APPEAL_SERVER_FOOTER), len(INFRACTION_APPEAL_MODMAIL_FOOTER)) -INFRACTION_DESCRIPTION_TEMPLATE = ( +INFRACTION_DESCRIPTION_NOT_WARNING_TEMPLATE = ( "**Type:** {type}\n" "**Duration:** {duration}\n" "**Expires:** {expires}\n" "**Reason:** {reason}\n" ) +INFRACTION_DESCRIPTION_WARNING_TEMPLATE = ( + "**Type:** Warning\n" + "**Reason:** {reason}\n" +) + async def post_user(ctx: Context, user: MemberOrUser) -> dict | None: """ @@ -213,18 +222,27 @@ async def notify_infraction( if reason is None: reason = infraction["reason"] - text = INFRACTION_DESCRIPTION_TEMPLATE.format( - type=infr_type.title(), - expires=expires_at, - duration=duration, - reason=reason or "No reason provided." - ) + if infraction["type"] == "warning": + text = INFRACTION_DESCRIPTION_WARNING_TEMPLATE.format( + reason=reason or "No reason provided." + ) + else: + text = INFRACTION_DESCRIPTION_NOT_WARNING_TEMPLATE.format( + type=infr_type.title(), + expires=expires_at, + duration=duration, + reason=reason or "No reason provided." + ) # For case when other fields than reason is too long and this reach limit, then force-shorten string 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_MODMAIL_FOOTER if infraction["type"] == "warning" + else INFRACTION_APPEAL_MODMAIL_FOOTER + ) embed = discord.Embed( description=text, diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 25337673e..e2a7bad9f 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -142,7 +142,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): ), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + description=utils.INFRACTION_DESCRIPTION_NOT_WARNING_TEMPLATE.format( type="Ban", expires="2020-02-26 09:20 (23 hours and 59 minutes)", reason="No reason provided." @@ -160,7 +160,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (dict(id=0, type="warning", reason="Test reason.", expires_at=None), self.user), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + description=utils.INFRACTION_DESCRIPTION_NOT_WARNING_TEMPLATE.format( type="Warning", expires="N/A", reason="Test reason." @@ -180,7 +180,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (dict(id=0, type="note", reason=None, expires_at=None), self.user), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + description=utils.INFRACTION_DESCRIPTION_NOT_WARNING_TEMPLATE.format( type="Note", expires="N/A", reason="No reason provided." @@ -201,7 +201,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): ), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + description=utils.INFRACTION_DESCRIPTION_NOT_WARNING_TEMPLATE.format( type="Mute", expires="2020-02-26 09:20 (23 hours and 59 minutes)", reason="Test" @@ -219,7 +219,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "args": (dict(id=0, type="mute", reason="foo bar" * 4000, expires_at=None), self.user), "expected_output": Embed( title=utils.INFRACTION_TITLE, - description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( + description=utils.INFRACTION_DESCRIPTION_NOT_WARNING_TEMPLATE.format( type="Mute", expires="N/A", reason="foo bar" * 4000 -- cgit v1.2.3 From 15c380f4bfb9cba94490b0521379600c56598b44 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Aug 2023 11:26:44 +0100 Subject: Bump ruff from 0.0.282 to 0.0.283 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.0.282 to 0.0.283. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/BREAKING_CHANGES.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.0.282...v0.0.283) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 38 +++++++++++++++++++------------------- pyproject.toml | 2 +- tests/_autospec.py | 2 +- 3 files changed, 21 insertions(+), 21 deletions(-) (limited to 'tests') diff --git a/poetry.lock b/poetry.lock index 3860d52af..e45697329 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1882,28 +1882,28 @@ six = "*" [[package]] name = "ruff" -version = "0.0.282" +version = "0.0.283" description = "An extremely fast Python linter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.0.282-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:01b76309ddab16eb258dabc5e86e73e6542f59f3ea6b4ab886ecbcfc80ce062c"}, - {file = "ruff-0.0.282-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:e177cbb6dc0b1dbef5e999900d798b73e33602abf9b6c62d5d2cbe101026d931"}, - {file = "ruff-0.0.282-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5374b40b6d860d334d28678a53a92f0bf04b53acdf0395900361ad54ce71cd1d"}, - {file = "ruff-0.0.282-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1ccbceb44e94fe2205b63996166e98a513a19ed23ec01d7193b7494b94ba30d"}, - {file = "ruff-0.0.282-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eee9c8c50bc77eb9c0811c91d9d67ff39fe4f394c2f44ada37dac6d45e50c9f1"}, - {file = "ruff-0.0.282-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:826e4de98e91450a6fe699a4e4a7cf33b9a90a2c5c270dc5b202241c37359ff8"}, - {file = "ruff-0.0.282-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d99758f8bbcb8f8da99acabf711ffad5e7a015247adf27211100b3586777fd56"}, - {file = "ruff-0.0.282-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f30c9958ab9cb02bf0c574c629e87c19454cbbdb82750e49e3d1559a5a8f216"}, - {file = "ruff-0.0.282-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47a7a9366ab8e4ee20df9339bef172eec7b2e9e123643bf3ede005058f5b114e"}, - {file = "ruff-0.0.282-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f05f5e6d6df6f8b1974c08f963c33f0a4d8cfa15cba12d35ca3ece8e9be5b1f"}, - {file = "ruff-0.0.282-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0710ea2cadc504b96c1d94c414a7802369d0fff2ab7c94460344bba69135cb40"}, - {file = "ruff-0.0.282-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2ca52536e1c7603fe4cbb5ad9dc141df47c3200df782f5ec559364716ea27f96"}, - {file = "ruff-0.0.282-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:aab9ed5bfba6b0a2242a7ec9a72858c802ceeaf0076fe72b2ad455639275f22c"}, - {file = "ruff-0.0.282-py3-none-win32.whl", hash = "sha256:f51bbb64f8f29e444c16d21b269ba82e25f8d536beda3df7c9fe1816297e508e"}, - {file = "ruff-0.0.282-py3-none-win_amd64.whl", hash = "sha256:bd25085c42ebaffe336ed7bda8a0ae7b6c454a5f386ec8b2299503f79bd12bdf"}, - {file = "ruff-0.0.282-py3-none-win_arm64.whl", hash = "sha256:f03fba9621533d67d7ab995847467d78b9337e3697779ef2cea6f1deaee5fbef"}, - {file = "ruff-0.0.282.tar.gz", hash = "sha256:ef677c26bae756e4c98af6d8972da83caea550bc92ffef97a6e939ca5b24ad06"}, + {file = "ruff-0.0.283-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:d59615628b43c40b8335af0fafd544b3a09e9891829461fa2eb0d67f00570df5"}, + {file = "ruff-0.0.283-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:742d3c09bb4272d92fcd0a01a203d837488060280c28a42461e166226651a12a"}, + {file = "ruff-0.0.283-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03622378270a37c61bb0f430c29f41bdf0699e8791d0d7548ad5745c737723fb"}, + {file = "ruff-0.0.283-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a4d36f0b3beecc01b50933795da718347ee442afa14cced5a60afe20e8335d24"}, + {file = "ruff-0.0.283-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d21b29dc63d8ec246207dd7115ec39814ca74ee0f0f7b261aa82fb9c1cd8dfcf"}, + {file = "ruff-0.0.283-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fe095f2c3e8e557f2709945d611efd476b3eb39bdec5b258b2f88cfb8b5d136d"}, + {file = "ruff-0.0.283-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b773f1dc57e642f707ee0e8bd68a0bc5ec95441367166a276e5dfdf88b21e1bf"}, + {file = "ruff-0.0.283-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d539c73e207a13a915bde6c52ae8b2beb0b00c3b975e9e5d808fe288ea354a72"}, + {file = "ruff-0.0.283-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e43d3ab5c0bdb7b7a045411773b18ed115f0590a30c8d267c484393c6b0486ba"}, + {file = "ruff-0.0.283-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5d72c97daa72f8914bf1b0c0ae4dbffc999e1945c291fa2d37c02ee4fa7f398"}, + {file = "ruff-0.0.283-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c32eb49ecf190a7bec0305270c864f796b362027b39a7d49c6fb120ea75e7b52"}, + {file = "ruff-0.0.283-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b1eae6990b078883c0cae60f1df0c31d2071f20afcec285baa363b9b6f7321cf"}, + {file = "ruff-0.0.283-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ad5a3042cbae1b82c3c953be77181a0934a6b02ba476fec4f177eb9c297e19ed"}, + {file = "ruff-0.0.283-py3-none-win32.whl", hash = "sha256:bd64f9775d96f35a236980ac98ed50fb5c755b227845612a873ad4f247c0cf8d"}, + {file = "ruff-0.0.283-py3-none-win_amd64.whl", hash = "sha256:28e3545ff24ae44e13da2b8fc706dd62a374cc74e4f5c1fbc8bc071ab0cc309f"}, + {file = "ruff-0.0.283-py3-none-win_arm64.whl", hash = "sha256:28732d956171f493b45c096d27f015e34fde065414330b68d59efcd0f3f67d5d"}, + {file = "ruff-0.0.283.tar.gz", hash = "sha256:6ee6928ad7b6b2b103d3b41517ff252cb81506dacbef01bab31fcfd0de39c5bb"}, ] [[package]] @@ -2223,4 +2223,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "3.11.*" -content-hash = "b8f55487228a8bb16e493d5d00807f1fa6d0fd18dcfb484a5d6debfa6ec2fea3" +content-hash = "60353f299efeb38128df337aabef6ec53c5018c9a1fe14254737b5079f094f41" diff --git a/pyproject.toml b/pyproject.toml index 107c4a472..68aa2685f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ pytest = "7.4.0" pytest-cov = "4.1.0" pytest-subtests = "0.11.0" pytest-xdist = "3.3.1" -ruff = "0.0.282" +ruff = "0.0.283" taskipy = "1.12.0" diff --git a/tests/_autospec.py b/tests/_autospec.py index 6f990a580..a9cd59dc0 100644 --- a/tests/_autospec.py +++ b/tests/_autospec.py @@ -51,7 +51,7 @@ def autospec(target, *attributes: str, pass_mocks: bool = True, **patch_kwargs) # Import the target if it's a string. # This is to support both object and string targets like patch.multiple. - if type(target) is str: + if isinstance(target, str): target = pkgutil.resolve_name(target) def decorator(func): -- cgit v1.2.3 From 292fba7c58b24df30f7631290bfecdbbd74da141 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 25 Jul 2023 15:21:02 +0100 Subject: Replace deprecated functions with new pydantic v2 functions --- bot/constants.py | 4 ++-- bot/exts/filtering/_filter_lists/antispam.py | 2 +- bot/exts/filtering/_filters/filter.py | 4 ++-- bot/exts/filtering/_settings.py | 2 +- .../actions/infraction_and_notification.py | 16 ++++++++-------- bot/exts/filtering/_settings_types/actions/ping.py | 4 ++-- bot/exts/filtering/_settings_types/settings_entry.py | 2 +- .../_settings_types/validations/channel_scope.py | 4 ++-- bot/exts/filtering/_ui/filter.py | 6 +++--- bot/exts/filtering/_ui/filter_list.py | 2 +- bot/exts/filtering/_utils.py | 13 +++++++++---- bot/exts/filtering/filtering.py | 14 ++++++++++---- bot/exts/recruitment/talentpool/_api.py | 12 ++++++------ botstrap.py | 6 +++--- tests/bot/exts/filtering/test_settings_entries.py | 4 ++-- 15 files changed, 53 insertions(+), 42 deletions(-) (limited to 'tests') diff --git a/bot/constants.py b/bot/constants.py index 922f41abd..fce3c09ec 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -8,7 +8,7 @@ By default, the values defined in the classes are used, these can be overridden import os from enum import Enum -from pydantic import BaseModel, root_validator +from pydantic import BaseModel, model_validator from pydantic_settings import BaseSettings @@ -311,7 +311,7 @@ class _Colours(EnvConfig, env_prefix="colours_"): white: int = 0xfffffe yellow: int = 0xffd241 - @root_validator(pre=True) + @model_validator(mode="before") def parse_hex_values(cls, values: dict) -> dict: # noqa: N805 """Convert hex strings to ints.""" for key, value in values.items(): diff --git a/bot/exts/filtering/_filter_lists/antispam.py b/bot/exts/filtering/_filter_lists/antispam.py index 22f35f40e..f27412e1a 100644 --- a/bot/exts/filtering/_filter_lists/antispam.py +++ b/bot/exts/filtering/_filter_lists/antispam.py @@ -93,7 +93,7 @@ class AntispamList(UniquesListBase): current_actions.pop("ping", None) current_actions.pop("send_alert", None) - new_infraction = current_actions[InfractionAndNotification.name].copy() + new_infraction = current_actions[InfractionAndNotification.name].model_copy() # Smaller infraction value => higher in hierarchy. 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. diff --git a/bot/exts/filtering/_filters/filter.py b/bot/exts/filtering/_filters/filter.py index 6745e4f66..3f201cfde 100644 --- a/bot/exts/filtering/_filters/filter.py +++ b/bot/exts/filtering/_filters/filter.py @@ -31,7 +31,7 @@ class Filter(FieldRequiring): self.updated_at = arrow.get(filter_data["updated_at"]) self.actions, self.validations = create_settings(filter_data["settings"], defaults=defaults) if self.extra_fields_type: - self.extra_fields = self.extra_fields_type.parse_obj(filter_data["additional_settings"]) + self.extra_fields = self.extra_fields_type.model_validate(filter_data["additional_settings"]) else: self.extra_fields = None @@ -46,7 +46,7 @@ class Filter(FieldRequiring): filter_settings = {} if self.extra_fields: - filter_settings = self.extra_fields.dict(exclude_unset=True) + filter_settings = self.extra_fields.model_dump(exclude_unset=True) return settings, filter_settings diff --git a/bot/exts/filtering/_settings.py b/bot/exts/filtering/_settings.py index 766a5ea10..7005dd2d1 100644 --- a/bot/exts/filtering/_settings.py +++ b/bot/exts/filtering/_settings.py @@ -227,5 +227,5 @@ class Defaults(NamedTuple): """Return a dict representation of the stored fields across all entries.""" dict_ = {} for settings in self: - dict_ = reduce(operator.or_, (entry.dict() for entry in settings.values()), dict_) + dict_ = reduce(operator.or_, (entry.model_dump() for entry in settings.values()), dict_) return dict_ 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 f12538294..359aa7bc3 100644 --- a/bot/exts/filtering/_settings_types/actions/infraction_and_notification.py +++ b/bot/exts/filtering/_settings_types/actions/infraction_and_notification.py @@ -6,7 +6,7 @@ import discord.abc from dateutil.relativedelta import relativedelta from discord import Colour, Embed, Member, User from discord.errors import Forbidden -from pydantic import validator +from pydantic import field_validator from pydis_core.utils.logging import get_logger from pydis_core.utils.members import get_or_fetch_member @@ -151,7 +151,7 @@ class InfractionAndNotification(ActionEntry): infraction_duration: InfractionDuration infraction_channel: int - @validator("infraction_type", pre=True) + @field_validator("infraction_type", mode="before") @classmethod def convert_infraction_name(cls, infr_type: str | Infraction) -> Infraction: """Convert the string to an Infraction by name.""" @@ -221,14 +221,14 @@ class InfractionAndNotification(ActionEntry): """ # Lower number -> higher in the hierarchy if self.infraction_type is None: - return other.copy() + return other.model_copy() if other.infraction_type is None: - return self.copy() + return self.model_copy() if self.infraction_type.value < other.infraction_type.value: - result = self.copy() + result = self.model_copy() elif self.infraction_type.value > other.infraction_type.value: - result = other.copy() + result = other.model_copy() other = self else: now = arrow.utcnow().datetime @@ -236,9 +236,9 @@ class InfractionAndNotification(ActionEntry): other.infraction_duration is not None and now + self.infraction_duration.value > now + other.infraction_duration.value ): - result = self.copy() + result = self.model_copy() else: - result = other.copy() + result = other.model_copy() other = self # If the winner has no message but the loser does, copy the message to the winner. diff --git a/bot/exts/filtering/_settings_types/actions/ping.py b/bot/exts/filtering/_settings_types/actions/ping.py index fa3e2224f..4b38a19f3 100644 --- a/bot/exts/filtering/_settings_types/actions/ping.py +++ b/bot/exts/filtering/_settings_types/actions/ping.py @@ -1,6 +1,6 @@ from typing import ClassVar, Self -from pydantic import validator +from pydantic import field_validator from bot.exts.filtering._filter_context import FilterContext from bot.exts.filtering._settings_types.settings_entry import ActionEntry @@ -25,7 +25,7 @@ class Ping(ActionEntry): guild_pings: set[str] dm_pings: set[str] - @validator("*", pre=True) + @field_validator("*", mode="before") @classmethod def init_sequence_if_none(cls, pings: list[str] | None) -> list[str]: """Initialize an empty sequence if the value is None.""" diff --git a/bot/exts/filtering/_settings_types/settings_entry.py b/bot/exts/filtering/_settings_types/settings_entry.py index a3a2cbe1a..1cb9d9f2a 100644 --- a/bot/exts/filtering/_settings_types/settings_entry.py +++ b/bot/exts/filtering/_settings_types/settings_entry.py @@ -28,7 +28,7 @@ class SettingsEntry(BaseModel, FieldRequiring): def __init__(self, defaults: SettingsEntry | None = None, /, **data): overrides = set() if defaults: - defaults_dict = defaults.dict() + defaults_dict = defaults.model_dump() for field_name, field_value in list(data.items()): if field_value is None: data[field_name] = defaults_dict[field_name] diff --git a/bot/exts/filtering/_settings_types/validations/channel_scope.py b/bot/exts/filtering/_settings_types/validations/channel_scope.py index 1880ae27e..69de4199c 100644 --- a/bot/exts/filtering/_settings_types/validations/channel_scope.py +++ b/bot/exts/filtering/_settings_types/validations/channel_scope.py @@ -1,6 +1,6 @@ from typing import ClassVar, Union -from pydantic import validator +from pydantic import field_validator from bot.exts.filtering._filter_context import FilterContext from bot.exts.filtering._settings_types.settings_entry import ValidationEntry @@ -36,7 +36,7 @@ class ChannelScope(ValidationEntry): enabled_channels: set[Union[int, str]] # noqa: UP007 enabled_categories: set[Union[int, str]] # noqa: UP007 - @validator("*", pre=True) + @field_validator("*", mode="before") @classmethod def init_if_sequence_none(cls, sequence: list[str] | None) -> list[str]: """Initialize an empty sequence if the value is None.""" diff --git a/bot/exts/filtering/_ui/filter.py b/bot/exts/filtering/_ui/filter.py index 4300de19c..6927a5d41 100644 --- a/bot/exts/filtering/_ui/filter.py +++ b/bot/exts/filtering/_ui/filter.py @@ -34,7 +34,7 @@ def build_filter_repr_dict( default_setting_values = {} for settings_group in filter_list[list_type].defaults: for _, setting in settings_group.items(): - default_setting_values.update(to_serializable(setting.dict(), ui_repr=True)) + default_setting_values.update(to_serializable(setting.model_dump(), ui_repr=True)) # Add overrides. It's done in this way to preserve field order, since the filter won't have all settings. total_values = {} @@ -47,7 +47,7 @@ def build_filter_repr_dict( # Add the filter-specific settings. if filter_type.extra_fields_type: # This iterates over the default values of the extra fields model. - for name, value in filter_type.extra_fields_type().dict().items(): + for name, value in filter_type.extra_fields_type().model_dump().items(): if name not in extra_fields_overrides or repr_equals(extra_fields_overrides[name], value): total_values[f"{filter_type.name}/{name}"] = value else: @@ -287,7 +287,7 @@ class FilterEditView(EditBaseView): if "/" in setting_name: filter_name, setting_name = setting_name.split("/", maxsplit=1) dict_to_edit = self.filter_settings_overrides - default_value = self.filter_type.extra_fields_type().dict()[setting_name] + default_value = self.filter_type.extra_fields_type().model_dump()[setting_name] else: dict_to_edit = self.settings_overrides default_value = self.filter_list[self.list_type].default(setting_name) diff --git a/bot/exts/filtering/_ui/filter_list.py b/bot/exts/filtering/_ui/filter_list.py index 062975ad7..2858817e3 100644 --- a/bot/exts/filtering/_ui/filter_list.py +++ b/bot/exts/filtering/_ui/filter_list.py @@ -50,7 +50,7 @@ def build_filterlist_repr_dict(filter_list: FilterList, list_type: ListType, new default_setting_values = {} for settings_group in filter_list[list_type].defaults: for _, setting in settings_group.items(): - default_setting_values.update(to_serializable(setting.dict(), ui_repr=True)) + default_setting_values.update(to_serializable(setting.model_dump(), ui_repr=True)) # Add new values. It's done in this way to preserve field order, since the new_values won't have all settings. total_values = {} diff --git a/bot/exts/filtering/_utils.py b/bot/exts/filtering/_utils.py index e109a47ee..944cf3837 100644 --- a/bot/exts/filtering/_utils.py +++ b/bot/exts/filtering/_utils.py @@ -7,7 +7,7 @@ import pkgutil import types from abc import ABC, abstractmethod from collections import defaultdict -from collections.abc import Iterable +from collections.abc import Callable, Iterable from dataclasses import dataclass from functools import cache from typing import Any, Self, TypeVar, Union, get_args, get_origin @@ -15,6 +15,7 @@ from typing import Any, Self, TypeVar, Union, get_args, get_origin import discord import regex from discord.ext.commands import Command +from pydantic_core import core_schema import bot from bot.bot import Bot @@ -252,12 +253,16 @@ class CustomIOField: self.value = self.process_value(value) @classmethod - def __get_validators__(cls): + def __get_pydantic_core_schema__( + cls, + _source: type[Any], + _handler: Callable[[Any], core_schema.CoreSchema], + ) -> core_schema.CoreSchema: """Boilerplate for Pydantic.""" - yield cls.validate + return core_schema.general_plain_validator_function(cls.validate) @classmethod - def validate(cls, v: Any) -> Self: + def validate(cls, v: Any, _info: core_schema.ValidationInfo) -> Self: """Takes the given value and returns a class instance with that value.""" if isinstance(v, CustomIOField): return cls(v.value) diff --git a/bot/exts/filtering/filtering.py b/bot/exts/filtering/filtering.py index 2b7ad2ff3..bedd9958f 100644 --- a/bot/exts/filtering/filtering.py +++ b/bot/exts/filtering/filtering.py @@ -181,7 +181,7 @@ class Filtering(Cog): extra_fields_type, type_hints[field_name] ) - for field_name in extra_fields_type.__fields__ + for field_name in extra_fields_type.model_fields } async def schedule_offending_messages_deletion(self) -> None: @@ -754,7 +754,7 @@ class Filtering(Cog): setting_values = {} for settings_group in filter_list[list_type].defaults: for _, setting in settings_group.items(): - setting_values.update(to_serializable(setting.dict(), ui_repr=True)) + setting_values.update(to_serializable(setting.model_dump(), ui_repr=True)) embed = Embed(colour=Colour.blue()) populate_embed_from_dict(embed, setting_values) @@ -1239,7 +1239,13 @@ class Filtering(Cog): for current_settings in (filter_.actions, filter_.validations): if current_settings: for setting_entry in current_settings.values(): - settings.update({setting: None for setting in setting_entry.dict() if setting not in settings}) + settings.update( + { + setting: None + for setting in setting_entry.model_dump() + if setting not in settings + } + ) # Even though the list ID remains unchanged, it still needs to be provided for correct serializer validation. list_id = filter_list[list_type].id @@ -1295,7 +1301,7 @@ class Filtering(Cog): if not (differ_by_default <= override_matches): # The overrides didn't cover for the default mismatches. return False - filter_settings = filter_.extra_fields.dict() if filter_.extra_fields else {} + filter_settings = filter_.extra_fields.model_dump() if filter_.extra_fields else {} # If the dict changes then some fields were not the same. return (filter_settings | filter_settings_query) == filter_settings diff --git a/bot/exts/recruitment/talentpool/_api.py b/bot/exts/recruitment/talentpool/_api.py index e12111de5..f7b243209 100644 --- a/bot/exts/recruitment/talentpool/_api.py +++ b/bot/exts/recruitment/talentpool/_api.py @@ -1,6 +1,6 @@ from datetime import datetime -from pydantic import BaseModel, Field, parse_obj_as +from pydantic import BaseModel, Field, TypeAdapter from pydis_core.site_api import APIClient @@ -50,13 +50,13 @@ class NominationAPI: params["user__id"] = str(user_id) data = await self.site_api.get("bot/nominations", params=params) - nominations = parse_obj_as(list[Nomination], data) + nominations = TypeAdapter(list[Nomination]).validate_python(data) return nominations async def get_nomination(self, nomination_id: int) -> Nomination: """Fetch a nomination by ID.""" data = await self.site_api.get(f"bot/nominations/{nomination_id}") - nomination = Nomination.parse_obj(data) + nomination = Nomination.model_validate(data) return nomination async def edit_nomination( @@ -84,7 +84,7 @@ class NominationAPI: data["thread_id"] = thread_id result = await self.site_api.patch(f"bot/nominations/{nomination_id}", json=data) - return Nomination.parse_obj(result) + return Nomination.model_validate(result) async def edit_nomination_entry( self, @@ -96,7 +96,7 @@ class NominationAPI: """Edit a nomination entry.""" data = {"actor": actor_id, "reason": reason} result = await self.site_api.patch(f"bot/nominations/{nomination_id}", json=data) - return Nomination.parse_obj(result) + return Nomination.model_validate(result) async def post_nomination( self, @@ -111,7 +111,7 @@ class NominationAPI: "user": user_id, } result = await self.site_api.post("bot/nominations", json=data) - return Nomination.parse_obj(result) + return Nomination.model_validate(result) async def get_activity( self, diff --git a/botstrap.py b/botstrap.py index c57a254a3..7a9d94d8b 100644 --- a/botstrap.py +++ b/botstrap.py @@ -176,7 +176,7 @@ with DiscordClient(guild_id=GUILD_ID) as discord_client: all_roles = discord_client.get_all_roles() - for role_name in _Roles.__fields__: + for role_name in _Roles.model_fields: role_id = all_roles.get(role_name, None) if not role_id: @@ -209,7 +209,7 @@ with DiscordClient(guild_id=GUILD_ID) as discord_client: 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 - for channel_name in _Channels.__fields__: + for channel_name in _Channels.model_fields: channel_id = all_channels.get(channel_name, None) if not channel_id: log.warning( @@ -222,7 +222,7 @@ with DiscordClient(guild_id=GUILD_ID) as discord_client: config_str += "\n#Categories\n" - for category_name in _Categories.__fields__: + for category_name in _Categories.model_fields: category_id = all_categories.get(category_name, None) if not category_id: log.warning( diff --git a/tests/bot/exts/filtering/test_settings_entries.py b/tests/bot/exts/filtering/test_settings_entries.py index 3ae0b5ab5..988435022 100644 --- a/tests/bot/exts/filtering/test_settings_entries.py +++ b/tests/bot/exts/filtering/test_settings_entries.py @@ -173,7 +173,7 @@ class FilterTests(unittest.TestCase): result = infraction1.union(infraction2) self.assertDictEqual( - result.dict(), + result.model_dump(), { "infraction_type": Infraction.TIMEOUT, "infraction_reason": "there", @@ -206,7 +206,7 @@ class FilterTests(unittest.TestCase): result = infraction1.union(infraction2) self.assertDictEqual( - result.dict(), + result.model_dump(), { "infraction_type": Infraction.BAN, "infraction_reason": "", -- cgit v1.2.3 From 7a919c013f6a254a28422befddb0f73e8c1b7276 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 11 Aug 2023 16:09:09 +0100 Subject: Update import style for ruff isort rule ruff's isort rule doesn't allow multiple imports on the same line if wrapping. This is arguably easier to read too. --- bot/exts/filtering/_ui/filter.py | 12 ++++++++++-- bot/exts/filtering/_ui/filter_list.py | 9 +++++++-- bot/exts/filtering/_ui/search.py | 9 +++++++-- bot/exts/filtering/filtering.py | 11 +++++++++-- bot/exts/moderation/stream.py | 8 +++++++- bot/exts/utils/reminders.py | 9 ++++++++- bot/utils/checks.py | 12 ++++++++++-- tests/bot/exts/filtering/test_settings_entries.py | 4 +++- tests/bot/exts/moderation/test_incidents.py | 12 ++++++++++-- tests/bot/exts/moderation/test_silence.py | 9 ++++++++- 10 files changed, 79 insertions(+), 16 deletions(-) (limited to 'tests') diff --git a/bot/exts/filtering/_ui/filter.py b/bot/exts/filtering/_ui/filter.py index 6927a5d41..d15a3cacb 100644 --- a/bot/exts/filtering/_ui/filter.py +++ b/bot/exts/filtering/_ui/filter.py @@ -13,8 +13,16 @@ from pydis_core.site_api import ResponseCodeError from bot.exts.filtering._filter_lists.filter_list import FilterList, ListType from bot.exts.filtering._filters.filter import Filter from bot.exts.filtering._ui.ui import ( - COMPONENT_TIMEOUT, CustomCallbackSelect, EditBaseView, MAX_EMBED_DESCRIPTION, MISSING, SETTINGS_DELIMITER, - SINGLE_SETTING_PATTERN, format_response_error, parse_value, populate_embed_from_dict + COMPONENT_TIMEOUT, + CustomCallbackSelect, + EditBaseView, + MAX_EMBED_DESCRIPTION, + MISSING, + SETTINGS_DELIMITER, + SINGLE_SETTING_PATTERN, + format_response_error, + parse_value, + populate_embed_from_dict, ) from bot.exts.filtering._utils import repr_equals, to_serializable from bot.log import get_logger diff --git a/bot/exts/filtering/_ui/filter_list.py b/bot/exts/filtering/_ui/filter_list.py index 2858817e3..a06e8a71e 100644 --- a/bot/exts/filtering/_ui/filter_list.py +++ b/bot/exts/filtering/_ui/filter_list.py @@ -10,8 +10,13 @@ from pydis_core.site_api import ResponseCodeError from bot.exts.filtering._filter_lists import FilterList, ListType from bot.exts.filtering._ui.ui import ( - CustomCallbackSelect, EditBaseView, MISSING, SETTINGS_DELIMITER, format_response_error, parse_value, - populate_embed_from_dict + CustomCallbackSelect, + EditBaseView, + MISSING, + SETTINGS_DELIMITER, + format_response_error, + parse_value, + populate_embed_from_dict, ) from bot.exts.filtering._utils import repr_equals, to_serializable diff --git a/bot/exts/filtering/_ui/search.py b/bot/exts/filtering/_ui/search.py index ccb47cdd2..d26fd9929 100644 --- a/bot/exts/filtering/_ui/search.py +++ b/bot/exts/filtering/_ui/search.py @@ -12,8 +12,13 @@ from bot.exts.filtering._filters.filter import Filter from bot.exts.filtering._settings_types.settings_entry import SettingsEntry from bot.exts.filtering._ui.filter import filter_overrides_for_ui from bot.exts.filtering._ui.ui import ( - COMPONENT_TIMEOUT, CustomCallbackSelect, EditBaseView, MISSING, SETTINGS_DELIMITER, parse_value, - populate_embed_from_dict + COMPONENT_TIMEOUT, + CustomCallbackSelect, + EditBaseView, + MISSING, + SETTINGS_DELIMITER, + parse_value, + populate_embed_from_dict, ) diff --git a/bot/exts/filtering/filtering.py b/bot/exts/filtering/filtering.py index bedd9958f..173b220f8 100644 --- a/bot/exts/filtering/filtering.py +++ b/bot/exts/filtering/filtering.py @@ -33,12 +33,19 @@ from bot.exts.filtering._filters.filter import Filter, UniqueFilter from bot.exts.filtering._settings import ActionSettings from bot.exts.filtering._settings_types.actions.infraction_and_notification import Infraction from bot.exts.filtering._ui.filter import ( - build_filter_repr_dict, description_and_settings_converter, filter_overrides_for_ui, populate_embed_from_dict + build_filter_repr_dict, + description_and_settings_converter, + filter_overrides_for_ui, + populate_embed_from_dict, ) from bot.exts.filtering._ui.filter_list import FilterListAddView, FilterListEditView, settings_converter from bot.exts.filtering._ui.search import SearchEditView, search_criteria_converter from bot.exts.filtering._ui.ui import ( - AlertView, ArgumentCompletionView, DeleteConfirmationView, build_mod_alert, format_response_error + AlertView, + ArgumentCompletionView, + DeleteConfirmationView, + build_mod_alert, + format_response_error, ) from bot.exts.filtering._utils import past_tense, repr_equals, starting_value, to_serializable from bot.exts.moderation.infraction.infractions import COMP_BAN_DURATION, COMP_BAN_REASON diff --git a/bot/exts/moderation/stream.py b/bot/exts/moderation/stream.py index b69f280c9..6cc5c4c51 100644 --- a/bot/exts/moderation/stream.py +++ b/bot/exts/moderation/stream.py @@ -10,7 +10,13 @@ from pydis_core.utils import scheduling from bot.bot import Bot from bot.constants import ( - Colours, Emojis, Guild, MODERATION_ROLES, Roles, STAFF_PARTNERS_COMMUNITY_ROLES, VideoPermission + Colours, + Emojis, + Guild, + MODERATION_ROLES, + Roles, + STAFF_PARTNERS_COMMUNITY_ROLES, + VideoPermission, ) from bot.converters import Expiry from bot.log import get_logger diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py index b6dae0b51..8b0cbed77 100644 --- a/bot/exts/utils/reminders.py +++ b/bot/exts/utils/reminders.py @@ -13,7 +13,14 @@ from pydis_core.utils.scheduling import Scheduler from bot.bot import Bot from bot.constants import ( - Channels, Guild, Icons, MODERATION_ROLES, NEGATIVE_REPLIES, POSITIVE_REPLIES, Roles, STAFF_PARTNERS_COMMUNITY_ROLES + Channels, + Guild, + Icons, + MODERATION_ROLES, + NEGATIVE_REPLIES, + POSITIVE_REPLIES, + Roles, + STAFF_PARTNERS_COMMUNITY_ROLES, ) from bot.converters import Duration, UnambiguousUser from bot.errors import LockedResourceError diff --git a/bot/utils/checks.py b/bot/utils/checks.py index d4f9c3a27..e46c8209b 100644 --- a/bot/utils/checks.py +++ b/bot/utils/checks.py @@ -1,8 +1,16 @@ from collections.abc import Callable, Container, Iterable from discord.ext.commands import ( - BucketType, CheckFailure, Cog, Command, CommandOnCooldown, Context, Cooldown, CooldownMapping, NoPrivateMessage, - has_any_role + BucketType, + CheckFailure, + Cog, + Command, + CommandOnCooldown, + Context, + Cooldown, + CooldownMapping, + NoPrivateMessage, + has_any_role, ) from bot import constants diff --git a/tests/bot/exts/filtering/test_settings_entries.py b/tests/bot/exts/filtering/test_settings_entries.py index 988435022..f12b2caa5 100644 --- a/tests/bot/exts/filtering/test_settings_entries.py +++ b/tests/bot/exts/filtering/test_settings_entries.py @@ -2,7 +2,9 @@ import unittest from bot.exts.filtering._filter_context import Event, FilterContext from bot.exts.filtering._settings_types.actions.infraction_and_notification import ( - Infraction, InfractionAndNotification, InfractionDuration + Infraction, + InfractionAndNotification, + InfractionDuration, ) from bot.exts.filtering._settings_types.validations.bypass_roles import RoleBypass from bot.exts.filtering._settings_types.validations.channel_scope import ChannelScope diff --git a/tests/bot/exts/moderation/test_incidents.py b/tests/bot/exts/moderation/test_incidents.py index 61f64da44..d8e3ddd8c 100644 --- a/tests/bot/exts/moderation/test_incidents.py +++ b/tests/bot/exts/moderation/test_incidents.py @@ -16,8 +16,16 @@ from bot.utils.messages import format_user from bot.utils.time import TimestampFormats, discord_timestamp from tests.base import RedisTestCase from tests.helpers import ( - MockAsyncWebhook, MockAttachment, MockBot, MockMember, MockMessage, MockReaction, MockRole, MockTextChannel, - MockUser, no_create_task + MockAsyncWebhook, + MockAttachment, + MockBot, + MockMember, + MockMessage, + MockReaction, + MockRole, + MockTextChannel, + MockUser, + no_create_task, ) CURRENT_TIME = datetime.datetime(2022, 1, 1, tzinfo=datetime.UTC) diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index ec0b3bf43..a7f239d7f 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -10,7 +10,14 @@ from bot.constants import Channels, Guild, MODERATION_ROLES, Roles from bot.exts.moderation import silence from tests.base import RedisTestCase from tests.helpers import ( - MockBot, MockContext, MockGuild, MockMember, MockRole, MockTextChannel, MockVoiceChannel, autospec + MockBot, + MockContext, + MockGuild, + MockMember, + MockRole, + MockTextChannel, + MockVoiceChannel, + autospec, ) -- cgit v1.2.3 From 30853cc3558a419604f7775b546598f00a2a8598 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 14:50:46 +0100 Subject: Bump ruff from 0.0.285 to 0.0.287 (#2745) * Bump ruff from 0.0.285 to 0.0.287 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.0.285 to 0.0.287. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/BREAKING_CHANGES.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.0.285...v0.0.287) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update bot to comply with ruff 0.0.287 linting rules --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Joe Banks --- poetry.lock | 38 +++++++++++++++++++------------------- pyproject.toml | 2 +- tests/bot/exts/test_cogs.py | 3 +-- 3 files changed, 21 insertions(+), 22 deletions(-) (limited to 'tests') diff --git a/poetry.lock b/poetry.lock index 1196c981d..323aca9fc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1980,28 +1980,28 @@ six = "*" [[package]] name = "ruff" -version = "0.0.285" +version = "0.0.287" description = "An extremely fast Python linter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.0.285-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:72a3a0936369b986b0e959f9090206ed3c18f9e5e439ea5b8e6867c6707aded5"}, - {file = "ruff-0.0.285-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:0d9ab6ad16742eb78919e0fba09f914f042409df40ad63423c34bb20d350162a"}, - {file = "ruff-0.0.285-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c48926156288b8ac005eb1db5e77c15e8a37309ae49d9fb6771d5cf5f777590"}, - {file = "ruff-0.0.285-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d2a60c102e7a5e147b58fc2cbea12a563c565383effc527c987ea2086a05742"}, - {file = "ruff-0.0.285-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b02aae62f922d088bb01943e1dbd861688ada13d735b78b8348a7d90121fd292"}, - {file = "ruff-0.0.285-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f572c4296d8c7ddd22c3204de4031965be524fdd1fdaaef273945932912b28c5"}, - {file = "ruff-0.0.285-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80effdf4fe69763d69eb4ab9443e186fd09e668b59fe70ba4b49f4c077d15a1b"}, - {file = "ruff-0.0.285-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5977ce304da35c263f5e082901bd7ac0bd2be845a8fcfd1a29e4d6680cddb307"}, - {file = "ruff-0.0.285-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72a087712d474fa17b915d7cb9ef807e1256182b12ddfafb105eb00aeee48d1a"}, - {file = "ruff-0.0.285-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7ce67736cd8dfe97162d1e7adfc2d9a1bac0efb9aaaff32e4042c7cde079f54b"}, - {file = "ruff-0.0.285-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5473a4c6cac34f583bff08c5f63b8def5599a0ea4dc96c0302fbd2cc0b3ecbad"}, - {file = "ruff-0.0.285-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e6b1c961d608d373a032f047a20bf3c55ad05f56c32e7b96dcca0830a2a72348"}, - {file = "ruff-0.0.285-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2933cc9631f453305399c7b8fb72b113ad76b49ae1d7103cc4afd3a423bed164"}, - {file = "ruff-0.0.285-py3-none-win32.whl", hash = "sha256:770c5eb6376de024111443022cda534fb28980a9dd3b4abc83992a8770167ba6"}, - {file = "ruff-0.0.285-py3-none-win_amd64.whl", hash = "sha256:a8c6ad6b9cd77489bf6d1510950cbbe47a843aa234adff0960bae64bd06c3b6d"}, - {file = "ruff-0.0.285-py3-none-win_arm64.whl", hash = "sha256:de44fbc6c3b25fccee473ddf851416fd4e246fc6027b2197c395b1b3b3897921"}, - {file = "ruff-0.0.285.tar.gz", hash = "sha256:45866048d1dcdcc80855998cb26c4b2b05881f9e043d2e3bfe1aa36d9a2e8f28"}, + {file = "ruff-0.0.287-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:1e0f9ee4c3191444eefeda97d7084721d9b8e29017f67997a20c153457f2eafd"}, + {file = "ruff-0.0.287-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:e9843e5704d4fb44e1a8161b0d31c1a38819723f0942639dfeb53d553be9bfb5"}, + {file = "ruff-0.0.287-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca1ed11d759a29695aed2bfc7f914b39bcadfe2ef08d98ff69c873f639ad3a8"}, + {file = "ruff-0.0.287-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1cf4d5ad3073af10f186ea22ce24bc5a8afa46151f6896f35c586e40148ba20b"}, + {file = "ruff-0.0.287-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d9d58bcb29afd72d2afe67120afcc7d240efc69a235853813ad556443dc922"}, + {file = "ruff-0.0.287-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:06ac5df7dd3ba8bf83bba1490a72f97f1b9b21c7cbcba8406a09de1a83f36083"}, + {file = "ruff-0.0.287-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2bfb478e1146a60aa740ab9ebe448b1f9e3c0dfb54be3cc58713310eef059c30"}, + {file = "ruff-0.0.287-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00d579a011949108c4b4fa04c4f1ee066dab536a9ba94114e8e580c96be2aeb4"}, + {file = "ruff-0.0.287-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3a810a79b8029cc92d06c36ea1f10be5298d2323d9024e1d21aedbf0a1a13e5"}, + {file = "ruff-0.0.287-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:150007028ad4976ce9a7704f635ead6d0e767f73354ce0137e3e44f3a6c0963b"}, + {file = "ruff-0.0.287-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a24a280db71b0fa2e0de0312b4aecb8e6d08081d1b0b3c641846a9af8e35b4a7"}, + {file = "ruff-0.0.287-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2918cb7885fa1611d542de1530bea3fbd63762da793751cc8c8d6e4ba234c3d8"}, + {file = "ruff-0.0.287-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:33d7b251afb60bec02a64572b0fd56594b1923ee77585bee1e7e1daf675e7ae7"}, + {file = "ruff-0.0.287-py3-none-win32.whl", hash = "sha256:022f8bed2dcb5e5429339b7c326155e968a06c42825912481e10be15dafb424b"}, + {file = "ruff-0.0.287-py3-none-win_amd64.whl", hash = "sha256:26bd0041d135a883bd6ab3e0b29c42470781fb504cf514e4c17e970e33411d90"}, + {file = "ruff-0.0.287-py3-none-win_arm64.whl", hash = "sha256:44bceb3310ac04f0e59d4851e6227f7b1404f753997c7859192e41dbee9f5c8d"}, + {file = "ruff-0.0.287.tar.gz", hash = "sha256:02dc4f5bf53ef136e459d467f3ce3e04844d509bc46c025a05b018feb37bbc39"}, ] [[package]] @@ -2322,4 +2322,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "3.11.*" -content-hash = "af4bf74ce65b11aef950ef3216285850bc3f2b9abf6152f65d97186c71557205" +content-hash = "a0d911cb0c463a00f3a3311392cf494b7bc72835af963ec68caa11bbeb9c54e1" diff --git a/pyproject.toml b/pyproject.toml index 2b5a8638c..086e12055 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ pytest = "7.4.1" pytest-cov = "4.1.0" pytest-subtests = "0.11.0" pytest-xdist = "3.3.1" -ruff = "0.0.285" +ruff = "0.0.287" taskipy = "1.12.0" diff --git a/tests/bot/exts/test_cogs.py b/tests/bot/exts/test_cogs.py index 99bc87120..0c117b3e6 100644 --- a/tests/bot/exts/test_cogs.py +++ b/tests/bot/exts/test_cogs.py @@ -62,8 +62,7 @@ class CommandNameTests(unittest.TestCase): """Yield all commands for all cogs in all extensions.""" for module in self.walk_modules(): for cog in self.walk_cogs(module): - for cmd in self.walk_commands(cog): - yield cmd + yield from self.walk_commands(cog) def test_names_dont_shadow(self): """Names and aliases of commands should be unique.""" -- cgit v1.2.3 From 081f5b567415b05fe3f31bb7e88ee800145bc315 Mon Sep 17 00:00:00 2001 From: wookie184 Date: Tue, 19 Sep 2023 06:24:54 +0100 Subject: Centralise error handling for commands invoked from error handler (#2436) --- bot/exts/backend/error_handler.py | 50 +++++++++++++++------------- tests/bot/exts/backend/test_error_handler.py | 26 +++------------ 2 files changed, 30 insertions(+), 46 deletions(-) (limited to 'tests') diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py index 0c8938918..c1e3eb70e 100644 --- a/bot/exts/backend/error_handler.py +++ b/bot/exts/backend/error_handler.py @@ -64,11 +64,24 @@ class ErrorHandler(Cog): ) if isinstance(e, errors.CommandNotFound) and not getattr(ctx, "invoked_from_error_handler", False): - if await self.try_silence(ctx): - return - if await self.try_run_fixed_codeblock(ctx): - return - await self.try_get_tag(ctx) # Try to look for a tag with the command's name + # We might not invoke a command from the error handler, but it's easier and safer to ensure + # this is always set rather than trying to get it exact, and shouldn't cause any issues. + ctx.invoked_from_error_handler = True + + # All errors from attempting to execute these commands should be handled by the error handler. + # We wrap non CommandErrors in CommandInvokeError to mirror the behaviour of normal commands. + try: + if await self.try_silence(ctx): + return + if await self.try_run_fixed_codeblock(ctx): + return + await self.try_get_tag(ctx) + except Exception as err: + log.info("Re-handling error raised by command in error handler") + if isinstance(err, errors.CommandError): + await self.on_command_error(ctx, err) + else: + await self.on_command_error(ctx, errors.CommandInvokeError(err)) elif isinstance(e, errors.UserInputError): log.debug(debug_message) await self.handle_user_input_error(ctx, e) @@ -123,7 +136,6 @@ class ErrorHandler(Cog): command = ctx.invoked_with.lower() args = ctx.message.content.lower().split(" ") - ctx.invoked_from_error_handler = True try: if not await silence_command.can_run(ctx): @@ -160,13 +172,7 @@ class ErrorHandler(Cog): return False async def try_get_tag(self, ctx: Context) -> None: - """ - Attempt to display a tag by interpreting the command name as a tag name. - - The invocation of tags get respects its checks. Any CommandErrors raised will be handled - by `on_command_error`, but the `invoked_from_error_handler` attribute will be added to - the context to prevent infinite recursion in the case of a CommandNotFound exception. - """ + """Attempt to display a tag by interpreting the command name as a tag name.""" tags_cog = self.bot.get_cog("Tags") if not tags_cog: log.debug("Not attempting to parse message as a tag as could not find `Tags` cog.") @@ -177,23 +183,19 @@ class ErrorHandler(Cog): if not maybe_tag_name or not isinstance(ctx.author, Member): return - ctx.invoked_from_error_handler = True try: if not await self.bot.can_run(ctx): log.debug("Cancelling attempt to fall back to a tag due to failed checks.") return + except errors.CommandError: + log.debug("Cancelling attempt to fall back to a tag due to failed checks.") + return - if await tags_get_command(ctx, maybe_tag_name): - return + if await tags_get_command(ctx, maybe_tag_name): + return - if not any(role.id in MODERATION_ROLES for role in ctx.author.roles): - await self.send_command_suggestion(ctx, maybe_tag_name) - except Exception as err: - log.debug("Error while attempting to invoke tag fallback.") - if isinstance(err, errors.CommandError): - await self.on_command_error(ctx, err) - else: - await self.on_command_error(ctx, errors.CommandInvokeError(err)) + if not any(role.id in MODERATION_ROLES for role in ctx.author.roles): + await self.send_command_suggestion(ctx, maybe_tag_name) async def try_run_fixed_codeblock(self, ctx: Context) -> bool: """ diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py index 0ba2fcf11..e28d5cfd0 100644 --- a/tests/bot/exts/backend/test_error_handler.py +++ b/tests/bot/exts/backend/test_error_handler.py @@ -54,12 +54,15 @@ class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase): self.ctx.reset_mock() self.cog.try_silence.reset_mock(return_value=True) self.cog.try_get_tag.reset_mock() + self.ctx.invoked_from_error_handler = False self.cog.try_silence.return_value = case["try_silence_return"] self.ctx.channel.id = 1234 self.assertIsNone(await self.cog.on_command_error(self.ctx, error)) + self.assertTrue(self.ctx.invoked_from_error_handler) + if case["try_silence_return"]: self.cog.try_get_tag.assert_not_awaited() self.cog.try_silence.assert_awaited_once() @@ -203,13 +206,6 @@ class TrySilenceTests(unittest.IsolatedAsyncioTestCase): self.ctx = MockContext(bot=self.bot, guild=self.guild) self.cog = error_handler.ErrorHandler(self.bot) - async def test_try_silence_context_invoked_from_error_handler(self): - """Should set `Context.invoked_from_error_handler` to `True`.""" - self.ctx.invoked_with = "foo" - await self.cog.try_silence(self.ctx) - self.assertTrue(hasattr(self.ctx, "invoked_from_error_handler")) - self.assertTrue(self.ctx.invoked_from_error_handler) - async def test_try_silence_get_command(self): """Should call `get_command` with `silence`.""" self.ctx.invoked_with = "foo" @@ -342,26 +338,12 @@ class TryGetTagTests(unittest.IsolatedAsyncioTestCase): await self.cog.try_get_tag(self.ctx) self.bot.get_cog.assert_called_once_with("Tags") - async def test_try_get_tag_invoked_from_error_handler(self): - """`self.ctx` should have `invoked_from_error_handler` `True`.""" - self.ctx.invoked_from_error_handler = False - await self.cog.try_get_tag(self.ctx) - self.assertTrue(self.ctx.invoked_from_error_handler) - async def test_try_get_tag_no_permissions(self): """Test how to handle checks failing.""" self.bot.can_run = AsyncMock(return_value=False) self.ctx.invoked_with = "foo" self.assertIsNone(await self.cog.try_get_tag(self.ctx)) - async def test_try_get_tag_command_error(self): - """Should call `on_command_error` when `CommandError` raised.""" - err = errors.CommandError() - self.bot.can_run = AsyncMock(side_effect=err) - self.cog.on_command_error = AsyncMock() - self.assertIsNone(await self.cog.try_get_tag(self.ctx)) - self.cog.on_command_error.assert_awaited_once_with(self.ctx, err) - async def test_dont_call_suggestion_tag_sent(self): """Should never call command suggestion if tag is already sent.""" self.ctx.message = MagicMock(content="foo") @@ -375,7 +357,7 @@ class TryGetTagTests(unittest.IsolatedAsyncioTestCase): async def test_dont_call_suggestion_if_user_mod(self): """Should not call command suggestion if user is a mod.""" self.ctx.invoked_with = "foo" - self.ctx.invoke = AsyncMock(return_value=False) + self.tag.get_command_ctx = AsyncMock(return_value=False) self.ctx.author.roles = [MockRole(id=1234)] self.cog.send_command_suggestion = AsyncMock() -- cgit v1.2.3 From e9b47304ff0be54369ef7598b9c71401f950b0ac Mon Sep 17 00:00:00 2001 From: ChrisJL Date: Wed, 4 Oct 2023 04:55:30 +0100 Subject: Update all references of 3.11 in eval to 3.12 (#2777) --- bot/exts/utils/snekbox/_cog.py | 10 +++++----- bot/exts/utils/snekbox/_eval.py | 2 +- tests/bot/exts/utils/snekbox/test_snekbox.py | 28 ++++++++++++++-------------- 3 files changed, 20 insertions(+), 20 deletions(-) (limited to 'tests') diff --git a/bot/exts/utils/snekbox/_cog.py b/bot/exts/utils/snekbox/_cog.py index 9f2d87a15..08d867d0f 100644 --- a/bot/exts/utils/snekbox/_cog.py +++ b/bot/exts/utils/snekbox/_cog.py @@ -86,7 +86,7 @@ SNEKBOX_ROLES = (Roles.helpers, Roles.moderators, Roles.admins, Roles.owners, Ro REDO_EMOJI = "\U0001f501" # :repeat: REDO_TIMEOUT = 30 -SupportedPythonVersions = Literal["3.11"] +SupportedPythonVersions = Literal["3.12"] class FilteredFiles(NamedTuple): @@ -562,13 +562,13 @@ class Snekbox(Cog): If multiple codeblocks are in a message, all of them will be joined and evaluated, ignoring the text outside them. - Currently only 3.11 version is supported. + Currently only 3.12 version is supported. We've done our best to make this sandboxed, but do let us know if you manage to find an issue with it! """ code: list[str] - python_version = python_version or "3.11" + python_version = python_version or "3.12" job = EvalJob.from_code("\n".join(code)).as_version(python_version) await self.run_job(ctx, job) @@ -598,13 +598,13 @@ class Snekbox(Cog): If multiple formatted codeblocks are provided, the first one will be the setup code, which will not be timed. The remaining codeblocks will be joined together and timed. - Currently only 3.11 version is supported. + Currently only 3.12 version is supported. We've done our best to make this sandboxed, but do let us know if you manage to find an issue with it! """ code: list[str] - python_version = python_version or "3.11" + python_version = python_version or "3.12" args = self.prepare_timeit_input(code) job = EvalJob(args, version=python_version, name="timeit") diff --git a/bot/exts/utils/snekbox/_eval.py b/bot/exts/utils/snekbox/_eval.py index 9eb8039ce..d3d1e7a18 100644 --- a/bot/exts/utils/snekbox/_eval.py +++ b/bot/exts/utils/snekbox/_eval.py @@ -26,7 +26,7 @@ class EvalJob: args: list[str] files: list[FileAttachment] = field(default_factory=list) name: str = "eval" - version: SupportedPythonVersions = "3.11" + version: SupportedPythonVersions = "3.12" @classmethod def from_code(cls, code: str, path: str = "main.py") -> EvalJob: diff --git a/tests/bot/exts/utils/snekbox/test_snekbox.py b/tests/bot/exts/utils/snekbox/test_snekbox.py index 200aa5a20..8ee0f46ff 100644 --- a/tests/bot/exts/utils/snekbox/test_snekbox.py +++ b/tests/bot/exts/utils/snekbox/test_snekbox.py @@ -103,9 +103,9 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): 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.12 eval job has failed", "ERROR", "")), + ("", 128 + snekbox._eval.SIGKILL, ("Your 3.12 eval job timed out or ran out of memory", "", "")), + ("", 255, ("Your 3.12 eval job has failed", "A fatal NsJail error occurred", "")) ) for stdout, returncode, expected in cases: exp_msg, exp_err, exp_files_err = expected @@ -177,8 +177,8 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): mock_signals.return_value.name = "SIGTEST" result = EvalResult(stdout="", returncode=127) self.assertEqual( - result.get_message(EvalJob([], version="3.11")), - "Your 3.11 eval job has completed with return code 127 (SIGTEST)" + result.get_message(EvalJob([], version="3.12")), + "Your 3.12 eval job has completed with return code 127 (SIGTEST)" ) def test_eval_result_status_emoji(self): @@ -252,7 +252,7 @@ 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.12", 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") @@ -266,7 +266,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): self.cog.continue_job = AsyncMock() 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.12", code=["MyAwesomeCode"]) expected_job = EvalJob.from_code("MyAwesomeFormattedCode") self.cog.send_job.assert_called_with(ctx, expected_job) @@ -311,7 +311,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): ctx.send.assert_called_once() self.assertEqual( ctx.send.call_args.args[0], - "@LemonLemonishBeard#0042 :warning: Your 3.11 eval job has completed " + "@LemonLemonishBeard#0042 :warning: Your 3.12 eval job has completed " "with return code 0.\n\n```\n[No output]\n```" ) allowed_mentions = ctx.send.call_args.kwargs["allowed_mentions"] @@ -337,13 +337,13 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): mocked_filter_cog.filter_snekbox_output = AsyncMock(return_value=(False, [])) self.bot.get_cog.return_value = mocked_filter_cog - job = EvalJob.from_code("MyAwesomeCode").as_version("3.11") + job = EvalJob.from_code("MyAwesomeCode").as_version("3.12") await self.cog.send_job(ctx, job), ctx.send.assert_called_once() self.assertEqual( ctx.send.call_args.args[0], - "@LemonLemonishBeard#0042 :white_check_mark: Your 3.11 eval job " + "@LemonLemonishBeard#0042 :white_check_mark: Your 3.12 eval job " "has completed with return code 0." "\n\n```\nWay too long beard\n```\nFull output: lookatmybeard.com" ) @@ -366,13 +366,13 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): mocked_filter_cog.filter_snekbox_output = AsyncMock(return_value=(False, [])) self.bot.get_cog.return_value = mocked_filter_cog - job = EvalJob.from_code("MyAwesomeCode").as_version("3.11") + job = EvalJob.from_code("MyAwesomeCode").as_version("3.12") await self.cog.send_job(ctx, job), 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." + "@LemonLemonishBeard#0042 :x: Your 3.12 eval job has completed with return code 127." "\n\n```\nERROR\n```" ) @@ -394,13 +394,13 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): mocked_filter_cog.filter_snekbox_output = AsyncMock(return_value=(False, [".disallowed"])) self.bot.get_cog.return_value = mocked_filter_cog - job = EvalJob.from_code("MyAwesomeCode").as_version("3.11") + job = EvalJob.from_code("MyAwesomeCode").as_version("3.12") await self.cog.send_job(ctx, job), ctx.send.assert_called_once() res = ctx.send.call_args.args[0] self.assertTrue( - res.startswith("@user#7700 :white_check_mark: Your 3.11 eval job has completed with return code 0.") + res.startswith("@user#7700 :white_check_mark: Your 3.12 eval job has completed with return code 0.") ) self.assertIn("Files with disallowed extensions can't be uploaded: **.disallowed**", res) -- cgit v1.2.3 From cb82c685127aee606413390ca68196012cac5979 Mon Sep 17 00:00:00 2001 From: Xithrius Date: Wed, 8 Nov 2023 21:54:40 -0800 Subject: Appeased the linter --- tests/test_helpers.py | 1 - 1 file changed, 1 deletion(-) (limited to 'tests') diff --git a/tests/test_helpers.py b/tests/test_helpers.py index fa7d0eb44..3d4cb09e7 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -316,7 +316,6 @@ class MockObjectTests(unittest.TestCase): class MyMock(helpers.CustomMockMixin, unittest.mock.MagicMock): child_mock_type = unittest.mock.MagicMock - pass mock = MyMock() unittest.mock.seal(mock) -- cgit v1.2.3 From 6ba1588fbd2c163d4b25604958ad427ea46497bc Mon Sep 17 00:00:00 2001 From: wookie184 Date: Sun, 31 Dec 2023 15:20:00 +0000 Subject: Fix test code that was doing nothing --- tests/bot/exts/backend/test_error_handler.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py index e28d5cfd0..9670d42a0 100644 --- a/tests/bot/exts/backend/test_error_handler.py +++ b/tests/bot/exts/backend/test_error_handler.py @@ -1,5 +1,5 @@ import unittest -from unittest.mock import AsyncMock, MagicMock, call, patch +from unittest.mock import AsyncMock, MagicMock, Mock, call, patch from discord.ext.commands import errors from pydis_core.site_api import ResponseCodeError @@ -503,6 +503,10 @@ class IndividualErrorHandlerTests(unittest.IsolatedAsyncioTestCase): self.ctx.reset_mock() log_mock.reset_mock() push_scope_mock.reset_mock() + scope_mock = Mock() + + # Mock `with push_scope_mock() as scope:` + push_scope_mock.return_value.__enter__.return_value = scope_mock self.ctx.guild = case await self.cog.handle_unexpected_error(self.ctx, errors.CommandError()) @@ -526,8 +530,8 @@ class IndividualErrorHandlerTests(unittest.IsolatedAsyncioTestCase): ) set_extra_calls.append(call("jump_to", url)) - push_scope_mock.set_tag.has_calls(set_tag_calls) - push_scope_mock.set_extra.has_calls(set_extra_calls) + scope_mock.set_tag.assert_has_calls(set_tag_calls) + scope_mock.set_extra.assert_has_calls(set_extra_calls) class ErrorHandlerSetupTests(unittest.IsolatedAsyncioTestCase): -- cgit v1.2.3 From e753bb3e9e05e7dcd28febdef851c04fe9f20ce7 Mon Sep 17 00:00:00 2001 From: wookie184 Date: Mon, 8 Jan 2024 01:18:01 +0000 Subject: Move modlog method to util (#2877) --- bot/exts/moderation/clean.py | 4 +- bot/exts/moderation/defcon.py | 13 +- bot/exts/moderation/infraction/_scheduler.py | 10 +- bot/exts/moderation/infraction/management.py | 10 +- bot/exts/moderation/modlog.py | 211 ++++++++++----------- bot/exts/moderation/watchchannels/_watchchannel.py | 13 +- bot/utils/modlog.py | 69 +++++++ tests/bot/exts/moderation/test_modlog.py | 4 +- 8 files changed, 203 insertions(+), 131 deletions(-) create mode 100644 bot/utils/modlog.py (limited to 'tests') diff --git a/bot/exts/moderation/clean.py b/bot/exts/moderation/clean.py index 1c73736d7..d38de15e0 100644 --- a/bot/exts/moderation/clean.py +++ b/bot/exts/moderation/clean.py @@ -21,6 +21,7 @@ from bot.exts.moderation.modlog import ModLog from bot.log import get_logger from bot.utils.channel import is_mod_channel from bot.utils.messages import upload_log +from bot.utils.modlog import send_log_message log = get_logger(__name__) @@ -367,7 +368,8 @@ class Clean(Cog): f"A log of the deleted messages can be found [here]({log_url})." ) - await self.mod_log.send_log_message( + await send_log_message( + self.bot, icon_url=Icons.message_bulk_delete, colour=Colour(Colours.soft_red), title="Bulk message delete", diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 3c16f8e0e..3196c4a1a 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -21,6 +21,7 @@ from bot.exts.moderation.modlog import ModLog from bot.log import get_logger from bot.utils import time from bot.utils.messages import format_user +from bot.utils.modlog import send_log_message log = get_logger(__name__) @@ -136,9 +137,13 @@ class Defcon(Cog): if not message_sent: message = f"{message}\n\nUnable to send rejection message via DM; they probably have DMs disabled." - await (await self.get_mod_log()).send_log_message( - Icons.defcon_denied, Colours.soft_red, "Entry denied", - message, member.display_avatar.url + await send_log_message( + self.bot, + Icons.defcon_denied, + Colours.soft_red, + "Entry denied", + message, + thumbnail=member.display_avatar.url ) @group(name="defcon", aliases=("dc",), invoke_without_command=True) @@ -304,7 +309,7 @@ class Defcon(Cog): ) status_msg = f"DEFCON {action.name.lower()}" - await (await self.get_mod_log()).send_log_message(info.icon, info.color, status_msg, log_msg) + await send_log_message(self.bot, info.icon, info.color, status_msg, log_msg) def _update_notifier(self) -> None: """Start or stop the notifier according to the DEFCON status.""" diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index a079f775e..aed6210a2 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -20,6 +20,7 @@ from bot.exts.moderation.modlog import ModLog from bot.log import get_logger from bot.utils import messages, time from bot.utils.channel import is_mod_channel +from bot.utils.modlog import send_log_message log = get_logger(__name__) @@ -270,7 +271,8 @@ class InfractionScheduler: # Send a log message to the mod log. # Don't use ctx.message.author for the actor; antispam only patches ctx.author. log.trace(f"Sending apply mod log for infraction #{id_}.") - await self.mod_log.send_log_message( + await send_log_message( + self.bot, icon_url=icon, colour=Colours.soft_red, title=f"Infraction {log_title}: {' '.join(infr_type.split('_'))}", @@ -369,7 +371,8 @@ class InfractionScheduler: log_text["Reason"] = log_text.pop("Reason") # Send a log message to the mod log. - await self.mod_log.send_log_message( + await send_log_message( + self.bot, icon_url=_utils.INFRACTION_ICONS[infr_type][1], colour=Colours.soft_green, title=f"Infraction {log_title}: {' '.join(infr_type.split('_'))}", @@ -507,7 +510,8 @@ class InfractionScheduler: log_text["Reason"] = log_text.pop("Reason") log.trace(f"Sending deactivation mod log for infraction #{id_}.") - await self.mod_log.send_log_message( + await send_log_message( + self.bot, icon_url=_utils.INFRACTION_ICONS[type_][1], colour=Colours.soft_green, title=f"Infraction {log_title}: {type_}", diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index 9bf3a4dbb..93959042b 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -16,11 +16,11 @@ from bot.decorators import ensure_future_timestamp 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 from bot.log import get_logger from bot.pagination import LinePaginator from bot.utils import messages, time from bot.utils.channel import is_in_category, is_mod_channel +from bot.utils.modlog import send_log_message from bot.utils.time import unpack_duration log = get_logger(__name__) @@ -57,11 +57,6 @@ class ModManagement(commands.Cog): ): command.help += f"\n{SYMBOLS_GUIDE}" - @property - def mod_log(self) -> ModLog: - """Get currently loaded ModLog cog instance.""" - return self.bot.get_cog("ModLog") - @property def infractions_cog(self) -> Infractions: """Get currently loaded Infractions cog instance.""" @@ -260,7 +255,8 @@ class ModManagement(commands.Cog): else: jump_url = f"[Click here.]({ctx.message.jump_url})" - await self.mod_log.send_log_message( + await send_log_message( + self.bot, icon_url=constants.Icons.pencil, colour=discord.Colour.og_blurple(), title="Infraction edited", diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index b349f4d5d..3c256396a 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -8,7 +8,7 @@ from dateutil.relativedelta import relativedelta from deepdiff import DeepDiff from discord import Colour, Message, Thread from discord.abc import GuildChannel -from discord.ext.commands import Cog, Context +from discord.ext.commands import Cog from discord.utils import escape_markdown, format_dt, snowflake_time from bot.bot import Bot @@ -16,6 +16,7 @@ from bot.constants import Channels, Colours, Emojis, Event, Guild as GuildConsta from bot.log import get_logger from bot.utils import time from bot.utils.messages import format_user, upload_log +from bot.utils.modlog import send_log_message log = get_logger(__name__) @@ -47,63 +48,6 @@ class ModLog(Cog, name="ModLog"): if item not in self._ignored[event]: self._ignored[event].append(item) - async def send_log_message( - self, - icon_url: str | None, - colour: discord.Colour | int, - title: str | None, - text: str, - thumbnail: str | discord.Asset | None = None, - channel_id: int = Channels.mod_log, - ping_everyone: bool = False, - 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() - # Truncate string directly here to avoid removing newlines - embed = discord.Embed( - description=text[:4093] + "..." if len(text) > 4096 else text - ) - - if title and icon_url: - embed.set_author(name=title, icon_url=icon_url) - - embed.colour = colour - embed.timestamp = timestamp_override or datetime.now(tz=UTC) - - if footer: - embed.set_footer(text=footer) - - if thumbnail: - embed.set_thumbnail(url=thumbnail) - - if ping_everyone: - if content: - content = f"<@&{Roles.moderators}> {content}" - else: - content = f"<@&{Roles.moderators}>" - - # Truncate content to 2000 characters and append an ellipsis. - if content and len(content) > 2000: - content = content[:2000 - 3] + "..." - - channel = self.bot.get_channel(channel_id) - log_message = await channel.send( - content=content, - embed=embed, - files=files - ) - - if additional_embeds: - for additional_embed in additional_embeds: - await channel.send(embed=additional_embed) - - return await self.bot.get_context(log_message) # Optionally return for use with antispam - @Cog.listener() async def on_guild_channel_create(self, channel: GUILD_CHANNEL) -> None: """Log channel create event to mod log.""" @@ -128,7 +72,7 @@ class ModLog(Cog, name="ModLog"): else: message = f"{channel.name} (`{channel.id}`)" - await self.send_log_message(Icons.hash_green, Colours.soft_green, title, message) + await send_log_message(self.bot, Icons.hash_green, Colours.soft_green, title, message) @Cog.listener() async def on_guild_channel_delete(self, channel: GUILD_CHANNEL) -> None: @@ -148,9 +92,12 @@ class ModLog(Cog, name="ModLog"): else: message = f"{channel.name} (`{channel.id}`)" - await self.send_log_message( - Icons.hash_red, Colours.soft_red, - title, message + await send_log_message( + self.bot, + Icons.hash_red, + Colours.soft_red, + title, + message ) @Cog.listener() @@ -211,9 +158,12 @@ class ModLog(Cog, name="ModLog"): else: message = f"**#{after.name}** (`{after.id}`)\n{message}" - await self.send_log_message( - Icons.hash_blurple, Colour.og_blurple(), - "Channel updated", message + await send_log_message( + self.bot, + Icons.hash_blurple, + Colour.og_blurple(), + "Channel updated", + message ) @Cog.listener() @@ -222,9 +172,12 @@ class ModLog(Cog, name="ModLog"): if role.guild.id != GuildConstant.id: return - await self.send_log_message( - Icons.crown_green, Colours.soft_green, - "Role created", f"`{role.id}`" + await send_log_message( + self.bot, + Icons.crown_green, + Colours.soft_green, + "Role created", + f"`{role.id}`" ) @Cog.listener() @@ -233,9 +186,12 @@ class ModLog(Cog, name="ModLog"): if role.guild.id != GuildConstant.id: return - await self.send_log_message( - Icons.crown_red, Colours.soft_red, - "Role removed", f"{role.name} (`{role.id}`)" + await send_log_message( + self.bot, + Icons.crown_red, + Colours.soft_red, + "Role removed", + f"{role.name} (`{role.id}`)" ) @Cog.listener() @@ -286,9 +242,12 @@ class ModLog(Cog, name="ModLog"): message = f"**{after.name}** (`{after.id}`)\n{message}" - await self.send_log_message( - Icons.crown_blurple, Colour.og_blurple(), - "Role updated", message + await send_log_message( + self.bot, + Icons.crown_blurple, + Colour.og_blurple(), + "Role updated", + message ) @Cog.listener() @@ -336,9 +295,12 @@ class ModLog(Cog, name="ModLog"): message = f"**{after.name}** (`{after.id}`)\n{message}" - await self.send_log_message( - Icons.guild_update, Colour.og_blurple(), - "Guild updated", message, + await send_log_message( + self.bot, + Icons.guild_update, + Colour.og_blurple(), + "Guild updated", + message, thumbnail=after.icon.with_static_format("png") ) @@ -352,9 +314,12 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.member_ban].remove(member.id) return - await self.send_log_message( - Icons.user_ban, Colours.soft_red, - "User banned", format_user(member), + await send_log_message( + self.bot, + Icons.user_ban, + Colours.soft_red, + "User banned", + format_user(member), thumbnail=member.display_avatar.url, channel_id=Channels.user_log ) @@ -373,9 +338,12 @@ class ModLog(Cog, name="ModLog"): if difference.days < 1 and difference.months < 1 and difference.years < 1: # New user account! message = f"{Emojis.new} {message}" - await self.send_log_message( - Icons.sign_in, Colours.soft_green, - "User joined", message, + await send_log_message( + self.bot, + Icons.sign_in, + Colours.soft_green, + "User joined", + message, thumbnail=member.display_avatar.url, channel_id=Channels.user_log ) @@ -390,9 +358,12 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.member_remove].remove(member.id) return - await self.send_log_message( - Icons.sign_out, Colours.soft_red, - "User left", format_user(member), + await send_log_message( + self.bot, + Icons.sign_out, + Colours.soft_red, + "User left", + format_user(member), thumbnail=member.display_avatar.url, channel_id=Channels.user_log ) @@ -407,9 +378,12 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.member_unban].remove(member.id) return - await self.send_log_message( - Icons.user_unban, Colour.og_blurple(), - "User unbanned", format_user(member), + await send_log_message( + self.bot, + Icons.user_unban, + Colour.og_blurple(), + "User unbanned", + format_user(member), thumbnail=member.display_avatar.url, channel_id=Channels.mod_log ) @@ -471,7 +445,8 @@ class ModLog(Cog, name="ModLog"): message = f"{format_user(after)}\n{message}" - await self.send_log_message( + await send_log_message( + self.bot, icon_url=Icons.user_update, colour=Colour.og_blurple(), title="Member updated", @@ -565,8 +540,10 @@ class ModLog(Cog, name="ModLog"): response += f"{content}" - await self.send_log_message( - Icons.message_delete, Colours.soft_red, + await send_log_message( + self.bot, + Icons.message_delete, + Colours.soft_red, "Message deleted", response, channel_id=Channels.message_log @@ -606,8 +583,10 @@ class ModLog(Cog, name="ModLog"): "This message was not cached, so the message content cannot be displayed." ) - await self.send_log_message( - Icons.message_delete, Colours.soft_red, + await send_log_message( + self.bot, + Icons.message_delete, + Colours.soft_red, "Message deleted", response, channel_id=Channels.message_log @@ -687,9 +666,15 @@ class ModLog(Cog, name="ModLog"): timestamp = msg_before.created_at footer = None - await self.send_log_message( - Icons.message_edit, Colour.og_blurple(), "Message edited", response, - channel_id=Channels.message_log, timestamp_override=timestamp, footer=footer + await send_log_message( + self.bot, + Icons.message_edit, + Colour.og_blurple(), + "Message edited", + response, + channel_id=Channels.message_log, + timestamp_override=timestamp, + footer=footer ) @Cog.listener() @@ -734,14 +719,22 @@ class ModLog(Cog, name="ModLog"): f"{message.clean_content}" ) - await self.send_log_message( - Icons.message_edit, Colour.og_blurple(), "Message edited (Before)", - before_response, channel_id=Channels.message_log + await send_log_message( + self.bot, + Icons.message_edit, + Colour.og_blurple(), + "Message edited (Before)", + before_response, + channel_id=Channels.message_log ) - await self.send_log_message( - Icons.message_edit, Colour.og_blurple(), "Message edited (After)", - after_response, channel_id=Channels.message_log + await send_log_message( + self.bot, + Icons.message_edit, + Colour.og_blurple(), + "Message edited (After)", + after_response, + channel_id=Channels.message_log ) @Cog.listener() @@ -752,7 +745,8 @@ class ModLog(Cog, name="ModLog"): return if before.name != after.name: - await self.send_log_message( + await send_log_message( + self.bot, Icons.hash_blurple, Colour.og_blurple(), "Thread name edited", @@ -774,7 +768,8 @@ class ModLog(Cog, name="ModLog"): else: return - await self.send_log_message( + await send_log_message( + self.bot, icon, colour, f"Thread {action}", @@ -792,7 +787,8 @@ class ModLog(Cog, name="ModLog"): log.trace("Ignoring deletion of thread %s (%d)", thread.mention, thread.id) return - await self.send_log_message( + await send_log_message( + self.bot, Icons.hash_red, Colours.soft_red, "Thread deleted", @@ -868,7 +864,8 @@ class ModLog(Cog, name="ModLog"): message = "\n".join(f"{Emojis.bullet} {item}" for item in sorted(changes)) message = f"{format_user(member)}\n{message}" - await self.send_log_message( + await send_log_message( + self.bot, icon_url=icon, colour=colour, title="Voice state updated", diff --git a/bot/exts/moderation/watchchannels/_watchchannel.py b/bot/exts/moderation/watchchannels/_watchchannel.py index d0fc0de44..71600e9df 100644 --- a/bot/exts/moderation/watchchannels/_watchchannel.py +++ b/bot/exts/moderation/watchchannels/_watchchannel.py @@ -19,10 +19,10 @@ from bot.bot import Bot from bot.constants import BigBrother as BigBrotherConfig, Guild as GuildConfig, Icons from bot.exts.filtering._filters.unique.discord_token import DiscordTokenFilter from bot.exts.filtering._filters.unique.webhook import WEBHOOK_URL_RE -from bot.exts.moderation.modlog import ModLog from bot.log import get_logger from bot.pagination import LinePaginator from bot.utils import CogABCMeta, messages, time +from bot.utils.modlog import send_log_message log = get_logger(__name__) @@ -72,11 +72,6 @@ class WatchChannel(metaclass=CogABCMeta): self.message_history = MessageHistory() self.disable_header = disable_header - @property - def modlog(self) -> ModLog: - """Provides access to the ModLog cog for alert purposes.""" - return self.bot.get_cog("ModLog") - @property def consuming_messages(self) -> bool: """Checks if a consumption task is currently running.""" @@ -122,7 +117,8 @@ class WatchChannel(metaclass=CogABCMeta): """ ) - await self.modlog.send_log_message( + await send_log_message( + self.bot, title=f"Error: Failed to initialize the {self.__class__.__name__} watch channel", text=message, ping_everyone=True, @@ -134,7 +130,8 @@ class WatchChannel(metaclass=CogABCMeta): return if not await self.fetch_user_cache(): - await self.modlog.send_log_message( + await send_log_message( + self.bot, title=f"Warning: Failed to retrieve user cache for the {self.__class__.__name__} watch channel", text=( "Could not retrieve the list of watched users from the API. " diff --git a/bot/utils/modlog.py b/bot/utils/modlog.py new file mode 100644 index 000000000..6a432e65e --- /dev/null +++ b/bot/utils/modlog.py @@ -0,0 +1,69 @@ +from datetime import UTC, datetime + +import discord + +from bot.bot import Bot +from bot.constants import Channels, Roles + + +async def send_log_message( + bot: Bot, + icon_url: str | None, + colour: discord.Colour | int, + title: str | None, + text: str, + *, + thumbnail: str | discord.Asset | None = None, + channel_id: int = Channels.mod_log, + ping_everyone: bool = False, + 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, +) -> discord.Message: + """Generate log embed and send to logging channel.""" + await bot.wait_until_guild_available() + # Truncate string directly here to avoid removing newlines + embed = discord.Embed( + description=text[:4093] + "..." if len(text) > 4096 else text + ) + + if title and icon_url: + embed.set_author(name=title, icon_url=icon_url) + elif title: + raise ValueError("title cannot be set without icon_url") + elif icon_url: + raise ValueError("icon_url cannot be set without title") + + embed.colour = colour + embed.timestamp = timestamp_override or datetime.now(tz=UTC) + + if footer: + embed.set_footer(text=footer) + + if thumbnail: + embed.set_thumbnail(url=thumbnail) + + if ping_everyone: + if content: + content = f"<@&{Roles.moderators}> {content}" + else: + content = f"<@&{Roles.moderators}>" + + # Truncate content to 2000 characters and append an ellipsis. + if content and len(content) > 2000: + content = content[:2000 - 3] + "..." + + channel = bot.get_channel(channel_id) + log_message = await channel.send( + content=content, + embed=embed, + files=files + ) + + if additional_embeds: + for additional_embed in additional_embeds: + await channel.send(embed=additional_embed) + + return log_message diff --git a/tests/bot/exts/moderation/test_modlog.py b/tests/bot/exts/moderation/test_modlog.py index 79e04837d..f2b02bd1b 100644 --- a/tests/bot/exts/moderation/test_modlog.py +++ b/tests/bot/exts/moderation/test_modlog.py @@ -3,6 +3,7 @@ import unittest import discord from bot.exts.moderation.modlog import ModLog +from bot.utils.modlog import send_log_message from tests.helpers import MockBot, MockTextChannel @@ -17,7 +18,8 @@ class ModLogTests(unittest.IsolatedAsyncioTestCase): async def test_log_entry_description_truncation(self): """Test that embed description for ModLog entry is truncated.""" self.bot.get_channel.return_value = self.channel - await self.cog.send_log_message( + await send_log_message( + self.bot, icon_url="foo", colour=discord.Colour.blue(), title="bar", -- cgit v1.2.3 From 888c4c8f17a871a10f664661a301a0ef135c9c10 Mon Sep 17 00:00:00 2001 From: shtlrs Date: Fri, 5 Jan 2024 16:00:40 +0100 Subject: delete pagination tests These have been ported to bot-core --- tests/bot/test_pagination.py | 46 -------------------------------------------- 1 file changed, 46 deletions(-) delete mode 100644 tests/bot/test_pagination.py (limited to 'tests') diff --git a/tests/bot/test_pagination.py b/tests/bot/test_pagination.py deleted file mode 100644 index cf23f1948..000000000 --- a/tests/bot/test_pagination.py +++ /dev/null @@ -1,46 +0,0 @@ -from unittest import TestCase - -from bot import pagination - - -class LinePaginatorTests(TestCase): - """Tests functionality of the `LinePaginator`.""" - - def setUp(self): - """Create a paginator for the test method.""" - 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)) - # 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.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.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.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.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)) - # 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) -- cgit v1.2.3 From f86bd795ee0e80b4e511946231dfd74ae2392f75 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 2 Feb 2024 16:22:05 +0000 Subject: Replace aliased errors with TimeoutError --- bot/exts/backend/branding/_cog.py | 2 +- bot/exts/fun/off_topic_names.py | 2 +- bot/exts/moderation/incidents.py | 2 +- bot/exts/utils/snekbox/_cog.py | 3 +-- bot/utils/messages.py | 3 +-- tests/bot/exts/moderation/test_incidents.py | 4 ++-- 6 files changed, 7 insertions(+), 9 deletions(-) (limited to 'tests') diff --git a/bot/exts/backend/branding/_cog.py b/bot/exts/backend/branding/_cog.py index 6472a477c..433f152b4 100644 --- a/bot/exts/backend/branding/_cog.py +++ b/bot/exts/backend/branding/_cog.py @@ -162,7 +162,7 @@ class Branding(commands.Cog): except discord.HTTPException: log.exception("Asset upload to Discord failed.") return False - except asyncio.TimeoutError: + except TimeoutError: log.error(f"Asset upload to Discord timed out after {timeout} seconds.") return False else: diff --git a/bot/exts/fun/off_topic_names.py b/bot/exts/fun/off_topic_names.py index a2941932b..21dfaee20 100644 --- a/bot/exts/fun/off_topic_names.py +++ b/bot/exts/fun/off_topic_names.py @@ -207,7 +207,7 @@ class OffTopicNames(Cog): try: await asyncio.wait_for(rename_channel(), 3) - except asyncio.TimeoutError: + except TimeoutError: # Channel rename endpoint rate limited. The task was cancelled by asyncio. btn_yes = Button(label="Yes", style=ButtonStyle.success) btn_no = Button(label="No", style=ButtonStyle.danger) diff --git a/bot/exts/moderation/incidents.py b/bot/exts/moderation/incidents.py index 2aecf787a..9c11ad96c 100644 --- a/bot/exts/moderation/incidents.py +++ b/bot/exts/moderation/incidents.py @@ -478,7 +478,7 @@ class Incidents(Cog): log.trace(f"Awaiting deletion confirmation: {timeout=} seconds") try: await confirmation_task - except asyncio.TimeoutError: + except TimeoutError: log.info(f"Did not receive incident deletion confirmation within {timeout} seconds!") else: log.trace("Deletion was confirmed") diff --git a/bot/exts/utils/snekbox/_cog.py b/bot/exts/utils/snekbox/_cog.py index 08d867d0f..db4181d68 100644 --- a/bot/exts/utils/snekbox/_cog.py +++ b/bot/exts/utils/snekbox/_cog.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio import contextlib import re from functools import partial @@ -460,7 +459,7 @@ class Snekbox(Cog): if code is None: return None - except asyncio.TimeoutError: + except TimeoutError: with contextlib.suppress(HTTPException): await ctx.message.clear_reaction(REDO_EMOJI) return None diff --git a/bot/utils/messages.py b/bot/utils/messages.py index 4febf09dc..57bb2c8e3 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -1,4 +1,3 @@ -import asyncio import random import re from collections.abc import Callable, Iterable, Sequence @@ -102,7 +101,7 @@ async def wait_for_deletion( try: try: await bot.instance.wait_for("reaction_add", check=check, timeout=timeout) - except asyncio.TimeoutError: + except TimeoutError: await message.clear_reactions() else: await message.delete() diff --git a/tests/bot/exts/moderation/test_incidents.py b/tests/bot/exts/moderation/test_incidents.py index d8e3ddd8c..444bb1142 100644 --- a/tests/bot/exts/moderation/test_incidents.py +++ b/tests/bot/exts/moderation/test_incidents.py @@ -557,7 +557,7 @@ class TestProcessEvent(TestIncidents): exception should it propagate out of `process_event`. This is so that we can then manually fail the test with a more informative message than just the plain traceback. """ - mock_task = AsyncMock(side_effect=asyncio.TimeoutError()) + mock_task = AsyncMock(side_effect=TimeoutError()) try: with patch("bot.exts.moderation.incidents.Incidents.make_confirmation_task", mock_task): @@ -566,7 +566,7 @@ class TestProcessEvent(TestIncidents): incident=MockMessage(id=123, created_at=CURRENT_TIME), member=MockMember(roles=[MockRole(id=1)]) ) - except asyncio.TimeoutError: + except TimeoutError: self.fail("TimeoutError was not handled gracefully, and propagated out of `process_event`!") -- cgit v1.2.3