diff --git a/pyproject.toml b/pyproject.toml index 9420fee8d8..dacc2bc40c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,9 +38,8 @@ dev = [ "coverage==7.11.0", "httpx==0.28.1", "pre-commit==4.3.0", - "pytest==8.4.2", + "pytest==9.0.0", "pytest-cov==7.0.0", - "pytest-subtests==0.14.1", "pytest-xdist==3.8.0", "ruff==0.14.2", "taskipy==1.14.1", @@ -96,8 +95,8 @@ combine-as-imports = true [tool.ruff.lint.per-file-ignores] "tests/*" = ["ANN", "D"] -[tool.pytest.ini_options] +[tool.pytest] # We don't use nose style tests so disable them in pytest. # This stops pytest from running functions named `setup` in test files. # See https://github.com/python-discord/bot/pull/2229#issuecomment-1204436420 -addopts = "-p no:nose" +addopts = ["-p", "no:nose"] diff --git a/tests/bot/exts/backend/sync/test_base.py b/tests/bot/exts/backend/sync/test_base.py index 4dacfda17a..a98b6251ff 100644 --- a/tests/bot/exts/backend/sync/test_base.py +++ b/tests/bot/exts/backend/sync/test_base.py @@ -39,8 +39,8 @@ async def test_sync_message_edited(self): (helpers.MockMessage(), ResponseCodeError(mock.MagicMock()), True), ) - for message, side_effect, should_edit in subtests: - with self.subTest(message=message, side_effect=side_effect, should_edit=should_edit): + for i, (message, side_effect, should_edit) in enumerate(subtests): + with self.subTest(test_case=i, has_message=message is not None, should_edit=should_edit): TestSyncer._sync.side_effect = side_effect ctx = helpers.MockContext() ctx.send.return_value = message @@ -58,8 +58,8 @@ async def test_sync_message_sent(self): (helpers.MockContext(), helpers.MockMessage()), ) - for ctx, message in subtests: - with self.subTest(ctx=ctx, message=message): + for i, (ctx, _message) in enumerate(subtests): + with self.subTest(test_case=i, has_ctx=ctx is not None): await TestSyncer.sync(self.guild, ctx) if ctx is not None: diff --git a/tests/bot/exts/backend/sync/test_cog.py b/tests/bot/exts/backend/sync/test_cog.py index 6d7356bf27..d67e709c84 100644 --- a/tests/bot/exts/backend/sync/test_cog.py +++ b/tests/bot/exts/backend/sync/test_cog.py @@ -65,8 +65,8 @@ class SyncCogTests(SyncCogTestCase): @unittest.mock.patch("bot.exts.backend.sync._cog.create_task", new_callable=unittest.mock.MagicMock) async def test_sync_cog_sync_on_load(self, mock_create_task: unittest.mock.MagicMock): """Sync function should be synced on cog load only if guild is found.""" - for guild in (helpers.MockGuild(), None): - with self.subTest(guild=guild): + for i, guild in enumerate((helpers.MockGuild(), None)): + with self.subTest(test_case=i, has_guild=guild is not None): mock_create_task.reset_mock() self.bot.reset_mock() self.RoleSyncer.reset_mock() @@ -126,8 +126,8 @@ async def patch_user_helper(self, side_effect: BaseException) -> None: async def test_sync_cog_patch_user(self): """A PATCH request should be sent and 404 errors ignored.""" - for side_effect in (None, self.response_error(404)): - with self.subTest(side_effect=side_effect): + for i, side_effect in enumerate((None, self.response_error(404))): + with self.subTest(test_case=i, has_error=side_effect is not None): await self.patch_user_helper(side_effect) async def test_sync_cog_patch_user_non_404(self): @@ -207,7 +207,7 @@ async def test_sync_cog_on_guild_role_update(self): for should_put, attributes in subtests: for attribute in attributes: - with self.subTest(should_put=should_put, changed_attribute=attribute): + with self.subTest(should_put=should_put, attribute=attribute): self.bot.api_client.put.reset_mock() after_role_data = role_data.copy() @@ -372,8 +372,8 @@ async def on_member_join_helper(self, side_effect: Exception) -> dict: async def test_sync_cog_on_member_join(self): """Should PUT user's data or POST it if the user doesn't exist.""" - for side_effect in (None, self.response_error(404)): - with self.subTest(side_effect=side_effect): + for i, side_effect in enumerate((None, self.response_error(404))): + with self.subTest(test_case=i, has_error=side_effect is not None): self.bot.api_client.post.reset_mock() data = await self.on_member_join_helper(side_effect) @@ -422,6 +422,6 @@ async def test_commands_require_admin(self): self.cog.sync_users_command, ) - for cmd in cmds: - with self.subTest(cmd=cmd): + for i, cmd in enumerate(cmds): + with self.subTest(test_case=i): await self.assertHasPermissionsCheck(cmd, {"administrator": True}) diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py index 85dc33999d..de12443dd1 100644 --- a/tests/bot/exts/backend/test_error_handler.py +++ b/tests/bot/exts/backend/test_error_handler.py @@ -135,8 +135,9 @@ async def test_error_handler_command_invoke_error(self): } ) - for case in test_cases: - with self.subTest(args=case["args"], expect_mock_call=case["expect_mock_call"]): + for i, case in enumerate(test_cases): + mock_type = "send" if case["expect_mock_call"] == "send" else "mock_function" + with self.subTest(test_case=i, expect_mock_call=mock_type): self.ctx.send.reset_mock() self.assertIsNone(await self.cog.on_command_error(*case["args"])) if case["expect_mock_call"] == "send": @@ -161,8 +162,8 @@ async def test_error_handler_conversion_error(self): } ) - for case in cases: - with self.subTest(**case): + for i, case in enumerate(cases): + with self.subTest(test_case=i): self.assertIsNone(await self.cog.on_command_error(self.ctx, case["error"])) case["mock_function_to_call"].assert_awaited_once_with(self.ctx, case["error"].original) @@ -173,8 +174,8 @@ async def test_error_handler_unexpected_errors(self): errors.ExtensionError(name="foo"), ) - for err in errs: - with self.subTest(error=err): + for i, err in enumerate(errs): + with self.subTest(test_case=i): self.cog.handle_unexpected_error.reset_mock() self.assertIsNone(await self.cog.on_command_error(self.ctx, err)) self.cog.handle_unexpected_error.assert_awaited_once_with(self.ctx, err) @@ -251,8 +252,8 @@ async def test_try_silence_silence_arguments(self): (MockTextChannel(), True) ) - for channel, kick in test_cases: - with self.subTest(kick=kick, channel=channel): + for i, (channel, kick) in enumerate(test_cases): + with self.subTest(test_case=i, kick=kick): self.ctx.reset_mock() self.ctx.invoked_with = "shh" @@ -291,8 +292,8 @@ async def test_try_silence_unsilence(self): ("unshh", MockTextChannel()) ) - for invoke, channel in test_cases: - with self.subTest(message=invoke, channel=channel): + for i, (invoke, channel) in enumerate(test_cases): + with self.subTest(test_case=i, message=invoke, has_channel=channel is not None): self.bot.get_command.side_effect = (self.silence.silence, self.silence.unsilence) self.ctx.reset_mock() @@ -386,33 +387,39 @@ async def test_handle_input_error_handler_errors(self): """Should handle each error probably.""" test_cases = ( { + "error_type": "MissingRequiredArgument", "error": errors.MissingRequiredArgument(MagicMock()), "call_prepared": True }, { + "error_type": "TooManyArguments", "error": errors.TooManyArguments(), "call_prepared": True }, { + "error_type": "BadArgument", "error": errors.BadArgument(), "call_prepared": True }, { + "error_type": "BadUnionArgument", "error": errors.BadUnionArgument(MagicMock(), MagicMock(), MagicMock()), "call_prepared": True }, { + "error_type": "ArgumentParsingError", "error": errors.ArgumentParsingError(), "call_prepared": False }, { + "error_type": "UserInputError", "error": errors.UserInputError(), "call_prepared": True } ) for case in test_cases: - with self.subTest(error=case["error"], call_prepared=case["call_prepared"]): + with self.subTest(error_type=case["error_type"], call_prepared=case["call_prepared"]): self.ctx.reset_mock() self.cog.send_error_with_help = AsyncMock() self.assertIsNone(await self.cog.handle_user_input_error(self.ctx, case["error"])) @@ -426,33 +433,39 @@ async def test_handle_check_failure_errors(self): """Should await `ctx.send` when error is check failure.""" test_cases = ( { + "error_type": "BotMissingPermissions", "error": errors.BotMissingPermissions(MagicMock()), "call_ctx_send": True }, { + "error_type": "BotMissingRole", "error": errors.BotMissingRole(MagicMock()), "call_ctx_send": True }, { + "error_type": "BotMissingAnyRole", "error": errors.BotMissingAnyRole(MagicMock()), "call_ctx_send": True }, { + "error_type": "NoPrivateMessage", "error": errors.NoPrivateMessage(), "call_ctx_send": True }, { + "error_type": "InWhitelistCheckFailure", "error": InWhitelistCheckFailure(1234), "call_ctx_send": True }, { + "error_type": "ResponseCodeError", "error": ResponseCodeError(MagicMock()), "call_ctx_send": False } ) for case in test_cases: - with self.subTest(error=case["error"], call_ctx_send=case["call_ctx_send"]): + with self.subTest(error_type=case["error_type"], call_ctx_send=case["call_ctx_send"]): self.ctx.reset_mock() await self.cog.handle_check_failure(self.ctx, case["error"]) if case["call_ctx_send"]: @@ -465,25 +478,29 @@ async def test_handle_api_error(self, log_mock): """Should `ctx.send` on HTTP error codes, and log at correct level.""" test_cases = ( { + "status": 400, "error": ResponseCodeError(AsyncMock(status=400)), "log_level": "error" }, { + "status": 404, "error": ResponseCodeError(AsyncMock(status=404)), "log_level": "debug" }, { + "status": 550, "error": ResponseCodeError(AsyncMock(status=550)), "log_level": "warning" }, { + "status": 1000, "error": ResponseCodeError(AsyncMock(status=1000)), "log_level": "warning" } ) for case in test_cases: - with self.subTest(error=case["error"], log_level=case["log_level"]): + with self.subTest(status=case["status"], log_level=case["log_level"]): self.ctx.reset_mock() log_mock.reset_mock() await self.cog.handle_api_error(self.ctx, case["error"]) @@ -500,7 +517,7 @@ async def test_handle_api_error(self, log_mock): async def test_handle_unexpected_error(self, log_mock, new_scope_mock): """Should `ctx.send` this error, error log this and sent to Sentry.""" for case in (None, MockGuild()): - with self.subTest(guild=case): + with self.subTest(has_guild=case is not None): self.ctx.reset_mock() log_mock.reset_mock() new_scope_mock.reset_mock() diff --git a/tests/bot/exts/filtering/test_settings_entries.py b/tests/bot/exts/filtering/test_settings_entries.py index f12b2caa55..1e9536c9e3 100644 --- a/tests/bot/exts/filtering/test_settings_entries.py +++ b/tests/bot/exts/filtering/test_settings_entries.py @@ -144,8 +144,8 @@ def test_filtering_dms_when_necessary(self): (False, MockTextChannel(), True) ) - for apply_in_dms, channel, expected in cases: - with self.subTest(apply_in_dms=apply_in_dms, channel=channel): + for i, (apply_in_dms, channel, expected) in enumerate(cases): + with self.subTest(test_case=i, apply_in_dms=apply_in_dms): filter_dms = FilterDM(filter_dm=apply_in_dms) self.ctx.channel = channel diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index 13a82f9b83..6f9799ce0d 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -122,22 +122,22 @@ async def test_user_command_helper_method_get_requests(self): }, ) - for test_value in test_values: + for i, test_value in enumerate(test_values): helper_method = test_value["helper_method"] endpoint, params = test_value["expected_args"] - with self.subTest(method=helper_method, endpoint=endpoint, params=params): + with self.subTest(test_case=i, endpoint=endpoint): await helper_method(self.member) self.bot.api_client.get.assert_called_once_with(endpoint, params=params) self.bot.api_client.get.reset_mock() async def _method_subtests(self, method, test_values, default_header): """Helper method that runs the subtests for the different helper methods.""" - for test_value in test_values: + for i, test_value in enumerate(test_values): api_response = test_value["api response"] expected_lines = test_value["expected_lines"] - with self.subTest(method=method, api_response=api_response, expected_lines=expected_lines): + with self.subTest(test_case=i): self.bot.api_client.get.return_value = api_response expected_output = "\n".join(expected_lines) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index e2a7bad9f3..c18a937abc 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -66,13 +66,13 @@ async def test_post_user(self): } ] - for case in test_cases: + for i, case in enumerate(test_cases): user = case["user"] post_result = case["post_result"] raise_error = case["raise_error"] payload = case["payload"] - with self.subTest(user=user, post_result=post_result, raise_error=raise_error, payload=payload): + with self.subTest(test_case=i, has_error=raise_error is not None): self.bot.api_client.post.reset_mock(side_effect=True) self.ctx.bot.api_client.post.return_value = post_result @@ -235,8 +235,8 @@ async def test_send_infraction_embed(self, send_private_embed_mock): } ] - for case in test_cases: - with self.subTest(args=case["args"], expected=case["expected_output"], send=case["send_result"]): + for i, case in enumerate(test_cases): + with self.subTest(test_case=i, send=case["send_result"]): send_private_embed_mock.reset_mock() send_private_embed_mock.return_value = case["send_result"] @@ -259,7 +259,7 @@ async def test_notify_pardon(self, send_private_embed_mock): test_case((self.user, "Test title", "Example content", Icons.user_update), Icons.user_update, False) ] - for case in test_cases: + for i, case in enumerate(test_cases): expected = Embed( description="Example content", colour=Colours.soft_green @@ -268,7 +268,7 @@ async def test_notify_pardon(self, send_private_embed_mock): icon_url=case.icon ) - with self.subTest(args=case.args, expected=expected): + with self.subTest(test_case=i): send_private_embed_mock.reset_mock() send_private_embed_mock.return_value = case.send_result @@ -288,13 +288,13 @@ async def test_send_private_embed(self): test_case = namedtuple("test_case", ["expected_output", "raised_exception"]) test_cases = [ test_case(True, None), - test_case(False, HTTPException(AsyncMock(), AsyncMock())), - test_case(False, Forbidden(AsyncMock(), AsyncMock())), - test_case(False, NotFound(AsyncMock(), AsyncMock())) + test_case(False, HTTPException(AsyncMock(), "test error")), + test_case(False, Forbidden(AsyncMock(), "test error")), + test_case(False, NotFound(AsyncMock(), "test error")) ] - for case in test_cases: - with self.subTest(expected=case.expected_output, raised=case.raised_exception): + for i, case in enumerate(test_cases): + with self.subTest(test_case=i, expected=case.expected_output): self.user.send.reset_mock(side_effect=True) self.user.send.side_effect = case.raised_exception diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 86d396afda..019a0634c2 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -260,8 +260,8 @@ async def test_command(self, parser_mock): ctx = MockContext() parser_mock.return_value = (ctx.channel, 10) - for case in test_cases: - with self.subTest("Test command converters", args=case): + for i, case in enumerate(test_cases): + with self.subTest(msg="Test command converters", test_case=i): await cog.silence.callback(cog, ctx, *case) try: @@ -433,10 +433,10 @@ async def test_sent_correct_message(self): targets = (MockTextChannel(), MockVoiceChannel(), None) - for (duration, message, was_silenced), target in itertools.product(test_cases, targets): + for i, ((duration, message, was_silenced), target) in enumerate(itertools.product(test_cases, targets)): with ( mock.patch.object(self.cog, "_set_silence_overwrites", return_value=was_silenced), - self.subTest(was_silenced=was_silenced, target=target, message=message), + self.subTest(test_case=i, was_silenced=was_silenced, has_target=target is not None), mock.patch.object(self.cog, "send_message") as send_message ): ctx = MockContext() @@ -525,8 +525,8 @@ async def test_skipped_already_silenced(self): (True, MockVoiceChannel(), PermissionOverwrite(connect=False, speak=False)), ) - for contains, channel, overwrite in subtests: - with self.subTest(contains=contains, is_text=isinstance(channel, MockTextChannel), overwrite=overwrite): + for i, (contains, channel, overwrite) in enumerate(subtests): + with self.subTest(test_case=i, contains=contains, is_text=isinstance(channel, MockTextChannel)): self.cog.scheduler.__contains__.return_value = contains channel.overwrites_for.return_value = overwrite @@ -702,7 +702,7 @@ async def test_sent_correct_message(self): targets = (None, MockTextChannel()) - for (was_unsilenced, message, overwrite), target in itertools.product(test_cases, targets): + for i, ((was_unsilenced, message, overwrite), target) in enumerate(itertools.product(test_cases, targets)): ctx = MockContext() ctx.channel.overwrites_for.return_value = overwrite if target: @@ -711,7 +711,7 @@ async def test_sent_correct_message(self): with ( mock.patch.object(self.cog, "_unsilence", return_value=was_unsilenced), mock.patch.object(self.cog, "send_message") as send_message, - self.subTest(was_unsilenced=was_unsilenced, overwrite=overwrite, target=target), + self.subTest(test_case=i, was_unsilenced=was_unsilenced, has_target=target is not None), ): await self.cog.unsilence.callback(self.cog, ctx, channel=target) @@ -722,8 +722,8 @@ async def test_skipped_already_unsilenced(self): """Permissions were not set and `False` was returned for an already unsilenced channel.""" self.cog.scheduler.__contains__.return_value = False - for channel in (MockVoiceChannel(), MockTextChannel()): - with self.subTest(channel=channel): + for i, channel in enumerate((MockVoiceChannel(), MockTextChannel())): + with self.subTest(test_case=i): self.assertFalse(await self.cog._unsilence(channel)) channel.set_permissions.assert_not_called() @@ -811,8 +811,8 @@ async def test_cancelled_task(self): async def test_preserved_other_overwrites_text(self): """Text channel's other unrelated overwrites were not changed, including cache misses.""" - for overwrite_json in ('{"send_messages": true, "add_reactions": null}', None): - with self.subTest(overwrite_json=overwrite_json): + for i, overwrite_json in enumerate(('{"send_messages": true, "add_reactions": null}', None)): + with self.subTest(test_case=i, has_overwrite=overwrite_json is not None): if overwrite_json is None: await self.cog.previous_overwrites.delete(self.text_channel.id) else: @@ -832,8 +832,8 @@ async def test_preserved_other_overwrites_text(self): async def test_preserved_other_overwrites_voice(self): """Voice channel's other unrelated overwrites were not changed, including cache misses.""" - for overwrite_json in ('{"connect": true, "speak": true}', None): - with self.subTest(overwrite_json=overwrite_json): + for i, overwrite_json in enumerate(('{"connect": true, "speak": true}', None)): + with self.subTest(test_case=i, has_overwrite=overwrite_json is not None): if overwrite_json is None: await self.cog.previous_overwrites.delete(self.voice_channel.id) else: @@ -858,8 +858,8 @@ async def test_unsilence_role(self): (MockVoiceChannel(), self.cog.bot.get_guild(Guild.id).get_role(Roles.voice_verified)) ) - for channel, role in test_cases: - with self.subTest(channel=channel, role=role): + for i, (channel, role) in enumerate(test_cases): + with self.subTest(test_case=i): await self.cog._unsilence_wrapper(channel, MockContext()) channel.overwrites_for.assert_called_with(role) diff --git a/tests/bot/exts/recruitment/talentpool/test_review.py b/tests/bot/exts/recruitment/talentpool/test_review.py index 8ec384bb28..195fb71c12 100644 --- a/tests/bot/exts/recruitment/talentpool/test_review.py +++ b/tests/bot/exts/recruitment/talentpool/test_review.py @@ -141,8 +141,8 @@ async def test_is_ready_for_review(self): ([], None, True), ) - for messages, last_review_timestamp, expected in cases: - with self.subTest(messages=messages, expected=expected): + for i, (messages, last_review_timestamp, expected) in enumerate(cases): + with self.subTest(test_case=i, expected=expected): self.voting_channel.history = AsyncIterator(messages) cache_get_mock = AsyncMock(return_value=last_review_timestamp) diff --git a/tests/bot/test_converters.py b/tests/bot/test_converters.py index e5ccf27f78..ffe9b91250 100644 --- a/tests/bot/test_converters.py +++ b/tests/bot/test_converters.py @@ -191,8 +191,8 @@ async def test_isodatetime_converter_for_valid(self): converter = ISODateTime() - for datetime_string, expected_dt in test_values: - with self.subTest(datetime_string=datetime_string, expected_dt=expected_dt): + for i, (datetime_string, expected_dt) in enumerate(test_values): + with self.subTest(test_case=i, datetime_string=datetime_string): converted_dt = await converter.convert(self.context, datetime_string) self.assertEqual(converted_dt, expected_dt) diff --git a/tests/bot/utils/test_message_cache.py b/tests/bot/utils/test_message_cache.py index ad3f4e8b66..da6dad8596 100644 --- a/tests/bot/utils/test_message_cache.py +++ b/tests/bot/utils/test_message_cache.py @@ -177,8 +177,8 @@ def test_slicing_with_unfilled_cache(self): for msg in messages: cache.append(msg) - for slice_ in slices: - with self.subTest(current_loop=(size, slice_)): + for i, slice_ in enumerate(slices): + with self.subTest(size=size, slice_index=i): self.assertListEqual(cache[slice_], messages[slice_]) def test_slicing_with_overfilled_cache(self): @@ -199,8 +199,8 @@ def test_slicing_with_overfilled_cache(self): cache.append(msg) messages = messages[size // 2:] - for slice_ in slices: - with self.subTest(current_loop=(size, slice_)): + for i, slice_ in enumerate(slices): + with self.subTest(size=size, slice_index=i): self.assertListEqual(cache[slice_], messages[slice_]) def test_length(self): diff --git a/tests/bot/utils/test_time.py b/tests/bot/utils/test_time.py index 6244a35484..d6f7b923ee 100644 --- a/tests/bot/utils/test_time.py +++ b/tests/bot/utils/test_time.py @@ -32,8 +32,8 @@ def test_humanize_delta_should_normal_usage(self): (relativedelta(days=2, hours=2), "days", 2, "2 days"), ) - for delta, precision, max_units, expected in test_cases: - with self.subTest(delta=delta, precision=precision, max_units=max_units, expected=expected): + for i, (delta, precision, max_units, expected) in enumerate(test_cases): + with self.subTest(test_case=i, precision=precision, max_units=max_units, expected=expected): actual = time.humanize_delta(delta, precision=precision, max_units=max_units) self.assertEqual(actual, expected) @@ -57,8 +57,8 @@ def test_format_with_duration_none_expiry(self): (None, "Why hello there!", float("inf"), None), ) - for expiry, date_from, max_units, expected in test_cases: - with self.subTest(expiry=expiry, date_from=date_from, max_units=max_units, expected=expected): + for i, (expiry, date_from, max_units, expected) in enumerate(test_cases): + with self.subTest(test_case=i, expiry=expiry, max_units=max_units, expected=expected): self.assertEqual(time.format_with_duration(expiry, date_from, max_units), expected) def test_format_with_duration_custom_units(self): @@ -70,8 +70,8 @@ def test_format_with_duration_custom_units(self): " (6 months, 28 days, 23 hours and 54 minutes)") ) - for expiry, date_from, max_units, expected in test_cases: - with self.subTest(expiry=expiry, date_from=date_from, max_units=max_units, expected=expected): + for i, (expiry, date_from, max_units, expected) in enumerate(test_cases): + with self.subTest(test_case=i, max_units=max_units, expected=expected): self.assertEqual(time.format_with_duration(expiry, date_from, max_units), expected) def test_format_with_duration_normal_usage(self): @@ -94,8 +94,8 @@ def test_format_with_duration_normal_usage(self): (None, datetime(2019, 11, 23, 23, 49, 5, tzinfo=UTC), 2, None), ) - for expiry, date_from, max_units, expected in test_cases: - with self.subTest(expiry=expiry, date_from=date_from, max_units=max_units, expected=expected): + for i, (expiry, date_from, max_units, expected) in enumerate(test_cases): + with self.subTest(test_case=i, max_units=max_units, expected=expected): self.assertEqual(time.format_with_duration(expiry, date_from, max_units), expected) def test_until_expiration_with_duration_none_expiry(self): @@ -109,8 +109,8 @@ def test_until_expiration_with_duration_custom_units(self): ("3000-11-23T20:09:00Z", "") ) - for expiry, expected in test_cases: - with self.subTest(expiry=expiry, expected=expected): + for i, (expiry, expected) in enumerate(test_cases): + with self.subTest(test_case=i, expiry=expiry, expected=expected): self.assertEqual(time.until_expiration(expiry,), expected) def test_until_expiration_normal_usage(self): @@ -123,6 +123,6 @@ def test_until_expiration_normal_usage(self): ("3000-11-23T20:09:00Z", ""), ) - for expiry, expected in test_cases: - with self.subTest(expiry=expiry, expected=expected): + for i, (expiry, expected) in enumerate(test_cases): + with self.subTest(test_case=i, expiry=expiry, expected=expected): self.assertEqual(time.until_expiration(expiry), expected) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 3d4cb09e77..75351e5e42 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -153,8 +153,8 @@ def test_mocks_allows_access_to_attributes_part_of_spec(self): (helpers.MockMessage(), "mention_everyone"), ) - for mock, valid_attribute in mocks: - with self.subTest(mock=mock): + for i, (mock, valid_attribute) in enumerate(mocks): + with self.subTest(test_case=i, attribute=valid_attribute): try: getattr(mock, valid_attribute) except AttributeError: @@ -183,8 +183,8 @@ def test_mocks_rejects_access_to_attributes_not_part_of_spec(self): helpers.MockMessage(), ) - for mock in mocks: - with self.subTest(mock=mock), self.assertRaises(AttributeError): + for i, mock in enumerate(mocks): + with self.subTest(test_case=i), self.assertRaises(AttributeError): mock.the_cake_is_a_lie # noqa: B018 def test_mocks_use_mention_when_provided_as_kwarg(self): @@ -195,8 +195,8 @@ def test_mocks_use_mention_when_provided_as_kwarg(self): (helpers.MockTextChannel, "channel mention"), ) - for mock_type, mention in test_cases: - with self.subTest(mock_type=mock_type, mention=mention): + for i, (mock_type, mention) in enumerate(test_cases): + with self.subTest(test_case=i, mention=mention): mock = mock_type(mention=mention) self.assertEqual(mock.mention, mention) @@ -276,15 +276,15 @@ class MockScragly(helpers.HashableMixin): def test_mock_class_with_hashable_mixin_uses_id_for_hashing(self): """Test if the MagicMock subclasses that implement the HashableMixin use id for hash.""" - for mock in self.hashable_mocks: - with self.subTest(mock_class=mock): + for i, _mock in enumerate(self.hashable_mocks): + with self.subTest(test_case=i): instance = helpers.MockRole(id=100) self.assertEqual(hash(instance), instance.id) def test_mock_class_with_hashable_mixin_uses_id_for_equality(self): """Test if MagicMocks that implement the HashableMixin use id for equality comparisons.""" - for mock_class in self.hashable_mocks: - with self.subTest(mock_class=mock_class): + for i, mock_class in enumerate(self.hashable_mocks): + with self.subTest(test_case=i): instance_one = mock_class() instance_two = mock_class() instance_three = mock_class() @@ -298,8 +298,8 @@ def test_mock_class_with_hashable_mixin_uses_id_for_equality(self): def test_mock_class_with_hashable_mixin_uses_id_for_nonequality(self): """Test if MagicMocks that implement HashableMixin use id for nonequality comparisons.""" - for mock_class in self.hashable_mocks: - with self.subTest(mock_class=mock_class): + for i, mock_class in enumerate(self.hashable_mocks): + with self.subTest(test_case=i): instance_one = mock_class() instance_two = mock_class() instance_three = mock_class() @@ -336,8 +336,8 @@ def test_spec_propagation_of_mock_subclasses(self): (helpers.MockReaction, "me"), ) - for mock_type, valid_attribute in test_values: - with self.subTest(mock_type=mock_type, attribute=valid_attribute): + for i, (mock_type, valid_attribute) in enumerate(test_values): + with self.subTest(test_case=i, attribute=valid_attribute): mock = mock_type() self.assertTrue(isinstance(mock, mock_type)) attribute = getattr(mock, valid_attribute) diff --git a/uv.lock b/uv.lock index 3d890e0f3d..56e4d236d6 100644 --- a/uv.lock +++ b/uv.lock @@ -231,7 +231,6 @@ dev = [ { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-cov" }, - { name = "pytest-subtests" }, { name = "pytest-xdist" }, { name = "ruff" }, { name = "taskipy" }, @@ -265,9 +264,8 @@ dev = [ { name = "coverage", specifier = "==7.11.0" }, { name = "httpx", specifier = "==0.28.1" }, { name = "pre-commit", specifier = "==4.3.0" }, - { name = "pytest", specifier = "==8.4.2" }, + { name = "pytest", specifier = "==9.0.0" }, { name = "pytest-cov", specifier = "==7.0.0" }, - { name = "pytest-subtests", specifier = "==0.14.1" }, { name = "pytest-xdist", specifier = "==3.8.0" }, { name = "ruff", specifier = "==0.14.2" }, { name = "taskipy", specifier = "==1.14.1" }, @@ -982,7 +980,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.2" +version = "9.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -991,9 +989,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/1d/eb34f286b164c5e431a810a38697409cca1112cee04b287bb56ac486730b/pytest-9.0.0.tar.gz", hash = "sha256:8f44522eafe4137b0f35c9ce3072931a788a21ee40a2ed279e817d3cc16ed21e", size = 1562764, upload-time = "2025-11-08T17:25:33.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, + { url = "https://files.pythonhosted.org/packages/72/99/cafef234114a3b6d9f3aaed0723b437c40c57bdb7b3e4c3a575bc4890052/pytest-9.0.0-py3-none-any.whl", hash = "sha256:e5ccdf10b0bac554970ee88fc1a4ad0ee5d221f8ef22321f9b7e4584e19d7f96", size = 373364, upload-time = "2025-11-08T17:25:31.811Z" }, ] [[package]] @@ -1010,19 +1008,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] -[[package]] -name = "pytest-subtests" -version = "0.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/4c/ba9eab21a2250c2d46c06c0e3cd316850fde9a90da0ac8d0202f074c6817/pytest_subtests-0.14.1.tar.gz", hash = "sha256:350c00adc36c3aff676a66135c81aed9e2182e15f6c3ec8721366918bbbf7580", size = 17632, upload-time = "2024-12-10T00:21:04.856Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/b7/7ca948d35642ae72500efda6ba6fa61dcb6683feb596d19c4747c63c0789/pytest_subtests-0.14.1-py3-none-any.whl", hash = "sha256:e92a780d98b43118c28a16044ad9b841727bd7cb6a417073b38fd2d7ccdf052d", size = 8833, upload-time = "2024-12-10T00:20:58.873Z" }, -] - [[package]] name = "pytest-xdist" version = "3.8.0"