From 4f4caf4e427490c43e2a78185afbbb58b438cb2a Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 31 May 2025 21:50:57 +0000 Subject: [PATCH 01/22] Implement minor config fixes --- config.py | 192 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 111 insertions(+), 81 deletions(-) diff --git a/config.py b/config.py index 73812917e..71d438a92 100644 --- a/config.py +++ b/config.py @@ -47,7 +47,7 @@ TRUE_VALUES: "Final[frozenset[str]]" = frozenset({"true", "1", "t", "y", "yes", "on"}) FALSE_VALUES: "Final[frozenset[str]]" = frozenset({"false", "0", "f", "n", "no", "off"}) VALID_SEND_INTRODUCTION_REMINDERS_VALUES: "Final[frozenset[str]]" = frozenset( - {"once"} | TRUE_VALUES | FALSE_VALUES, + {"once"} | TRUE_VALUES | FALSE_VALUES | {"interval"}, ) DEFAULT_STATISTICS_ROLES: "Final[frozenset[str]]" = frozenset( { @@ -136,7 +136,9 @@ def __getitem__(self, item: str) -> "Any": # type: ignore[explicit-any] # noqa @staticmethod def _setup_logging() -> None: - raw_console_log_level: str = str(os.getenv("CONSOLE_LOG_LEVEL", "INFO")).upper() + raw_console_log_level: str = ( + str(os.getenv("CONSOLE_LOG_LEVEL", "INFO")).upper().strip() + ) if raw_console_log_level not in LOG_LEVEL_CHOICES: INVALID_LOG_LEVEL_MESSAGE: Final[str] = f"""LOG_LEVEL must be one of { @@ -158,15 +160,15 @@ def _setup_logging() -> None: @classmethod def _setup_discord_bot_token(cls) -> None: - raw_discord_bot_token: str | None = os.getenv("DISCORD_BOT_TOKEN") + raw_discord_bot_token: str = os.getenv("DISCORD_BOT_TOKEN", default="").strip() DISCORD_BOT_TOKEN_IS_VALID: Final[bool] = bool( - raw_discord_bot_token - and re.fullmatch( + re.fullmatch( r"\A([A-Za-z0-9_-]{24,26})\.([A-Za-z0-9_-]{6})\.([A-Za-z0-9_-]{27,38})\Z", raw_discord_bot_token, ), ) + if not DISCORD_BOT_TOKEN_IS_VALID: INVALID_DISCORD_BOT_TOKEN_MESSAGE: Final[str] = ( "DISCORD_BOT_TOKEN must be a valid Discord bot token " # noqa: S105 @@ -180,13 +182,12 @@ def _setup_discord_bot_token(cls) -> None: def _setup_discord_log_channel_webhook(cls) -> "Logger": raw_discord_log_channel_webhook_url: str = os.getenv( "DISCORD_LOG_CHANNEL_WEBHOOK_URL", "" - ) + ).strip() - if raw_discord_log_channel_webhook_url and ( - not validators.url(raw_discord_log_channel_webhook_url) - or not raw_discord_log_channel_webhook_url.startswith( - "https://discord.com/api/webhooks/" - ) + if not validators.url( + raw_discord_log_channel_webhook_url + ) or not raw_discord_log_channel_webhook_url.startswith( + "https://discord.com/api/webhooks/" ): INVALID_DISCORD_LOG_CHANNEL_WEBHOOK_URL_MESSAGE: Final[str] = ( "DISCORD_LOG_CHANNEL_WEBHOOK_URL must be a valid webhook URL " @@ -215,11 +216,12 @@ def _setup_discord_log_channel_webhook(cls) -> "Logger": @classmethod def _setup_discord_guild_id(cls) -> None: - raw_discord_guild_id: str | None = os.getenv("DISCORD_GUILD_ID") + raw_discord_guild_id: str = os.getenv("DISCORD_GUILD_ID", default="").strip() DISCORD_GUILD_ID_IS_VALID: Final[bool] = bool( - raw_discord_guild_id and re.fullmatch(r"\A\d{17,20}\Z", raw_discord_guild_id), + re.fullmatch(r"\A\d{17,20}\Z", raw_discord_guild_id), ) + if not DISCORD_GUILD_ID_IS_VALID: INVALID_DISCORD_GUILD_ID_MESSAGE: Final[str] = ( "DISCORD_GUILD_ID must be a valid Discord guild ID " @@ -227,14 +229,11 @@ def _setup_discord_guild_id(cls) -> None: ) raise ImproperlyConfiguredError(INVALID_DISCORD_GUILD_ID_MESSAGE) - cls._settings["_DISCORD_MAIN_GUILD_ID"] = int(raw_discord_guild_id) # type: ignore[arg-type] + cls._settings["_DISCORD_MAIN_GUILD_ID"] = int(raw_discord_guild_id) @classmethod def _setup_group_full_name(cls) -> None: - raw_group_full_name: str | None = os.getenv("GROUP_NAME") - - if raw_group_full_name is not None: - raw_group_full_name = raw_group_full_name.strip() + raw_group_full_name: str = os.getenv("GROUP_NAME", default="").strip() if not raw_group_full_name: cls._settings["_GROUP_FULL_NAME"] = None @@ -250,10 +249,7 @@ def _setup_group_full_name(cls) -> None: @classmethod def _setup_group_short_name(cls) -> None: - raw_group_short_name: str | None = os.getenv("GROUP_SHORT_NAME") - - if raw_group_short_name is not None: - raw_group_short_name = raw_group_short_name.strip() + raw_group_short_name: str = os.getenv("GROUP_SHORT_NAME", default="").strip() if not raw_group_short_name: cls._settings["_GROUP_SHORT_NAME"] = None @@ -269,15 +265,17 @@ def _setup_group_short_name(cls) -> None: @classmethod def _setup_purchase_membership_url(cls) -> None: - raw_purchase_membership_url: str | None = os.getenv("PURCHASE_MEMBERSHIP_URL") - - if raw_purchase_membership_url is not None: - raw_purchase_membership_url = raw_purchase_membership_url.strip() + raw_purchase_membership_url: str = os.getenv( + "PURCHASE_MEMBERSHIP_URL", default="" + ).strip() if not raw_purchase_membership_url: cls._settings["PURCHASE_MEMBERSHIP_URL"] = None return + if "://" not in raw_purchase_membership_url: + raw_purchase_membership_url = "https://" + raw_purchase_membership_url + if not validators.url(raw_purchase_membership_url): INVALID_PURCHASE_MEMBERSHIP_URL_MESSAGE: Final[str] = ( "PURCHASE_MEMBERSHIP_URL must be a valid URL." @@ -288,15 +286,15 @@ def _setup_purchase_membership_url(cls) -> None: @classmethod def _setup_membership_perks_url(cls) -> None: - raw_membership_perks_url: str | None = os.getenv("MEMBERSHIP_PERKS_URL") - - if raw_membership_perks_url is not None: - raw_membership_perks_url = raw_membership_perks_url.strip() + raw_membership_perks_url: str = os.getenv("MEMBERSHIP_PERKS_URL", default="").strip() if not raw_membership_perks_url: cls._settings["MEMBERSHIP_PERKS_URL"] = None return + if "://" not in raw_membership_perks_url: + raw_membership_perks_url = "https://" + raw_membership_perks_url + if not validators.url(raw_membership_perks_url): INVALID_MEMBERSHIP_PERKS_URL_MESSAGE: Final[str] = ( "MEMBERSHIP_PERKS_URL must be a valid URL." @@ -307,15 +305,17 @@ def _setup_membership_perks_url(cls) -> None: @classmethod def _setup_custom_discord_invite_url(cls) -> None: - raw_custom_discord_invite_url: str | None = os.getenv("CUSTOM_DISCORD_INVITE_URL") - - if raw_custom_discord_invite_url is not None: - raw_custom_discord_invite_url = raw_custom_discord_invite_url.strip() + raw_custom_discord_invite_url: str = os.getenv( + "CUSTOM_DISCORD_INVITE_URL", default="" + ).strip() if not raw_custom_discord_invite_url: cls._settings["CUSTOM_DISCORD_INVITE_URL"] = None return + if "://" not in raw_custom_discord_invite_url: + raw_custom_discord_invite_url = "https://" + raw_custom_discord_invite_url + if not validators.url(raw_custom_discord_invite_url): INVALID_CUSTOM_DISCORD_INVITE_URL_MESSAGE: Final[str] = ( "CUSTOM_DISCORD_INVITE_URL must be a valid URL." @@ -326,14 +326,10 @@ def _setup_custom_discord_invite_url(cls) -> None: @classmethod def _setup_ping_command_easter_egg_probability(cls) -> None: - raw_ping_command_easter_egg_probability_string: str | None = os.getenv( - "PING_COMMAND_EASTER_EGG_PROBABILITY" - ) - - if raw_ping_command_easter_egg_probability_string is not None: - raw_ping_command_easter_egg_probability_string = ( - raw_ping_command_easter_egg_probability_string.strip() - ) + raw_ping_command_easter_egg_probability_string: str = os.getenv( + "PING_COMMAND_EASTER_EGG_PROBABILITY", + default="", + ).strip() if not raw_ping_command_easter_egg_probability_string: cls._settings["PING_COMMAND_EASTER_EGG_PROBABILITY"] = 1 @@ -371,7 +367,7 @@ def _get_messages_dict(cls, raw_messages_file_path: str | None) -> Mapping[str, ) messages_file_path: Path = ( - Path(raw_messages_file_path) + Path(raw_messages_file_path.strip()) if raw_messages_file_path else PROJECT_ROOT / Path("messages.json") ) @@ -438,10 +434,10 @@ def _setup_roles_messages(cls) -> None: @classmethod def _setup_organisation_id(cls) -> None: - raw_organisation_id: str | None = os.getenv("ORGANISATION_ID") + raw_organisation_id: str = os.getenv("ORGANISATION_ID", default="").strip() ORGANISATION_ID_IS_VALID: Final[bool] = bool( - raw_organisation_id and re.fullmatch(r"\A\d{4,5}\Z", raw_organisation_id), + re.fullmatch(r"\A\d{4,5}\Z", raw_organisation_id), ) if not ORGANISATION_ID_IS_VALID: @@ -454,14 +450,15 @@ def _setup_organisation_id(cls) -> None: @classmethod def _setup_members_list_auth_session_cookie(cls) -> None: - raw_members_list_auth_session_cookie: str | None = os.getenv( + raw_members_list_auth_session_cookie: str = os.getenv( "MEMBERS_LIST_URL_SESSION_COOKIE", - ) + default="", + ).strip() MEMBERS_LIST_AUTH_SESSION_COOKIE_IS_VALID: Final[bool] = bool( - raw_members_list_auth_session_cookie - and re.fullmatch(r"\A[A-Fa-f\d]{128,256}\Z", raw_members_list_auth_session_cookie), + re.fullmatch(r"\A[A-Fa-f\d]{128,256}\Z", raw_members_list_auth_session_cookie), ) + if not MEMBERS_LIST_AUTH_SESSION_COOKIE_IS_VALID: INVALID_MEMBERS_LIST_AUTH_SESSION_COOKIE_MESSAGE: Final[str] = ( "MEMBERS_LIST_URL_SESSION_COOKIE must be a valid .ASPXAUTH cookie." @@ -474,9 +471,9 @@ def _setup_members_list_auth_session_cookie(cls) -> None: @classmethod def _setup_send_introduction_reminders(cls) -> None: - raw_send_introduction_reminders: str | bool = str( - os.getenv("SEND_INTRODUCTION_REMINDERS", "Once"), - ).lower() + raw_send_introduction_reminders: str | bool = ( + str(os.getenv("SEND_INTRODUCTION_REMINDERS", "Once")).lower().strip() + ) if raw_send_introduction_reminders not in VALID_SEND_INTRODUCTION_REMINDERS_VALUES: INVALID_SEND_INTRODUCTION_REMINDERS_MESSAGE: Final[str] = ( @@ -503,7 +500,9 @@ def _setup_send_introduction_reminders_delay(cls) -> None: raw_send_introduction_reminders_delay: re.Match[str] | None = re.fullmatch( r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", - str(os.getenv("SEND_INTRODUCTION_REMINDERS_DELAY", "40h")), + str( + os.getenv("SEND_INTRODUCTION_REMINDERS_DELAY", "40h").strip().replace(" ", "") + ), ) raw_timedelta_send_introduction_reminders_delay: timedelta = timedelta() @@ -528,8 +527,7 @@ def _setup_send_introduction_reminders_delay(cls) -> None: if raw_timedelta_send_introduction_reminders_delay < timedelta(days=1): TOO_SMALL_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE: Final[str] = ( - "SEND_INTRODUCTION_REMINDERS_DELAY must be longer than or equal to 1 day " - "(in any allowed format)." + "SEND_INTRODUCTION_REMINDERS_DELAY must be longer than or equal to 1 day." ) raise ImproperlyConfiguredError( TOO_SMALL_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE, @@ -550,7 +548,11 @@ def _setup_send_introduction_reminders_interval(cls) -> None: raw_send_introduction_reminders_interval: re.Match[str] | None = re.fullmatch( r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?\Z", - str(os.getenv("SEND_INTRODUCTION_REMINDERS_INTERVAL", "6h")), + str( + os.getenv("SEND_INTRODUCTION_REMINDERS_INTERVAL", "6h") + .strip() + .replace(" ", "") + ), ) raw_timedelta_details_send_introduction_reminders_interval: Mapping[str, float] = { @@ -573,15 +575,28 @@ def _setup_send_introduction_reminders_interval(cls) -> None: if value } + if ( + timedelta( + **raw_timedelta_details_send_introduction_reminders_interval + ).total_seconds() + <= 3 + ): + TOO_SMALL_SEND_INTRODUCTION_REMINDERS_INTERVAL_MESSAGE: Final[str] = ( + "SEND_INTRODUCTION_REMINDERS_INTERVAL must be longer than 3 seconds." + ) + raise ImproperlyConfiguredError( + TOO_SMALL_SEND_INTRODUCTION_REMINDERS_INTERVAL_MESSAGE, + ) + cls._settings["SEND_INTRODUCTION_REMINDERS_INTERVAL"] = ( raw_timedelta_details_send_introduction_reminders_interval ) @classmethod def _setup_send_get_roles_reminders(cls) -> None: - raw_send_get_roles_reminders: str = str( - os.getenv("SEND_GET_ROLES_REMINDERS", "True"), - ).lower() + raw_send_get_roles_reminders: str = ( + str(os.getenv("SEND_GET_ROLES_REMINDERS", "True")).lower().strip() + ) if raw_send_get_roles_reminders not in TRUE_VALUES | FALSE_VALUES: INVALID_SEND_GET_ROLES_REMINDERS_MESSAGE: Final[str] = ( @@ -602,7 +617,7 @@ def _setup_send_get_roles_reminders_delay(cls) -> None: raw_send_get_roles_reminders_delay: re.Match[str] | None = re.fullmatch( r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", - str(os.getenv("SEND_GET_ROLES_REMINDERS_DELAY", "40h")), + str(os.getenv("SEND_GET_ROLES_REMINDERS_DELAY", "40h").strip().replace(" ", "")), ) raw_timedelta_send_get_roles_reminders_delay: timedelta = timedelta() @@ -627,8 +642,8 @@ def _setup_send_get_roles_reminders_delay(cls) -> None: if raw_timedelta_send_get_roles_reminders_delay < timedelta(days=1): TOO_SMALL_SEND_GET_ROLES_REMINDERS_DELAY_MESSAGE: Final[str] = ( - "SEND_SEND_GET_ROLES_REMINDERS_DELAY " - "must be longer than or equal to 1 day (in any allowed format)." + "SEND_SEND_GET_ROLES_REMINDERS_DELAY must be " + "longer than or equal to 1 day." ) raise ImproperlyConfiguredError( TOO_SMALL_SEND_GET_ROLES_REMINDERS_DELAY_MESSAGE, @@ -649,14 +664,16 @@ def _setup_advanced_send_get_roles_reminders_interval(cls) -> None: raw_advanced_send_get_roles_reminders_interval: re.Match[str] | None = re.fullmatch( r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?\Z", - str(os.getenv("ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL", "24h")), + str( + os.getenv("ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL", "24h") + .strip() + .replace(" ", "") + ), ) raw_timedelta_details_advanced_send_get_roles_reminders_interval: Mapping[ str, float - ] = { - "hours": 24, - } + ] = {"hours": 24} if cls._settings["SEND_GET_ROLES_REMINDERS"]: if not raw_advanced_send_get_roles_reminders_interval: @@ -684,36 +701,48 @@ def _setup_advanced_send_get_roles_reminders_interval(cls) -> None: def _setup_statistics_days(cls) -> None: e: ValueError try: - raw_statistics_days: float = float(os.getenv("STATISTICS_DAYS", "30")) + raw_statistics_days: float = float(os.getenv("STATISTICS_DAYS", "30").strip()) except ValueError as e: INVALID_STATISTICS_DAYS_MESSAGE: Final[str] = ( "STATISTICS_DAYS must contain the statistics period in days." ) raise ImproperlyConfiguredError(INVALID_STATISTICS_DAYS_MESSAGE) from e + if raw_statistics_days < 1: + TOO_SMALL_STATISTICS_DAYS_MESSAGE: Final[str] = ( + "STATISTICS_DAYS cannot be less than or equal to 1 day" + ) + raise ImproperlyConfiguredError(TOO_SMALL_STATISTICS_DAYS_MESSAGE) + cls._settings["STATISTICS_DAYS"] = timedelta(days=raw_statistics_days) @classmethod def _setup_statistics_roles(cls) -> None: - raw_statistics_roles: str | None = os.getenv("STATISTICS_ROLES") + raw_statistics_roles: str = os.getenv("STATISTICS_ROLES", default="").strip() if not raw_statistics_roles: cls._settings["STATISTICS_ROLES"] = DEFAULT_STATISTICS_ROLES + return - else: - cls._settings["STATISTICS_ROLES"] = { - raw_statistics_role - for raw_statistics_role in raw_statistics_roles.split(",") - if raw_statistics_role - } + cls._settings["STATISTICS_ROLES"] = { + raw_statistics_role.strip() + for raw_statistics_role in raw_statistics_roles.split(",") + if raw_statistics_role + } @classmethod def _setup_moderation_document_url(cls) -> None: - raw_moderation_document_url: str | None = os.getenv("MODERATION_DOCUMENT_URL") + raw_moderation_document_url: str = os.getenv( + "MODERATION_DOCUMENT_URL", default="" + ).strip() + + if raw_moderation_document_url and "://" not in raw_moderation_document_url: + raw_moderation_document_url = "https://" + raw_moderation_document_url MODERATION_DOCUMENT_URL_IS_VALID: Final[bool] = bool( - raw_moderation_document_url and validators.url(raw_moderation_document_url), + validators.url(raw_moderation_document_url), ) + if not MODERATION_DOCUMENT_URL_IS_VALID: MODERATION_DOCUMENT_URL_MESSAGE: Final[str] = ( "MODERATION_DOCUMENT_URL must be a valid URL." @@ -726,8 +755,9 @@ def _setup_moderation_document_url(cls) -> None: def _setup_strike_performed_manually_warning_location(cls) -> None: raw_strike_performed_manually_warning_location: str = os.getenv( "MANUAL_MODERATION_WARNING_MESSAGE_LOCATION", - "DM", - ) + default="DM", + ).strip() + if not raw_strike_performed_manually_warning_location: STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION_MESSAGE: Final[str] = ( "MANUAL_MODERATION_WARNING_MESSAGE_LOCATION must be a valid name " @@ -741,9 +771,9 @@ def _setup_strike_performed_manually_warning_location(cls) -> None: @classmethod def _setup_auto_add_committee_to_threads(cls) -> None: - raw_auto_add_committee_to_threads: str = str( - os.getenv("AUTO_ADD_COMMITTEE_TO_THREADS", "True") - ).lower() + raw_auto_add_committee_to_threads: str = ( + str(os.getenv("AUTO_ADD_COMMITTEE_TO_THREADS", "True")).lower().strip() + ) if raw_auto_add_committee_to_threads not in TRUE_VALUES | FALSE_VALUES: INVALID_AUTO_ADD_COMMITTEE_TO_THREADS_MESSAGE: Final[str] = ( From b644f3a828dda4b9c112f61ab055b45a48ef8050 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 31 May 2025 22:07:48 +0000 Subject: [PATCH 02/22] Implement minor workflow fixes --- .github/workflows/check-build-deploy.yaml | 16 ++++- pyproject.toml | 8 ++- uv.lock | 78 ++++++++++++++++++++++- 3 files changed, 98 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check-build-deploy.yaml b/.github/workflows/check-build-deploy.yaml index 80cd96e2b..bbda5f820 100644 --- a/.github/workflows/check-build-deploy.yaml +++ b/.github/workflows/check-build-deploy.yaml @@ -44,7 +44,7 @@ jobs: enable-cache: true - name: Install mypy From Locked Dependencies - run: uv sync --no-group dev --group type-check + run: uv sync --no-group dev --group type-check --group test - name: Store Hashed Python Version id: store-hashed-python-version @@ -165,7 +165,19 @@ jobs: key: pytest|${{steps.store-hashed-python-version.outputs.hashed_python_version}} - name: Run pytest - run: uv run -- pytest # TODO: Add GitHub workflows output format + run: uv run pytest --cov --cov-branch --cov-report=xml --junitxml=junit.xml + + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/test-results-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + - name: Upload coverage report to Codecov + uses: codecov/codecov-action@v5 + if: ${{ !cancelled() }} + with: + token: ${{ secrets.CODECOV_TOKEN }} ruff-lint: runs-on: ubuntu-latest diff --git a/pyproject.toml b/pyproject.toml index 4facdcfae..3b89c9357 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ main = [ "validators>=0.34", ] pre-commit = ["pre-commit>=4.0"] -test = ["pytest>=8.3"] +test = ["gitpython>=3.1.44", "pytest>=8.3", "pytest-cov>=6.1.1"] type-check = ["django-stubs[compatible-mypy]>=5.1", "mypy>=1.13", "types-beautifulsoup4>=4.12"] [project] # TODO: Remove [project] table once https://github.com/astral-sh/uv/issues/8582 is completed @@ -207,6 +207,12 @@ parametrize-values-type = "tuple" keep-runtime-typing = true +[tool.coverage.report] +exclude_also = ["if TYPE_CHECKING:"] +skip_covered = true +sort = "cover" + + [tool.pymarkdown] extensions.front-matter.enabled = true mode.strict-config = true diff --git a/uv.lock b/uv.lock index 958755141..1e0a0bbb4 100644 --- a/uv.lock +++ b/uv.lock @@ -219,6 +219,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, ] +[[package]] +name = "coverage" +version = "7.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/07/998afa4a0ecdf9b1981ae05415dad2d4e7716e1b1f00abbd91691ac09ac9/coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27", size = 812759, upload-time = "2025-05-23T11:39:57.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/2a/1da1ada2e3044fcd4a3254fb3576e160b8fe5b36d705c8a31f793423f763/coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c", size = 211876, upload-time = "2025-05-23T11:38:29.01Z" }, + { url = "https://files.pythonhosted.org/packages/70/e9/3d715ffd5b6b17a8be80cd14a8917a002530a99943cc1939ad5bb2aa74b9/coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1", size = 212130, upload-time = "2025-05-23T11:38:30.675Z" }, + { url = "https://files.pythonhosted.org/packages/a0/02/fdce62bb3c21649abfd91fbdcf041fb99be0d728ff00f3f9d54d97ed683e/coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279", size = 246176, upload-time = "2025-05-23T11:38:32.395Z" }, + { url = "https://files.pythonhosted.org/packages/a7/52/decbbed61e03b6ffe85cd0fea360a5e04a5a98a7423f292aae62423b8557/coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99", size = 243068, upload-time = "2025-05-23T11:38:33.989Z" }, + { url = "https://files.pythonhosted.org/packages/38/6c/d0e9c0cce18faef79a52778219a3c6ee8e336437da8eddd4ab3dbd8fadff/coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20", size = 245328, upload-time = "2025-05-23T11:38:35.568Z" }, + { url = "https://files.pythonhosted.org/packages/f0/70/f703b553a2f6b6c70568c7e398ed0789d47f953d67fbba36a327714a7bca/coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2", size = 245099, upload-time = "2025-05-23T11:38:37.627Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fb/4cbb370dedae78460c3aacbdad9d249e853f3bc4ce5ff0e02b1983d03044/coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57", size = 243314, upload-time = "2025-05-23T11:38:39.238Z" }, + { url = "https://files.pythonhosted.org/packages/39/9f/1afbb2cb9c8699b8bc38afdce00a3b4644904e6a38c7bf9005386c9305ec/coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f", size = 244489, upload-time = "2025-05-23T11:38:40.845Z" }, + { url = "https://files.pythonhosted.org/packages/79/fa/f3e7ec7d220bff14aba7a4786ae47043770cbdceeea1803083059c878837/coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8", size = 214366, upload-time = "2025-05-23T11:38:43.551Z" }, + { url = "https://files.pythonhosted.org/packages/54/aa/9cbeade19b7e8e853e7ffc261df885d66bf3a782c71cba06c17df271f9e6/coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223", size = 215165, upload-time = "2025-05-23T11:38:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/c4/73/e2528bf1237d2448f882bbebaec5c3500ef07301816c5c63464b9da4d88a/coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f", size = 213548, upload-time = "2025-05-23T11:38:46.74Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1a/0b9c32220ad694d66062f571cc5cedfa9997b64a591e8a500bb63de1bd40/coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32", size = 203623, upload-time = "2025-05-23T11:39:53.846Z" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -358,6 +378,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/3e/b04a0adda73bd52b390d730071c0d577073d3d26740ee1bad25c3ad0f37b/frozenlist-1.6.0-py3-none-any.whl", hash = "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191", size = 12404, upload-time = "2025-04-17T22:38:51.668Z" }, ] +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.44" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196, upload-time = "2025-01-02T07:32:43.59Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599, upload-time = "2025-01-02T07:32:40.731Z" }, +] + [[package]] name = "identify" version = "2.6.10" @@ -695,6 +739,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, ] +[[package]] +name = "pytest-cov" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload-time = "2025-04-05T14:07:51.592Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -794,6 +851,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, +] + [[package]] name = "soupsieve" version = "2.7" @@ -821,9 +887,11 @@ source = { virtual = "." } dev = [ { name = "ccft-pymarkdown" }, { name = "django-stubs", extra = ["compatible-mypy"] }, + { name = "gitpython" }, { name = "mypy" }, { name = "pre-commit" }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "ruff" }, { name = "types-beautifulsoup4" }, ] @@ -849,7 +917,9 @@ pre-commit = [ { name = "pre-commit" }, ] test = [ + { name = "gitpython" }, { name = "pytest" }, + { name = "pytest-cov" }, ] type-check = [ { name = "django-stubs", extra = ["compatible-mypy"] }, @@ -863,9 +933,11 @@ type-check = [ dev = [ { name = "ccft-pymarkdown", specifier = ">=2.0" }, { name = "django-stubs", extras = ["compatible-mypy"], specifier = ">=5.1" }, + { name = "gitpython", specifier = ">=3.1.44" }, { name = "mypy", specifier = ">=1.13" }, { name = "pre-commit", specifier = ">=4.0" }, { name = "pytest", specifier = ">=8.3" }, + { name = "pytest-cov", specifier = ">=6.1.1" }, { name = "ruff", specifier = ">=0.9" }, { name = "types-beautifulsoup4", specifier = ">=4.12" }, ] @@ -888,7 +960,11 @@ main = [ { name = "validators", specifier = ">=0.34" }, ] pre-commit = [{ name = "pre-commit", specifier = ">=4.0" }] -test = [{ name = "pytest", specifier = ">=8.3" }] +test = [ + { name = "gitpython", specifier = ">=3.1.44" }, + { name = "pytest", specifier = ">=8.3" }, + { name = "pytest-cov", specifier = ">=6.1.1" }, +] type-check = [ { name = "django-stubs", extras = ["compatible-mypy"], specifier = ">=5.1" }, { name = "mypy", specifier = ">=1.13" }, From 2567f2b0640bea3224d7aedabed8208457ad38f4 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 31 May 2025 22:11:26 +0000 Subject: [PATCH 03/22] fix stuff --- config.py | 192 +++++++++++++++++++++++------------------------------- 1 file changed, 81 insertions(+), 111 deletions(-) diff --git a/config.py b/config.py index 71d438a92..73812917e 100644 --- a/config.py +++ b/config.py @@ -47,7 +47,7 @@ TRUE_VALUES: "Final[frozenset[str]]" = frozenset({"true", "1", "t", "y", "yes", "on"}) FALSE_VALUES: "Final[frozenset[str]]" = frozenset({"false", "0", "f", "n", "no", "off"}) VALID_SEND_INTRODUCTION_REMINDERS_VALUES: "Final[frozenset[str]]" = frozenset( - {"once"} | TRUE_VALUES | FALSE_VALUES | {"interval"}, + {"once"} | TRUE_VALUES | FALSE_VALUES, ) DEFAULT_STATISTICS_ROLES: "Final[frozenset[str]]" = frozenset( { @@ -136,9 +136,7 @@ def __getitem__(self, item: str) -> "Any": # type: ignore[explicit-any] # noqa @staticmethod def _setup_logging() -> None: - raw_console_log_level: str = ( - str(os.getenv("CONSOLE_LOG_LEVEL", "INFO")).upper().strip() - ) + raw_console_log_level: str = str(os.getenv("CONSOLE_LOG_LEVEL", "INFO")).upper() if raw_console_log_level not in LOG_LEVEL_CHOICES: INVALID_LOG_LEVEL_MESSAGE: Final[str] = f"""LOG_LEVEL must be one of { @@ -160,15 +158,15 @@ def _setup_logging() -> None: @classmethod def _setup_discord_bot_token(cls) -> None: - raw_discord_bot_token: str = os.getenv("DISCORD_BOT_TOKEN", default="").strip() + raw_discord_bot_token: str | None = os.getenv("DISCORD_BOT_TOKEN") DISCORD_BOT_TOKEN_IS_VALID: Final[bool] = bool( - re.fullmatch( + raw_discord_bot_token + and re.fullmatch( r"\A([A-Za-z0-9_-]{24,26})\.([A-Za-z0-9_-]{6})\.([A-Za-z0-9_-]{27,38})\Z", raw_discord_bot_token, ), ) - if not DISCORD_BOT_TOKEN_IS_VALID: INVALID_DISCORD_BOT_TOKEN_MESSAGE: Final[str] = ( "DISCORD_BOT_TOKEN must be a valid Discord bot token " # noqa: S105 @@ -182,12 +180,13 @@ def _setup_discord_bot_token(cls) -> None: def _setup_discord_log_channel_webhook(cls) -> "Logger": raw_discord_log_channel_webhook_url: str = os.getenv( "DISCORD_LOG_CHANNEL_WEBHOOK_URL", "" - ).strip() + ) - if not validators.url( - raw_discord_log_channel_webhook_url - ) or not raw_discord_log_channel_webhook_url.startswith( - "https://discord.com/api/webhooks/" + if raw_discord_log_channel_webhook_url and ( + not validators.url(raw_discord_log_channel_webhook_url) + or not raw_discord_log_channel_webhook_url.startswith( + "https://discord.com/api/webhooks/" + ) ): INVALID_DISCORD_LOG_CHANNEL_WEBHOOK_URL_MESSAGE: Final[str] = ( "DISCORD_LOG_CHANNEL_WEBHOOK_URL must be a valid webhook URL " @@ -216,12 +215,11 @@ def _setup_discord_log_channel_webhook(cls) -> "Logger": @classmethod def _setup_discord_guild_id(cls) -> None: - raw_discord_guild_id: str = os.getenv("DISCORD_GUILD_ID", default="").strip() + raw_discord_guild_id: str | None = os.getenv("DISCORD_GUILD_ID") DISCORD_GUILD_ID_IS_VALID: Final[bool] = bool( - re.fullmatch(r"\A\d{17,20}\Z", raw_discord_guild_id), + raw_discord_guild_id and re.fullmatch(r"\A\d{17,20}\Z", raw_discord_guild_id), ) - if not DISCORD_GUILD_ID_IS_VALID: INVALID_DISCORD_GUILD_ID_MESSAGE: Final[str] = ( "DISCORD_GUILD_ID must be a valid Discord guild ID " @@ -229,11 +227,14 @@ def _setup_discord_guild_id(cls) -> None: ) raise ImproperlyConfiguredError(INVALID_DISCORD_GUILD_ID_MESSAGE) - cls._settings["_DISCORD_MAIN_GUILD_ID"] = int(raw_discord_guild_id) + cls._settings["_DISCORD_MAIN_GUILD_ID"] = int(raw_discord_guild_id) # type: ignore[arg-type] @classmethod def _setup_group_full_name(cls) -> None: - raw_group_full_name: str = os.getenv("GROUP_NAME", default="").strip() + raw_group_full_name: str | None = os.getenv("GROUP_NAME") + + if raw_group_full_name is not None: + raw_group_full_name = raw_group_full_name.strip() if not raw_group_full_name: cls._settings["_GROUP_FULL_NAME"] = None @@ -249,7 +250,10 @@ def _setup_group_full_name(cls) -> None: @classmethod def _setup_group_short_name(cls) -> None: - raw_group_short_name: str = os.getenv("GROUP_SHORT_NAME", default="").strip() + raw_group_short_name: str | None = os.getenv("GROUP_SHORT_NAME") + + if raw_group_short_name is not None: + raw_group_short_name = raw_group_short_name.strip() if not raw_group_short_name: cls._settings["_GROUP_SHORT_NAME"] = None @@ -265,17 +269,15 @@ def _setup_group_short_name(cls) -> None: @classmethod def _setup_purchase_membership_url(cls) -> None: - raw_purchase_membership_url: str = os.getenv( - "PURCHASE_MEMBERSHIP_URL", default="" - ).strip() + raw_purchase_membership_url: str | None = os.getenv("PURCHASE_MEMBERSHIP_URL") + + if raw_purchase_membership_url is not None: + raw_purchase_membership_url = raw_purchase_membership_url.strip() if not raw_purchase_membership_url: cls._settings["PURCHASE_MEMBERSHIP_URL"] = None return - if "://" not in raw_purchase_membership_url: - raw_purchase_membership_url = "https://" + raw_purchase_membership_url - if not validators.url(raw_purchase_membership_url): INVALID_PURCHASE_MEMBERSHIP_URL_MESSAGE: Final[str] = ( "PURCHASE_MEMBERSHIP_URL must be a valid URL." @@ -286,15 +288,15 @@ def _setup_purchase_membership_url(cls) -> None: @classmethod def _setup_membership_perks_url(cls) -> None: - raw_membership_perks_url: str = os.getenv("MEMBERSHIP_PERKS_URL", default="").strip() + raw_membership_perks_url: str | None = os.getenv("MEMBERSHIP_PERKS_URL") + + if raw_membership_perks_url is not None: + raw_membership_perks_url = raw_membership_perks_url.strip() if not raw_membership_perks_url: cls._settings["MEMBERSHIP_PERKS_URL"] = None return - if "://" not in raw_membership_perks_url: - raw_membership_perks_url = "https://" + raw_membership_perks_url - if not validators.url(raw_membership_perks_url): INVALID_MEMBERSHIP_PERKS_URL_MESSAGE: Final[str] = ( "MEMBERSHIP_PERKS_URL must be a valid URL." @@ -305,17 +307,15 @@ def _setup_membership_perks_url(cls) -> None: @classmethod def _setup_custom_discord_invite_url(cls) -> None: - raw_custom_discord_invite_url: str = os.getenv( - "CUSTOM_DISCORD_INVITE_URL", default="" - ).strip() + raw_custom_discord_invite_url: str | None = os.getenv("CUSTOM_DISCORD_INVITE_URL") + + if raw_custom_discord_invite_url is not None: + raw_custom_discord_invite_url = raw_custom_discord_invite_url.strip() if not raw_custom_discord_invite_url: cls._settings["CUSTOM_DISCORD_INVITE_URL"] = None return - if "://" not in raw_custom_discord_invite_url: - raw_custom_discord_invite_url = "https://" + raw_custom_discord_invite_url - if not validators.url(raw_custom_discord_invite_url): INVALID_CUSTOM_DISCORD_INVITE_URL_MESSAGE: Final[str] = ( "CUSTOM_DISCORD_INVITE_URL must be a valid URL." @@ -326,10 +326,14 @@ def _setup_custom_discord_invite_url(cls) -> None: @classmethod def _setup_ping_command_easter_egg_probability(cls) -> None: - raw_ping_command_easter_egg_probability_string: str = os.getenv( - "PING_COMMAND_EASTER_EGG_PROBABILITY", - default="", - ).strip() + raw_ping_command_easter_egg_probability_string: str | None = os.getenv( + "PING_COMMAND_EASTER_EGG_PROBABILITY" + ) + + if raw_ping_command_easter_egg_probability_string is not None: + raw_ping_command_easter_egg_probability_string = ( + raw_ping_command_easter_egg_probability_string.strip() + ) if not raw_ping_command_easter_egg_probability_string: cls._settings["PING_COMMAND_EASTER_EGG_PROBABILITY"] = 1 @@ -367,7 +371,7 @@ def _get_messages_dict(cls, raw_messages_file_path: str | None) -> Mapping[str, ) messages_file_path: Path = ( - Path(raw_messages_file_path.strip()) + Path(raw_messages_file_path) if raw_messages_file_path else PROJECT_ROOT / Path("messages.json") ) @@ -434,10 +438,10 @@ def _setup_roles_messages(cls) -> None: @classmethod def _setup_organisation_id(cls) -> None: - raw_organisation_id: str = os.getenv("ORGANISATION_ID", default="").strip() + raw_organisation_id: str | None = os.getenv("ORGANISATION_ID") ORGANISATION_ID_IS_VALID: Final[bool] = bool( - re.fullmatch(r"\A\d{4,5}\Z", raw_organisation_id), + raw_organisation_id and re.fullmatch(r"\A\d{4,5}\Z", raw_organisation_id), ) if not ORGANISATION_ID_IS_VALID: @@ -450,15 +454,14 @@ def _setup_organisation_id(cls) -> None: @classmethod def _setup_members_list_auth_session_cookie(cls) -> None: - raw_members_list_auth_session_cookie: str = os.getenv( + raw_members_list_auth_session_cookie: str | None = os.getenv( "MEMBERS_LIST_URL_SESSION_COOKIE", - default="", - ).strip() + ) MEMBERS_LIST_AUTH_SESSION_COOKIE_IS_VALID: Final[bool] = bool( - re.fullmatch(r"\A[A-Fa-f\d]{128,256}\Z", raw_members_list_auth_session_cookie), + raw_members_list_auth_session_cookie + and re.fullmatch(r"\A[A-Fa-f\d]{128,256}\Z", raw_members_list_auth_session_cookie), ) - if not MEMBERS_LIST_AUTH_SESSION_COOKIE_IS_VALID: INVALID_MEMBERS_LIST_AUTH_SESSION_COOKIE_MESSAGE: Final[str] = ( "MEMBERS_LIST_URL_SESSION_COOKIE must be a valid .ASPXAUTH cookie." @@ -471,9 +474,9 @@ def _setup_members_list_auth_session_cookie(cls) -> None: @classmethod def _setup_send_introduction_reminders(cls) -> None: - raw_send_introduction_reminders: str | bool = ( - str(os.getenv("SEND_INTRODUCTION_REMINDERS", "Once")).lower().strip() - ) + raw_send_introduction_reminders: str | bool = str( + os.getenv("SEND_INTRODUCTION_REMINDERS", "Once"), + ).lower() if raw_send_introduction_reminders not in VALID_SEND_INTRODUCTION_REMINDERS_VALUES: INVALID_SEND_INTRODUCTION_REMINDERS_MESSAGE: Final[str] = ( @@ -500,9 +503,7 @@ def _setup_send_introduction_reminders_delay(cls) -> None: raw_send_introduction_reminders_delay: re.Match[str] | None = re.fullmatch( r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", - str( - os.getenv("SEND_INTRODUCTION_REMINDERS_DELAY", "40h").strip().replace(" ", "") - ), + str(os.getenv("SEND_INTRODUCTION_REMINDERS_DELAY", "40h")), ) raw_timedelta_send_introduction_reminders_delay: timedelta = timedelta() @@ -527,7 +528,8 @@ def _setup_send_introduction_reminders_delay(cls) -> None: if raw_timedelta_send_introduction_reminders_delay < timedelta(days=1): TOO_SMALL_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE: Final[str] = ( - "SEND_INTRODUCTION_REMINDERS_DELAY must be longer than or equal to 1 day." + "SEND_INTRODUCTION_REMINDERS_DELAY must be longer than or equal to 1 day " + "(in any allowed format)." ) raise ImproperlyConfiguredError( TOO_SMALL_SEND_INTRODUCTION_REMINDERS_DELAY_MESSAGE, @@ -548,11 +550,7 @@ def _setup_send_introduction_reminders_interval(cls) -> None: raw_send_introduction_reminders_interval: re.Match[str] | None = re.fullmatch( r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?\Z", - str( - os.getenv("SEND_INTRODUCTION_REMINDERS_INTERVAL", "6h") - .strip() - .replace(" ", "") - ), + str(os.getenv("SEND_INTRODUCTION_REMINDERS_INTERVAL", "6h")), ) raw_timedelta_details_send_introduction_reminders_interval: Mapping[str, float] = { @@ -575,28 +573,15 @@ def _setup_send_introduction_reminders_interval(cls) -> None: if value } - if ( - timedelta( - **raw_timedelta_details_send_introduction_reminders_interval - ).total_seconds() - <= 3 - ): - TOO_SMALL_SEND_INTRODUCTION_REMINDERS_INTERVAL_MESSAGE: Final[str] = ( - "SEND_INTRODUCTION_REMINDERS_INTERVAL must be longer than 3 seconds." - ) - raise ImproperlyConfiguredError( - TOO_SMALL_SEND_INTRODUCTION_REMINDERS_INTERVAL_MESSAGE, - ) - cls._settings["SEND_INTRODUCTION_REMINDERS_INTERVAL"] = ( raw_timedelta_details_send_introduction_reminders_interval ) @classmethod def _setup_send_get_roles_reminders(cls) -> None: - raw_send_get_roles_reminders: str = ( - str(os.getenv("SEND_GET_ROLES_REMINDERS", "True")).lower().strip() - ) + raw_send_get_roles_reminders: str = str( + os.getenv("SEND_GET_ROLES_REMINDERS", "True"), + ).lower() if raw_send_get_roles_reminders not in TRUE_VALUES | FALSE_VALUES: INVALID_SEND_GET_ROLES_REMINDERS_MESSAGE: Final[str] = ( @@ -617,7 +602,7 @@ def _setup_send_get_roles_reminders_delay(cls) -> None: raw_send_get_roles_reminders_delay: re.Match[str] | None = re.fullmatch( r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?(?:(?P(?:\d*\.)?\d+)d)?(?:(?P(?:\d*\.)?\d+)w)?\Z", - str(os.getenv("SEND_GET_ROLES_REMINDERS_DELAY", "40h").strip().replace(" ", "")), + str(os.getenv("SEND_GET_ROLES_REMINDERS_DELAY", "40h")), ) raw_timedelta_send_get_roles_reminders_delay: timedelta = timedelta() @@ -642,8 +627,8 @@ def _setup_send_get_roles_reminders_delay(cls) -> None: if raw_timedelta_send_get_roles_reminders_delay < timedelta(days=1): TOO_SMALL_SEND_GET_ROLES_REMINDERS_DELAY_MESSAGE: Final[str] = ( - "SEND_SEND_GET_ROLES_REMINDERS_DELAY must be " - "longer than or equal to 1 day." + "SEND_SEND_GET_ROLES_REMINDERS_DELAY " + "must be longer than or equal to 1 day (in any allowed format)." ) raise ImproperlyConfiguredError( TOO_SMALL_SEND_GET_ROLES_REMINDERS_DELAY_MESSAGE, @@ -664,16 +649,14 @@ def _setup_advanced_send_get_roles_reminders_interval(cls) -> None: raw_advanced_send_get_roles_reminders_interval: re.Match[str] | None = re.fullmatch( r"\A(?:(?P(?:\d*\.)?\d+)s)?(?:(?P(?:\d*\.)?\d+)m)?(?:(?P(?:\d*\.)?\d+)h)?\Z", - str( - os.getenv("ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL", "24h") - .strip() - .replace(" ", "") - ), + str(os.getenv("ADVANCED_SEND_GET_ROLES_REMINDERS_INTERVAL", "24h")), ) raw_timedelta_details_advanced_send_get_roles_reminders_interval: Mapping[ str, float - ] = {"hours": 24} + ] = { + "hours": 24, + } if cls._settings["SEND_GET_ROLES_REMINDERS"]: if not raw_advanced_send_get_roles_reminders_interval: @@ -701,48 +684,36 @@ def _setup_advanced_send_get_roles_reminders_interval(cls) -> None: def _setup_statistics_days(cls) -> None: e: ValueError try: - raw_statistics_days: float = float(os.getenv("STATISTICS_DAYS", "30").strip()) + raw_statistics_days: float = float(os.getenv("STATISTICS_DAYS", "30")) except ValueError as e: INVALID_STATISTICS_DAYS_MESSAGE: Final[str] = ( "STATISTICS_DAYS must contain the statistics period in days." ) raise ImproperlyConfiguredError(INVALID_STATISTICS_DAYS_MESSAGE) from e - if raw_statistics_days < 1: - TOO_SMALL_STATISTICS_DAYS_MESSAGE: Final[str] = ( - "STATISTICS_DAYS cannot be less than or equal to 1 day" - ) - raise ImproperlyConfiguredError(TOO_SMALL_STATISTICS_DAYS_MESSAGE) - cls._settings["STATISTICS_DAYS"] = timedelta(days=raw_statistics_days) @classmethod def _setup_statistics_roles(cls) -> None: - raw_statistics_roles: str = os.getenv("STATISTICS_ROLES", default="").strip() + raw_statistics_roles: str | None = os.getenv("STATISTICS_ROLES") if not raw_statistics_roles: cls._settings["STATISTICS_ROLES"] = DEFAULT_STATISTICS_ROLES - return - cls._settings["STATISTICS_ROLES"] = { - raw_statistics_role.strip() - for raw_statistics_role in raw_statistics_roles.split(",") - if raw_statistics_role - } + else: + cls._settings["STATISTICS_ROLES"] = { + raw_statistics_role + for raw_statistics_role in raw_statistics_roles.split(",") + if raw_statistics_role + } @classmethod def _setup_moderation_document_url(cls) -> None: - raw_moderation_document_url: str = os.getenv( - "MODERATION_DOCUMENT_URL", default="" - ).strip() - - if raw_moderation_document_url and "://" not in raw_moderation_document_url: - raw_moderation_document_url = "https://" + raw_moderation_document_url + raw_moderation_document_url: str | None = os.getenv("MODERATION_DOCUMENT_URL") MODERATION_DOCUMENT_URL_IS_VALID: Final[bool] = bool( - validators.url(raw_moderation_document_url), + raw_moderation_document_url and validators.url(raw_moderation_document_url), ) - if not MODERATION_DOCUMENT_URL_IS_VALID: MODERATION_DOCUMENT_URL_MESSAGE: Final[str] = ( "MODERATION_DOCUMENT_URL must be a valid URL." @@ -755,9 +726,8 @@ def _setup_moderation_document_url(cls) -> None: def _setup_strike_performed_manually_warning_location(cls) -> None: raw_strike_performed_manually_warning_location: str = os.getenv( "MANUAL_MODERATION_WARNING_MESSAGE_LOCATION", - default="DM", - ).strip() - + "DM", + ) if not raw_strike_performed_manually_warning_location: STRIKE_PERFORMED_MANUALLY_WARNING_LOCATION_MESSAGE: Final[str] = ( "MANUAL_MODERATION_WARNING_MESSAGE_LOCATION must be a valid name " @@ -771,9 +741,9 @@ def _setup_strike_performed_manually_warning_location(cls) -> None: @classmethod def _setup_auto_add_committee_to_threads(cls) -> None: - raw_auto_add_committee_to_threads: str = ( - str(os.getenv("AUTO_ADD_COMMITTEE_TO_THREADS", "True")).lower().strip() - ) + raw_auto_add_committee_to_threads: str = str( + os.getenv("AUTO_ADD_COMMITTEE_TO_THREADS", "True") + ).lower() if raw_auto_add_committee_to_threads not in TRUE_VALUES | FALSE_VALUES: INVALID_AUTO_ADD_COMMITTEE_TO_THREADS_MESSAGE: Final[str] = ( From a458f3ba341678393f47f502bcd12425b0968e59 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sat, 31 May 2025 22:12:02 +0000 Subject: [PATCH 04/22] [pre-commit.ci lite] apply automatic fixes --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3b89c9357..83a4c080d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ main = [ "validators>=0.34", ] pre-commit = ["pre-commit>=4.0"] -test = ["gitpython>=3.1.44", "pytest>=8.3", "pytest-cov>=6.1.1"] +test = ["gitpython>=3.1.44", "pytest-cov>=6.1.1", "pytest>=8.3"] type-check = ["django-stubs[compatible-mypy]>=5.1", "mypy>=1.13", "types-beautifulsoup4>=4.12"] [project] # TODO: Remove [project] table once https://github.com/astral-sh/uv/issues/8582 is completed From 1236eff0f3c69ae3bf39dea20c55853991118bc7 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 1 Jun 2025 12:22:12 +0000 Subject: [PATCH 05/22] Implement exceptions tests --- tests/test_exceptions.py | 792 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 792 insertions(+) create mode 100644 tests/test_exceptions.py diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 000000000..c67bdae8e --- /dev/null +++ b/tests/test_exceptions.py @@ -0,0 +1,792 @@ +"""Automated test suite for all custom exceptions within `exceptions.py`.""" + +from typing import TYPE_CHECKING, override + +import pytest +from typed_classproperties import classproperty + +from exceptions import ( + ChannelDoesNotExistError, + DiscordMemberNotInMainGuildError, + GuildDoesNotExistError, + ImproperlyConfiguredError, + InvalidMessagesJSONFileError, + MessagesJSONFileMissingKeyError, + MessagesJSONFileValueError, + RoleDoesNotExistError, +) +from exceptions.base import BaseDoesNotExistError, BaseTeXBotError + +if TYPE_CHECKING: + from collections.abc import Iterator + from typing import Final + + +class TestImproperlyConfiguredError: + """Test case to unit-test the `ImproperlyConfiguredError` exception.""" + + @pytest.mark.parametrize("test_exception_message", ("Error 1 occurred",)) + def test_message(self, test_exception_message: str) -> None: + """Test that the custom error message is used in the `__str__` representation.""" + assert str(ImproperlyConfiguredError(test_exception_message)) == test_exception_message + + @pytest.mark.parametrize("test_exception_message", ("Error 1 occurred",)) + def test_message_when_raised(self, test_exception_message: str) -> None: + """Test that the custom error message is shown when the exception is raised.""" + with pytest.raises(ImproperlyConfiguredError, match=test_exception_message): + raise ImproperlyConfiguredError(test_exception_message) + + +class TestBaseTeXBotError: + """Test case to unit-test the `BaseTeXBotError` exception.""" + + class _DefaultMessageBaseTeXBotErrorSubclass(BaseTeXBotError): # noqa: N818 + """ + Custom subclass implementation of `BaseTeXBotError`. + + This specific custom subclass implementation is used for testing + with a subclass that has a `DEFAULT_MESSAGE` set. + """ + + @classproperty + @override + def DEFAULT_MESSAGE(cls) -> str: + """The message to be displayed alongside this exception class if not provided.""" + return "Error 1 occurred" + + class _AttributesBaseTeXBotErrorSubclass(_DefaultMessageBaseTeXBotErrorSubclass): + """ + Custom subclass implementation of `BaseTeXBotError`. + + This specific custom subclass implementation is used for testing + with a subclass that has a new instance attribute (compared to the parent base class). + """ + + def __init__( + self, message: str | None = None, test_attribute_value: object | None = None + ) -> None: + """Initialize a new exception with the given error message.""" + self.test_attribute: object | None = test_attribute_value + + super().__init__(message=message) + + @pytest.mark.parametrize( + "test_base_texbot_error_subclass", + ( + _DefaultMessageBaseTeXBotErrorSubclass(), + _DefaultMessageBaseTeXBotErrorSubclass(message=None), + _DefaultMessageBaseTeXBotErrorSubclass(message=""), + ), + ) + def test_default_message(self, test_base_texbot_error_subclass: BaseTeXBotError) -> None: + """Test that the class' default error message is shown, when no custom message.""" + assert ( + test_base_texbot_error_subclass.message + == self._DefaultMessageBaseTeXBotErrorSubclass.DEFAULT_MESSAGE + ) + assert ( + str(test_base_texbot_error_subclass) + == self._DefaultMessageBaseTeXBotErrorSubclass.DEFAULT_MESSAGE + ) + + @pytest.mark.parametrize("test_exception_message", ("Other test error occurred",)) + def test_custom_message(self, test_exception_message: str) -> None: + """Test that the custom error message is shown, when given.""" + assert ( + self._DefaultMessageBaseTeXBotErrorSubclass(test_exception_message).message + == test_exception_message + ) + assert ( + str(self._DefaultMessageBaseTeXBotErrorSubclass(test_exception_message)) + == test_exception_message + ) + + @pytest.mark.parametrize( + "test_attributes_base_texbot_error_subclass", + ( + _AttributesBaseTeXBotErrorSubclass(), + _AttributesBaseTeXBotErrorSubclass(test_attribute_value=None), + _AttributesBaseTeXBotErrorSubclass(test_attribute_value=7), + ), + ) + def test_repr_with_attributes( + self, test_attributes_base_texbot_error_subclass: _AttributesBaseTeXBotErrorSubclass + ) -> None: + """Test that the exception message contains any instance attributes.""" + assert ( + f"test_attribute={test_attributes_base_texbot_error_subclass.test_attribute!r}" + in repr(test_attributes_base_texbot_error_subclass) + ) + + +class TestBaseErrorWithErrorCode: + """ + Test case to unit-test the `BaseErrorWithErrorCode` exception. + + If there are no unit-tests within this test case, + it is because all the functionality of `BaseErrorWithErrorCode` is inherited + from its parent class so is already unit-tested in the parent class's dedicated test case. + """ + + +class TestBaseDoesNotExistError: + """Test case to unit-test the `BaseDoesNotExistError` exception.""" + + class _NoDependantsBaseDoesNotExistErrorSubclass(BaseDoesNotExistError): # noqa: N818 + """ + Custom subclass implementation of `BaseDoesNotExistError`. + + This specific custom subclass implementation is used for testing + with a subclass that has no dependent commands, tasks or events. + """ + + @classproperty + @override + def DEFAULT_MESSAGE(cls) -> str: + """The message to be displayed alongside this exception class if not provided.""" + return "Error 1 occurred" + + @classproperty + @override + def ERROR_CODE(cls) -> str: + """The unique error code for users to tell admins about an error that occurred.""" + return "E1" + + @classproperty + @override + def DOES_NOT_EXIST_TYPE(cls) -> str: + """The name of the Discord entity that this `DoesNotExistError` is attached to.""" + return "object_type" + + class _OneDependentCommandBaseDoesNotExistErrorSubclass( + _NoDependantsBaseDoesNotExistErrorSubclass + ): + """ + Custom subclass implementation of `BaseDoesNotExistError`. + + This specific custom subclass implementation is used for testing + with a subclass that has one command-dependent. + """ + + @classproperty + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: + """ + The set of names of bot commands that require this Discord entity. + + This set being empty could mean that all bot commands require this Discord entity, + or no bot commands require this Discord entity. + """ + return frozenset(("command_1",)) + + class _MultipleDependentCommandsBaseDoesNotExistErrorSubclass( + _NoDependantsBaseDoesNotExistErrorSubclass + ): + """ + Custom subclass implementation of `BaseDoesNotExistError`. + + This specific custom subclass implementation is used for testing + with a subclass that has multiple dependent commands. + """ + + @classproperty + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: + """ + The set of names of bot commands that require this Discord entity. + + This set being empty could mean that all bot commands require this Discord entity, + or no bot commands require this Discord entity. + """ + return frozenset(("command_1", "command_2", "command_3")) + + class _OneDependentTaskBaseDoesNotExistErrorSubclass( + _NoDependantsBaseDoesNotExistErrorSubclass + ): + """ + Custom subclass implementation of `BaseDoesNotExistError`. + + This specific custom subclass implementation is used for testing + with a subclass that has one task-dependent. + """ + + @classproperty + @override + def DEPENDENT_TASKS(cls) -> frozenset[str]: + """ + The set of names of bot tasks that require this Discord entity. + + This set being empty could mean that all bot tasks require this Discord entity, + or no bot tasks require this Discord entity. + """ + return frozenset(("task_1",)) + + class _MultipleDependentTasksBaseDoesNotExistErrorSubclass( + _NoDependantsBaseDoesNotExistErrorSubclass + ): + """ + Custom subclass implementation of `BaseDoesNotExistError`. + + This specific custom subclass implementation is used for testing + with a subclass that has multiple dependent tasks. + """ + + @classproperty + @override + def DEPENDENT_TASKS(cls) -> frozenset[str]: + """ + The set of names of bot tasks that require this Discord entity. + + This set being empty could mean that all bot tasks require this Discord entity, + or no bot tasks require this Discord entity. + """ + return frozenset(("task_1", "task_2", "task_3")) + + class _OneDependentEventBaseDoesNotExistErrorSubclass( + _NoDependantsBaseDoesNotExistErrorSubclass + ): + """ + Custom subclass implementation of `BaseDoesNotExistError`. + + This specific custom subclass implementation is used for testing + with a subclass that has one event dependent. + """ + + @classproperty + @override + def DEPENDENT_EVENTS(cls) -> frozenset[str]: + """ + The set of names of bot events that require this Discord entity. + + This set being empty could mean that all bot events require this Discord entity, + or no bot events require this Discord entity. + """ + return frozenset(("event_1",)) + + class _MultipleDependentEventsBaseDoesNotExistErrorSubclass( + _NoDependantsBaseDoesNotExistErrorSubclass + ): + """ + Custom subclass implementation of `BaseDoesNotExistError`. + + This specific custom subclass implementation is used for testing + with a subclass that has multiple dependent events. + """ + + @classproperty + @override + def DEPENDENT_EVENTS(cls) -> frozenset[str]: + """ + The set of names of bot events that require this Discord entity. + + This set being empty could mean that all bot events require this Discord entity, + or no bot events require this Discord entity. + """ + return frozenset(("event_1", "event_2", "event_3")) + + class _ChannelDoesNotExistTypeBaseDoesNotExistErrorSubclass( + _NoDependantsBaseDoesNotExistErrorSubclass + ): + """ + Custom subclass implementation of `BaseDoesNotExistError`. + + This specific custom subclass implementation is used for testing + with a subclass whose `DOES_NOT_EXIST_TYPE` is a channel. + """ + + @classproperty + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: + """ + The set of names of bot commands that require this Discord entity. + + This set being empty could mean that all bot commands require this Discord entity, + or no bot commands require this Discord entity. + """ + return frozenset(("command_1",)) + + @classproperty + @override + def DOES_NOT_EXIST_TYPE(cls) -> str: + """The name of the Discord entity that this `DoesNotExistError` is attached to.""" + return "channel" + + def test_get_formatted_message_with_no_dependants(self) -> None: + """ + Test that the `get_formatted_message()` function returns the correct value. + + This test is run with a `BaseDoesNotExistError` subclass + that has either no dependent commands, events or tasks. + In this case, the correct return value of the `get_formatted_message()` function + should contain the list of dependent command names separated by "," (comma) characters. + """ + with pytest.raises(ValueError, match="no dependants"): + self._NoDependantsBaseDoesNotExistErrorSubclass.get_formatted_message( + non_existent_object_identifier="object_1", + ) + + @pytest.mark.parametrize( + "dependent_commands_base_does_not_exist_error_subclass", + ( + _OneDependentCommandBaseDoesNotExistErrorSubclass, + _MultipleDependentCommandsBaseDoesNotExistErrorSubclass, + ), + ) + @pytest.mark.parametrize("test_non_existant_object_identifier", ("object_1",)) + def test_get_formatted_message_with_dependent_commands( + self, + dependent_commands_base_does_not_exist_error_subclass: type[ + _NoDependantsBaseDoesNotExistErrorSubclass + ], + test_non_existant_object_identifier: str, + ) -> None: + """ + Test that the `get_formatted_message()` function returns the correct value. + + This test is run with a `BaseDoesNotExistError` subclass + that has either one, or multiple dependent commands. + In this case, the correct return value of the `get_formatted_message()` function + should contain the list of dependent command names separated by "," (comma) characters. + """ + FORMATTED_MESSAGE: Final[str] = ( + dependent_commands_base_does_not_exist_error_subclass.get_formatted_message( + non_existent_object_identifier=test_non_existant_object_identifier, + ) + ) + + assert ( + f'"{test_non_existant_object_identifier}" ' + f"{dependent_commands_base_does_not_exist_error_subclass.DOES_NOT_EXIST_TYPE} " + f"must exist" + ) in FORMATTED_MESSAGE + + if len(dependent_commands_base_does_not_exist_error_subclass.DEPENDENT_COMMANDS) == 1: + assert ( + "the " + f"""\"/{ + next( + iter( + dependent_commands_base_does_not_exist_error_subclass.DEPENDENT_COMMANDS + ) + ) + }\" """ + "command" + ) in FORMATTED_MESSAGE + + elif len(dependent_commands_base_does_not_exist_error_subclass.DEPENDENT_COMMANDS) > 1: + DEPENDENT_COMMANDS: Final[Iterator[str]] = iter( + dependent_commands_base_does_not_exist_error_subclass.DEPENDENT_COMMANDS, + ) + + assert ( + f'the "/{next(DEPENDENT_COMMANDS)}", "/{next(DEPENDENT_COMMANDS)}" & ' + f'"/{next(DEPENDENT_COMMANDS)}" commands' + ) in FORMATTED_MESSAGE + + else: + raise NotImplementedError + + @pytest.mark.parametrize( + "dependent_tasks_base_does_not_exist_error_subclass", + ( + _OneDependentTaskBaseDoesNotExistErrorSubclass, + _MultipleDependentTasksBaseDoesNotExistErrorSubclass, + ), + ) + @pytest.mark.parametrize("test_non_existant_object_identifier", ("object_1",)) + def test_get_formatted_message_with_dependent_tasks( + self, + dependent_tasks_base_does_not_exist_error_subclass: type[ + _NoDependantsBaseDoesNotExistErrorSubclass + ], + test_non_existant_object_identifier: str, + ) -> None: + """ + Test that the `get_formatted_message()` function returns the correct value. + + This test is run with a `BaseDoesNotExistError` subclass + that has either one, or multiple dependent tasks. + In this case, the correct return value of the `get_formatted_message()` function + should contain the list of dependent task names separated by "," (comma) characters. + """ + FORMATTED_MESSAGE: Final[str] = ( + dependent_tasks_base_does_not_exist_error_subclass.get_formatted_message( + non_existent_object_identifier=test_non_existant_object_identifier, + ) + ) + + assert ( + f'"{test_non_existant_object_identifier}" ' + f"{dependent_tasks_base_does_not_exist_error_subclass.DOES_NOT_EXIST_TYPE} " + f"must exist" + ) in FORMATTED_MESSAGE + + if len(dependent_tasks_base_does_not_exist_error_subclass.DEPENDENT_TASKS) == 1: + assert ( + "the " + f"""\"{ + next( + iter( + dependent_tasks_base_does_not_exist_error_subclass.DEPENDENT_TASKS + ) + ) + }\" """ + "task" + ) in FORMATTED_MESSAGE + + elif len(dependent_tasks_base_does_not_exist_error_subclass.DEPENDENT_TASKS) > 1: + DEPENDENT_TASKS: Final[Iterator[str]] = iter( + dependent_tasks_base_does_not_exist_error_subclass.DEPENDENT_TASKS, + ) + + assert ( + f'the "{next(DEPENDENT_TASKS)}", "{next(DEPENDENT_TASKS)}" & ' + f'"{next(DEPENDENT_TASKS)}" tasks' + ) in FORMATTED_MESSAGE + + else: + raise NotImplementedError + + @pytest.mark.parametrize( + "dependent_events_base_does_not_exist_error_subclass", + ( + _OneDependentEventBaseDoesNotExistErrorSubclass, + _MultipleDependentEventsBaseDoesNotExistErrorSubclass, + ), + ) + @pytest.mark.parametrize("test_non_existant_object_identifier", ("object_1",)) + def test_get_formatted_message_with_dependent_events( + self, + dependent_events_base_does_not_exist_error_subclass: type[ + _NoDependantsBaseDoesNotExistErrorSubclass + ], + test_non_existant_object_identifier: str, + ) -> None: + """ + Test that the `get_formatted_message()` function returns the correct value. + + This test is run with a `BaseDoesNotExistError` subclass + that has either one, or multiple dependent events. + In this case, the correct return value of the `get_formatted_message()` function + should contain the list of dependent event names separated by "," (comma) characters. + """ + FORMATTED_MESSAGE: Final[str] = ( + dependent_events_base_does_not_exist_error_subclass.get_formatted_message( + non_existent_object_identifier=test_non_existant_object_identifier, + ) + ) + + assert ( + f'"{test_non_existant_object_identifier}" ' + f"{dependent_events_base_does_not_exist_error_subclass.DOES_NOT_EXIST_TYPE} " + f"must exist" + ) in FORMATTED_MESSAGE + + if len(dependent_events_base_does_not_exist_error_subclass.DEPENDENT_EVENTS) == 1: + assert ( + "the " + f"""\"{ + next( + iter( + dependent_events_base_does_not_exist_error_subclass.DEPENDENT_EVENTS + ) + ) + }\" """ + "event" + ) in FORMATTED_MESSAGE + + elif len(dependent_events_base_does_not_exist_error_subclass.DEPENDENT_EVENTS) > 1: + DEPENDENT_EVENTS: Final[Iterator[str]] = iter( + dependent_events_base_does_not_exist_error_subclass.DEPENDENT_EVENTS, + ) + + assert ( + f'the "{next(DEPENDENT_EVENTS)}", "{next(DEPENDENT_EVENTS)}" & ' + f'"{next(DEPENDENT_EVENTS)}" events' + ) in FORMATTED_MESSAGE + + else: + raise NotImplementedError + + @pytest.mark.parametrize("test_non_existant_object_identifier", ("object_1",)) + def test_get_formatted_message_with_channel_does_not_exist_type( + self, test_non_existant_object_identifier: str + ) -> None: + """ + Test that the `get_formatted_message()` function returns the correct value. + + This test is run with a `BaseDoesNotExistError` subclass + whose `DOES_NOT_EXIST_TYPE` is a Discord channel. + In this case, the correct return value of the `get_formatted_message()` function + should contain the Discord channel name prefixed by a "#" (hashtag) character, + as well as the word "channel". + """ + assert ( + f'"#{test_non_existant_object_identifier}" ' + f"""{ + self._ChannelDoesNotExistTypeBaseDoesNotExistErrorSubclass.DOES_NOT_EXIST_TYPE + } """ + "must exist" + ) in self._ChannelDoesNotExistTypeBaseDoesNotExistErrorSubclass.get_formatted_message( + non_existent_object_identifier=test_non_existant_object_identifier, + ) + + +class TestRulesChannelDoesNotExist: + """ + Test case to unit-test the `RulesChannelDoesNotExist` exception. + + If there are no unit-tests within this test case, + it is because all the functionality of `RulesChannelDoesNotExist` is inherited + from its parent class so is already unit-tested in the parent class's dedicated test case. + """ + + +class TestDiscordMemberNotInMainGuildError: + """Test case to unit-test the `DiscordMemberNotInMainGuildError` exception.""" + + @pytest.mark.parametrize("test_user_id", (99999,)) + def test_user_id_in_repr(self, test_user_id: int) -> None: + """Test that the exception message contains the given Discord user ID.""" + assert f"user_id={test_user_id!r}" in repr( + DiscordMemberNotInMainGuildError(user_id=test_user_id) + ) + + +class TestEveryoneRoleCouldNotBeRetrievedError: + """ + Test case to unit-test the `EveryoneRoleCouldNotBeRetrievedError` exception. + + If there are no unit-tests within this test case, + it is because all the functionality of `EveryoneRoleCouldNotBeRetrievedError` is inherited + from its parent class so is already unit-tested in the parent class's dedicated test case. + """ + + +class TestInvalidMessagesJSONFileError: + """Test case to unit-test the `InvalidMessagesJSONFileError` exception.""" + + @pytest.mark.parametrize("test_dict_key", ("key_1",)) + def test_dict_key_in_repr(self, test_dict_key: str) -> None: + """Test that the exception message contains the given dict key.""" + assert f"dict_key={test_dict_key!r}" in repr( + InvalidMessagesJSONFileError(dict_key=test_dict_key) + ) + + +class TestMessagesJSONFileMissingKeyError: + """Test case to unit-test the `MessagesJSONFileMissingKeyError` exception.""" + + @pytest.mark.parametrize("test_missing_key", ("key_1",)) + def test_missing_key_in_repr(self, test_missing_key: str) -> None: + """Test that the exception message contains the given JSON file missing key name.""" + assert f"dict_key={test_missing_key!r}" in repr( + MessagesJSONFileMissingKeyError(missing_key=test_missing_key) + ) + + +class TestMessagesJSONFileValueError: + """Test case to unit-test the `MessagesJSONFileValueError` exception.""" + + @pytest.mark.parametrize("test_json_file_invalid_name", ("value_1",)) + def test_invalid_value_in_repr(self, test_json_file_invalid_name: str) -> None: + """Test that the exception message contains the given invalid JSON file value.""" + assert f"invalid_value={test_json_file_invalid_name!r}" in repr( + MessagesJSONFileValueError(invalid_value=test_json_file_invalid_name) + ) + + +class TestStrikeTrackingError: + """ + Test case to unit-test the `StrikeTrackingError` exception. + + If there are no unit-tests within this test case, + it is because all the functionality of `StrikeTrackingError` is inherited + from its parent class so is already unit-tested in the parent class's dedicated test case. + """ + + +class TestGuildDoesNotExistError: + """Test case to unit-test the `GuildDoesNotExistError` exception.""" + + @pytest.mark.parametrize("test_guild_id", (99999,)) + def test_guild_id_in_repr(self, test_guild_id: int) -> None: + """Test that the exception message contains the given Discord guild ID.""" + assert f"guild_id={test_guild_id!r}" in repr( + GuildDoesNotExistError(guild_id=test_guild_id) + ) + + @pytest.mark.parametrize("test_guild_id", (99999,)) + def test_default_message_with_guild_id(self, test_guild_id: int) -> None: + """ + Test that the exception message contains the default error message. + + This test instantiates the GuildDoesNotExistError exception + with a specific Discord guild ID, + so the default error message should also contain the given Discord guild ID. + """ + assert f"ID '{test_guild_id}'" in str(GuildDoesNotExistError(guild_id=test_guild_id)) + + def test_default_message_without_guild_id(self) -> None: + """ + Test that the exception message contains the default error message. + + This test instantiates the GuildDoesNotExistError exception + without a specific Discord guild ID, + so the default error message should just contain "given ID". + """ + assert "given ID" in str(GuildDoesNotExistError()) + + +class TestRoleDoesNotExistError: + """Test case to unit-test the `RoleDoesNotExistError` exception.""" + + class _RoleDoesNotExistErrorSubclass(RoleDoesNotExistError): # noqa: N818 + """Custom subclass implementation of `RoleDoesNotExistError`, for testing purposes.""" + + @classproperty + @override + def ERROR_CODE(cls) -> str: + """The unique error code for users to tell admins about an error that occurred.""" + return "E1" + + @classproperty + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: + """ + The set of names of bot commands that require this Discord entity. + + This set being empty could mean that all bot commands require this Discord entity, + or no bot commands require this Discord entity. + """ + return frozenset(("test_command_1",)) + + @classproperty + @override + def ROLE_NAME(cls) -> str: + """The name of the Discord role that does not exist.""" + return "role_name_1" + + def test_str_contains_formatted_message(self) -> None: + """Test that the exception message contains the auto-generated formatted message.""" + assert self._RoleDoesNotExistErrorSubclass.get_formatted_message( + non_existent_object_identifier=self._RoleDoesNotExistErrorSubclass.ROLE_NAME, + ) in str(self._RoleDoesNotExistErrorSubclass()) + + def test_role_name_in_str(self) -> None: + """ + Test that the correct role name appears in the exception message. + + The correct channel name is the `ROLE_NAME` class-property + associated with the given `RoleDoesNotExistError` subclass. + """ + assert self._RoleDoesNotExistErrorSubclass.ROLE_NAME in str( + self._RoleDoesNotExistErrorSubclass() + ) + + +class TestCommitteeRoleDoesNotExistError: + """ + Test case to unit-test the `CommitteeRoleDoesNotExistError` exception. + + If there are no unit-tests within this test case, + it is because all the functionality of `CommitteeRoleDoesNotExistError` is inherited + from its parent class so is already unit-tested in the parent class's dedicated test case. + """ + + +class TestGuestRoleDoesNotExistError: + """ + Test case to unit-test the `GuestRoleDoesNotExistError` exception. + + If there are no unit-tests within this test case, + it is because all the functionality of `GuestRoleDoesNotExistError` is inherited + from its parent class so is already unit-tested in the parent class's dedicated test case. + """ + + +class TestMemberRoleDoesNotExistError: + """ + Test case to unit-test the `MemberRoleDoesNotExistError` exception. + + If there are no unit-tests within this test case, + it is because all the functionality of `MemberRoleDoesNotExistError` is inherited + from its parent class so is already unit-tested in the parent class's dedicated test case. + """ + + +class TestArchivistRoleDoesNotExistError: + """ + Test case to unit-test the `ArchivistRoleDoesNotExistError` exception. + + If there are no unit-tests within this test case, + it is because all the functionality of `ArchivistRoleDoesNotExistError` is inherited + from its parent class so is already unit-tested in the parent class's dedicated test case. + """ + + +class TestChannelDoesNotExistError: + """Test case to unit-test the `ChannelDoesNotExistError` exception.""" + + class _ChannelDoesNotExistErrorSubclass(ChannelDoesNotExistError): # noqa: N818 + """Custom subclass implementation of `ChannelDoesNotExistError`, for testing.""" + + @classproperty + @override + def ERROR_CODE(cls) -> str: + """The unique error code for users to tell admins about an error that occurred.""" + return "E1" + + @classproperty + @override + def DEPENDENT_COMMANDS(cls) -> frozenset[str]: + """ + The set of names of bot commands that require this Discord entity. + + This set being empty could mean that all bot commands require this Discord entity, + or no bot commands require this Discord entity. + """ + return frozenset(("test_command_1",)) + + @classproperty + @override + def CHANNEL_NAME(cls) -> str: + """The name of the Discord channel that does not exist.""" + return "channel_name_1" + + def test_str_contains_formatted_message(self) -> None: + """Test that the exception message contains the auto-generated formatted message.""" + assert self._ChannelDoesNotExistErrorSubclass.get_formatted_message( + non_existent_object_identifier=self._ChannelDoesNotExistErrorSubclass.CHANNEL_NAME, + ) in str(self._ChannelDoesNotExistErrorSubclass()) + + def test_channel_name_in_str(self) -> None: + """ + Test that the correct channel name appears in the exception message. + + The correct channel name is the `CHANNEL_NAME` class-property + associated with the given `ChannelDoesNotExistError` subclass. + """ + assert self._ChannelDoesNotExistErrorSubclass.CHANNEL_NAME in str( + self._ChannelDoesNotExistErrorSubclass() + ) + + +class TestRolesChannelDoesNotExistError: + """ + Test case to unit-test the `RolesChannelDoesNotExistError` exception. + + If there are no unit-tests within this test case, + it is because all the functionality of `RolesChannelDoesNotExistError` is inherited + from its parent class so is already unit-tested in the parent class's dedicated test case. + """ + + +class TestGeneralChannelDoesNotExistError: + """ + Test case to unit-test the `GeneralChannelDoesNotExistError` exception. + + If there are no unit-tests within this test case, + it is because all the functionality of `GeneralChannelDoesNotExistError` is inherited + from its parent class so is already unit-tested in the parent class's dedicated test case. + """ From 343cb287a58b1057e7b20fcc6cc40c16c8ff70a6 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 1 Jun 2025 12:34:12 +0000 Subject: [PATCH 06/22] more work --- tests/test_exceptions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index c67bdae8e..be25a29df 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -637,6 +637,10 @@ def test_default_message_without_guild_id(self) -> None: """ assert "given ID" in str(GuildDoesNotExistError()) + def test_error_code(self) -> None: + """Test that the error code is set correctly.""" + assert "E1011" in (GuildDoesNotExistError().ERROR_CODE) + class TestRoleDoesNotExistError: """Test case to unit-test the `RoleDoesNotExistError` exception.""" From 24864fe100b4b61efe0cccf1e1d63a9bc3d55391 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 1 Jun 2025 21:08:38 +0000 Subject: [PATCH 07/22] Fix some stuff --- tests/test_exceptions.py | 44 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index be25a29df..e5c11b969 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -665,6 +665,18 @@ def DEPENDENT_COMMANDS(cls) -> frozenset[str]: """ return frozenset(("test_command_1",)) + @classproperty + @override + def DEPENDENT_TASKS(cls) -> frozenset[str]: + """The set of names of bot tasks that require this Discord entity.""" + return frozenset(("test_task_1",)) + + @classproperty + @override + def DEPENDENT_EVENTS(cls) -> frozenset[str]: + """The set of names of bot events that require this Discord entity.""" + return frozenset(("test_event_1",)) + @classproperty @override def ROLE_NAME(cls) -> str: @@ -688,6 +700,38 @@ def test_role_name_in_str(self) -> None: self._RoleDoesNotExistErrorSubclass() ) + def test_error_code(self) -> None: + """Test that the error code is set correctly.""" + assert self._RoleDoesNotExistErrorSubclass.ERROR_CODE == "E1" + + def test_default_message(self) -> None: + """Test that the default message is correct.""" + assert ( + self._RoleDoesNotExistErrorSubclass.DEFAULT_MESSAGE == ( + 'Role with name "role_name_1" does not exist.' + ) + ) + + def test_init_with_custom_message(self) -> None: + """Test that a custom message is used if provided.""" + msg = "Custom error message" + exc = self._RoleDoesNotExistErrorSubclass(msg) + assert str(exc) == msg + + def test_default_dependant_message(self) -> None: + """Test that the default message is correct.""" + test_exception: RoleDoesNotExistError = self._RoleDoesNotExistErrorSubclass() + assert ( + test_exception.get_formatted_message( + non_existent_object_identifier=test_exception.ROLE_NAME, + ) + == ( + f'"{test_exception.ROLE_NAME}" {test_exception.DOES_NOT_EXIST_TYPE} must exist' + " in order to use the \"/test_command_1\" command, the \"test_task_1\" task" + " and the \"test_event_1\" event." + ) + ) + class TestCommitteeRoleDoesNotExistError: """ From 9d5b82b1dd8daecc02dfbeb8d439bd7eeb93290f Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 1 Jun 2025 22:16:00 +0000 Subject: [PATCH 08/22] Add CommitteeRoleDoesNotExistError tests --- tests/test_exceptions.py | 52 ++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index e5c11b969..b70d0b446 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -7,6 +7,7 @@ from exceptions import ( ChannelDoesNotExistError, + CommitteeRoleDoesNotExistError, DiscordMemberNotInMainGuildError, GuildDoesNotExistError, ImproperlyConfiguredError, @@ -706,10 +707,8 @@ def test_error_code(self) -> None: def test_default_message(self) -> None: """Test that the default message is correct.""" - assert ( - self._RoleDoesNotExistErrorSubclass.DEFAULT_MESSAGE == ( - 'Role with name "role_name_1" does not exist.' - ) + assert self._RoleDoesNotExistErrorSubclass.DEFAULT_MESSAGE == ( + 'Role with name "role_name_1" does not exist.' ) def test_init_with_custom_message(self) -> None: @@ -721,15 +720,12 @@ def test_init_with_custom_message(self) -> None: def test_default_dependant_message(self) -> None: """Test that the default message is correct.""" test_exception: RoleDoesNotExistError = self._RoleDoesNotExistErrorSubclass() - assert ( - test_exception.get_formatted_message( - non_existent_object_identifier=test_exception.ROLE_NAME, - ) - == ( - f'"{test_exception.ROLE_NAME}" {test_exception.DOES_NOT_EXIST_TYPE} must exist' - " in order to use the \"/test_command_1\" command, the \"test_task_1\" task" - " and the \"test_event_1\" event." - ) + assert test_exception.get_formatted_message( + non_existent_object_identifier=test_exception.ROLE_NAME, + ) == ( + f'"{test_exception.ROLE_NAME}" {test_exception.DOES_NOT_EXIST_TYPE} must exist' + ' in order to use the "/test_command_1" command, the "test_task_1" task' + ' and the "test_event_1" event.' ) @@ -742,6 +738,36 @@ class TestCommitteeRoleDoesNotExistError: from its parent class so is already unit-tested in the parent class's dedicated test case. """ + def test_committee_role_does_not_exist_error_code(self) -> None: + """Test that the error code is set correctly.""" + assert "E1021" in (CommitteeRoleDoesNotExistError().ERROR_CODE) + + def test_committee_role_does_not_exist_error_default_message(self) -> None: + """Test that the default message is correct.""" + assert ( + CommitteeRoleDoesNotExistError.DEFAULT_MESSAGE + == 'Role with name "Committee" does not exist.' + ) + + def test_committee_role_does_not_exist_dependent_commands(self) -> None: + """Test that the dependent commands are set correctly.""" + assert ( + frozenset( + ( + "writeroles", + "editmessage", + "induct", + "strike", + "archive", + "delete-all", + "ensure-members-inducted", + "kill", + "committee-handover", + ) + ) + == CommitteeRoleDoesNotExistError.DEPENDENT_COMMANDS + ) + class TestGuestRoleDoesNotExistError: """ From 3007aff0cff468d7c41d71703a6b2e39a7244c0a Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 1 Jun 2025 22:18:18 +0000 Subject: [PATCH 09/22] Add GuestRoleDoesNotExistError tests --- tests/test_exceptions.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index b70d0b446..0ed490d3a 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -9,6 +9,7 @@ ChannelDoesNotExistError, CommitteeRoleDoesNotExistError, DiscordMemberNotInMainGuildError, + GuestRoleDoesNotExistError, GuildDoesNotExistError, ImproperlyConfiguredError, InvalidMessagesJSONFileError, @@ -778,6 +779,30 @@ class TestGuestRoleDoesNotExistError: from its parent class so is already unit-tested in the parent class's dedicated test case. """ + def test_guest_role_does_not_exist_error_code(self) -> None: + """Test that the error code is set correctly.""" + assert "E1022" in (GuestRoleDoesNotExistError().ERROR_CODE) + + def test_guest_role_does_not_exist_error_default_message(self) -> None: + """Test that the default message is correct.""" + assert ( + GuestRoleDoesNotExistError.DEFAULT_MESSAGE + == 'Role with name "Guest" does not exist.' + ) + + def test_guest_role_does_not_exist_dependent_commands(self) -> None: + """Test that the dependent commands are set correctly.""" + assert ( + frozenset({ + "induct", + "stats", + "archive", + "ensure-members-inducted", + "increment-year-channels", + }) + == GuestRoleDoesNotExistError.DEPENDENT_COMMANDS + ) + class TestMemberRoleDoesNotExistError: """ From fe2dea1767f33748325c1f60011178b26477950b Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 1 Jun 2025 22:22:16 +0000 Subject: [PATCH 10/22] Add MemberRole and ArchivistRole tests --- tests/test_exceptions.py | 80 +++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 0ed490d3a..bfee18a80 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -6,6 +6,7 @@ from typed_classproperties import classproperty from exceptions import ( + ArchivistRoleDoesNotExistError, ChannelDoesNotExistError, CommitteeRoleDoesNotExistError, DiscordMemberNotInMainGuildError, @@ -13,6 +14,7 @@ GuildDoesNotExistError, ImproperlyConfiguredError, InvalidMessagesJSONFileError, + MemberRoleDoesNotExistError, MessagesJSONFileMissingKeyError, MessagesJSONFileValueError, RoleDoesNotExistError, @@ -731,13 +733,7 @@ def test_default_dependant_message(self) -> None: class TestCommitteeRoleDoesNotExistError: - """ - Test case to unit-test the `CommitteeRoleDoesNotExistError` exception. - - If there are no unit-tests within this test case, - it is because all the functionality of `CommitteeRoleDoesNotExistError` is inherited - from its parent class so is already unit-tested in the parent class's dedicated test case. - """ + """Test case to unit-test the `CommitteeRoleDoesNotExistError` exception.""" def test_committee_role_does_not_exist_error_code(self) -> None: """Test that the error code is set correctly.""" @@ -771,13 +767,7 @@ def test_committee_role_does_not_exist_dependent_commands(self) -> None: class TestGuestRoleDoesNotExistError: - """ - Test case to unit-test the `GuestRoleDoesNotExistError` exception. - - If there are no unit-tests within this test case, - it is because all the functionality of `GuestRoleDoesNotExistError` is inherited - from its parent class so is already unit-tested in the parent class's dedicated test case. - """ + """Test case to unit-test the `GuestRoleDoesNotExistError` exception.""" def test_guest_role_does_not_exist_error_code(self) -> None: """Test that the error code is set correctly.""" @@ -793,35 +783,59 @@ def test_guest_role_does_not_exist_error_default_message(self) -> None: def test_guest_role_does_not_exist_dependent_commands(self) -> None: """Test that the dependent commands are set correctly.""" assert ( - frozenset({ - "induct", - "stats", - "archive", - "ensure-members-inducted", - "increment-year-channels", - }) + frozenset( + { + "induct", + "stats", + "archive", + "ensure-members-inducted", + "increment-year-channels", + } + ) == GuestRoleDoesNotExistError.DEPENDENT_COMMANDS ) class TestMemberRoleDoesNotExistError: - """ - Test case to unit-test the `MemberRoleDoesNotExistError` exception. + """Test case to unit-test the `MemberRoleDoesNotExistError` exception.""" - If there are no unit-tests within this test case, - it is because all the functionality of `MemberRoleDoesNotExistError` is inherited - from its parent class so is already unit-tested in the parent class's dedicated test case. - """ + def test_member_role_does_not_exist_error_code(self) -> None: + """Test that the error code is set correctly.""" + assert "E1023" in (MemberRoleDoesNotExistError().ERROR_CODE) + + def test_member_role_does_not_exist_error_default_message(self) -> None: + """Test that the default message is correct.""" + assert ( + MemberRoleDoesNotExistError.DEFAULT_MESSAGE + == 'Role with name "Member" does not exist.' + ) + + def test_member_role_does_not_exist_dependent_commands(self) -> None: + """Test that the dependent commands are set correctly.""" + assert frozenset({"makemember", "ensure-members-inducted", "annual-roles-reset"}) == ( + MemberRoleDoesNotExistError.DEPENDENT_COMMANDS + ) class TestArchivistRoleDoesNotExistError: - """ - Test case to unit-test the `ArchivistRoleDoesNotExistError` exception. + """Test case to unit-test the `ArchivistRoleDoesNotExistError` exception.""" - If there are no unit-tests within this test case, - it is because all the functionality of `ArchivistRoleDoesNotExistError` is inherited - from its parent class so is already unit-tested in the parent class's dedicated test case. - """ + def test_archivist_role_does_not_exist_error_code(self) -> None: + """Test that the error code is set correctly.""" + assert "E1024" in (ArchivistRoleDoesNotExistError().ERROR_CODE) + + def test_archivist_role_does_not_exist_error_default_message(self) -> None: + """Test that the default message is correct.""" + assert ( + ArchivistRoleDoesNotExistError.DEFAULT_MESSAGE + == 'Role with name "Archivist" does not exist.' + ) + + def test_archivist_role_does_not_exist_dependent_commands(self) -> None: + """Test that the dependent commands are set correctly.""" + assert frozenset({"archive", "increment-year-channels"}) == ( + ArchivistRoleDoesNotExistError.DEPENDENT_COMMANDS + ) class TestChannelDoesNotExistError: From 5d4bfb0947ee450dc9b5c1d5eca050a7ffe5434b Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 1 Jun 2025 22:32:59 +0000 Subject: [PATCH 11/22] Add more tests --- tests/test_exceptions.py | 52 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index bfee18a80..0122bc7bb 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -6,8 +6,10 @@ from typed_classproperties import classproperty from exceptions import ( + ApplicantRoleDoesNotExistError, ArchivistRoleDoesNotExistError, ChannelDoesNotExistError, + CommitteeElectRoleDoesNotExistError, CommitteeRoleDoesNotExistError, DiscordMemberNotInMainGuildError, GuestRoleDoesNotExistError, @@ -737,7 +739,7 @@ class TestCommitteeRoleDoesNotExistError: def test_committee_role_does_not_exist_error_code(self) -> None: """Test that the error code is set correctly.""" - assert "E1021" in (CommitteeRoleDoesNotExistError().ERROR_CODE) + assert "E1021" in (CommitteeRoleDoesNotExistError.ERROR_CODE) def test_committee_role_does_not_exist_error_default_message(self) -> None: """Test that the default message is correct.""" @@ -766,12 +768,54 @@ def test_committee_role_does_not_exist_dependent_commands(self) -> None: ) +class TestCommitteeElectRoleDoesNotExistError: + """Test case to unit-test the `CommitteeElectRoleDoesNotExistError` exception.""" + + def test_committee_elector_role_does_not_exist_error_code(self) -> None: + """Test that the error code is set correctly.""" + assert "E1026" in (CommitteeElectRoleDoesNotExistError.ERROR_CODE) + + def test_committee_elector_role_does_not_exist_error_default_message(self) -> None: + """Test that the default message is correct.""" + assert ( + CommitteeElectRoleDoesNotExistError.DEFAULT_MESSAGE + == 'Role with name "Committee-Elect" does not exist.' + ) + + def test_committee_elector_role_does_not_exist_dependent_commands(self) -> None: + """Test that the dependent commands are set correctly.""" + assert frozenset({"handover"}) == ( + CommitteeElectRoleDoesNotExistError.DEPENDENT_COMMANDS + ) + + +class TestApplicantRoleDoesNotExistError: + """Test case to unit-test the `ApplicantRoleDoesNotExistError` exception.""" + + def test_applicant_role_does_not_exist_error_code(self) -> None: + """Test that the error code is set correctly.""" + assert "E1025" in (ApplicantRoleDoesNotExistError.ERROR_CODE) + + def test_applicant_role_does_not_exist_error_default_message(self) -> None: + """Test that the default message is correct.""" + assert ( + ApplicantRoleDoesNotExistError.DEFAULT_MESSAGE + == 'Role with name "Applicant" does not exist.' + ) + + def test_applicant_role_does_not_exist_dependent_commands(self) -> None: + """Test that the dependent commands are set correctly.""" + assert frozenset({"make_applicant"}) == ( + ApplicantRoleDoesNotExistError.DEPENDENT_COMMANDS + ) + + class TestGuestRoleDoesNotExistError: """Test case to unit-test the `GuestRoleDoesNotExistError` exception.""" def test_guest_role_does_not_exist_error_code(self) -> None: """Test that the error code is set correctly.""" - assert "E1022" in (GuestRoleDoesNotExistError().ERROR_CODE) + assert "E1022" in (GuestRoleDoesNotExistError.ERROR_CODE) def test_guest_role_does_not_exist_error_default_message(self) -> None: """Test that the default message is correct.""" @@ -801,7 +845,7 @@ class TestMemberRoleDoesNotExistError: def test_member_role_does_not_exist_error_code(self) -> None: """Test that the error code is set correctly.""" - assert "E1023" in (MemberRoleDoesNotExistError().ERROR_CODE) + assert "E1023" in (MemberRoleDoesNotExistError.ERROR_CODE) def test_member_role_does_not_exist_error_default_message(self) -> None: """Test that the default message is correct.""" @@ -822,7 +866,7 @@ class TestArchivistRoleDoesNotExistError: def test_archivist_role_does_not_exist_error_code(self) -> None: """Test that the error code is set correctly.""" - assert "E1024" in (ArchivistRoleDoesNotExistError().ERROR_CODE) + assert "E1024" in (ArchivistRoleDoesNotExistError.ERROR_CODE) def test_archivist_role_does_not_exist_error_default_message(self) -> None: """Test that the default message is correct.""" From 32afdcb78c6ee37c645fb1c954c939a82556ff91 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 1 Jun 2025 22:45:30 +0000 Subject: [PATCH 12/22] even more tests --- tests/test_exceptions.py | 77 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 0122bc7bb..736fbc46e 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -733,6 +733,25 @@ def test_default_dependant_message(self) -> None: ' and the "test_event_1" event.' ) + def test_dependent_commands(self) -> None: + """Test that the dependent commands are set correctly.""" + assert ( + frozenset({"test_command_1"}) + == self._RoleDoesNotExistErrorSubclass.DEPENDENT_COMMANDS + ) + + def test_dependent_tasks(self) -> None: + """Test that the dependent tasks are set correctly.""" + assert ( + frozenset({"test_task_1"}) == self._RoleDoesNotExistErrorSubclass.DEPENDENT_TASKS + ) + + def test_dependent_events(self) -> None: + """Test that the dependent events are set correctly.""" + assert ( + frozenset({"test_event_1"}) == self._RoleDoesNotExistErrorSubclass.DEPENDENT_EVENTS + ) + class TestCommitteeRoleDoesNotExistError: """Test case to unit-test the `CommitteeRoleDoesNotExistError` exception.""" @@ -767,27 +786,39 @@ def test_committee_role_does_not_exist_dependent_commands(self) -> None: == CommitteeRoleDoesNotExistError.DEPENDENT_COMMANDS ) + def test_committee_role_does_not_exist_dependent_tasks(self) -> None: + """Test that the dependent tasks are set correctly.""" + assert frozenset() == (CommitteeRoleDoesNotExistError.DEPENDENT_TASKS) + + def test_committee_role_does_not_exist_dependent_events(self) -> None: + """Test that the dependent events are set correctly.""" + assert frozenset() == (CommitteeRoleDoesNotExistError.DEPENDENT_EVENTS) + class TestCommitteeElectRoleDoesNotExistError: """Test case to unit-test the `CommitteeElectRoleDoesNotExistError` exception.""" - def test_committee_elector_role_does_not_exist_error_code(self) -> None: + def test_committee_elect_role_does_not_exist_error_code(self) -> None: """Test that the error code is set correctly.""" assert "E1026" in (CommitteeElectRoleDoesNotExistError.ERROR_CODE) - def test_committee_elector_role_does_not_exist_error_default_message(self) -> None: + def test_committee_elect_role_does_not_exist_error_default_message(self) -> None: """Test that the default message is correct.""" assert ( CommitteeElectRoleDoesNotExistError.DEFAULT_MESSAGE == 'Role with name "Committee-Elect" does not exist.' ) - def test_committee_elector_role_does_not_exist_dependent_commands(self) -> None: + def test_committee_elect_role_does_not_exist_dependent_commands(self) -> None: """Test that the dependent commands are set correctly.""" assert frozenset({"handover"}) == ( CommitteeElectRoleDoesNotExistError.DEPENDENT_COMMANDS ) + def test_committee_elect_role_does_not_exist_dependent_tasks(self) -> None: + """Test that the dependent tasks are set correctly.""" + assert frozenset() == (CommitteeElectRoleDoesNotExistError.DEPENDENT_TASKS) + class TestApplicantRoleDoesNotExistError: """Test case to unit-test the `ApplicantRoleDoesNotExistError` exception.""" @@ -809,6 +840,14 @@ def test_applicant_role_does_not_exist_dependent_commands(self) -> None: ApplicantRoleDoesNotExistError.DEPENDENT_COMMANDS ) + def test_applicant_role_does_not_exist_dependent_tasks(self) -> None: + """Test that the dependent tasks are set correctly.""" + assert frozenset() == (ApplicantRoleDoesNotExistError.DEPENDENT_TASKS) + + def test_applicant_role_does_not_exist_dependent_events(self) -> None: + """Test that the dependent events are set correctly.""" + assert frozenset() == (ApplicantRoleDoesNotExistError.DEPENDENT_EVENTS) + class TestGuestRoleDoesNotExistError: """Test case to unit-test the `GuestRoleDoesNotExistError` exception.""" @@ -839,6 +878,16 @@ def test_guest_role_does_not_exist_dependent_commands(self) -> None: == GuestRoleDoesNotExistError.DEPENDENT_COMMANDS ) + def test_guest_role_does_not_exist_dependent_tasks(self) -> None: + """Test that the dependent tasks are set correctly.""" + assert frozenset({"send_get_roles_reminders"}) == ( + GuestRoleDoesNotExistError.DEPENDENT_TASKS + ) + + def test_guest_role_does_not_exist_dependent_events(self) -> None: + """Test that the dependent events are set correctly.""" + assert frozenset() == (GuestRoleDoesNotExistError.DEPENDENT_EVENTS) + class TestMemberRoleDoesNotExistError: """Test case to unit-test the `MemberRoleDoesNotExistError` exception.""" @@ -860,6 +909,14 @@ def test_member_role_does_not_exist_dependent_commands(self) -> None: MemberRoleDoesNotExistError.DEPENDENT_COMMANDS ) + def test_member_role_does_not_exist_dependent_tasks(self) -> None: + """Test that the dependent tasks are set correctly.""" + assert frozenset() == (MemberRoleDoesNotExistError.DEPENDENT_TASKS) + + def test_member_role_does_not_exist_dependent_events(self) -> None: + """Test that the dependent events are set correctly.""" + assert frozenset() == (MemberRoleDoesNotExistError.DEPENDENT_EVENTS) + class TestArchivistRoleDoesNotExistError: """Test case to unit-test the `ArchivistRoleDoesNotExistError` exception.""" @@ -881,6 +938,14 @@ def test_archivist_role_does_not_exist_dependent_commands(self) -> None: ArchivistRoleDoesNotExistError.DEPENDENT_COMMANDS ) + def test_archivist_role_does_not_exist_dependent_tasks(self) -> None: + """Test that the dependent tasks are set correctly.""" + assert frozenset() == (ArchivistRoleDoesNotExistError.DEPENDENT_TASKS) + + def test_archivist_role_does_not_exist_dependent_events(self) -> None: + """Test that the dependent events are set correctly.""" + assert frozenset() == (ArchivistRoleDoesNotExistError.DEPENDENT_EVENTS) + class TestChannelDoesNotExistError: """Test case to unit-test the `ChannelDoesNotExistError` exception.""" @@ -928,6 +993,12 @@ def test_channel_name_in_str(self) -> None: self._ChannelDoesNotExistErrorSubclass() ) + def test_channel_does_not_exist_default_message(self) -> None: + """Test that the default message is correct.""" + assert self._ChannelDoesNotExistErrorSubclass.DEFAULT_MESSAGE == ( + 'Channel with name "channel_name_1" does not exist.' + ) + class TestRolesChannelDoesNotExistError: """ From e4a6927082d38a035f724bb2cb8727ac464df7d9 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 1 Jun 2025 23:03:10 +0000 Subject: [PATCH 13/22] Improve tests rather than being silly --- tests/test_exceptions.py | 42 ++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 736fbc46e..50b35880a 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -20,6 +20,7 @@ MessagesJSONFileMissingKeyError, MessagesJSONFileValueError, RoleDoesNotExistError, + RolesChannelDoesNotExistError, ) from exceptions.base import BaseDoesNotExistError, BaseTeXBotError @@ -712,8 +713,9 @@ def test_error_code(self) -> None: def test_default_message(self) -> None: """Test that the default message is correct.""" - assert self._RoleDoesNotExistErrorSubclass.DEFAULT_MESSAGE == ( - 'Role with name "role_name_1" does not exist.' + assert self._RoleDoesNotExistErrorSubclass().message == ( + '"role_name_1" role must exist in order to use the "/test_command_1" command,' + ' the "test_task_1" task and the "test_event_1" event.' ) def test_init_with_custom_message(self) -> None: @@ -767,6 +769,21 @@ def test_committee_role_does_not_exist_error_default_message(self) -> None: == 'Role with name "Committee" does not exist.' ) + assert CommitteeRoleDoesNotExistError().message.startswith( + '"Committee" role must exist in order to use the' + ) + + assert [ + command in CommitteeRoleDoesNotExistError().message + for command in ( + CommitteeRoleDoesNotExistError.DEPENDENT_COMMANDS.union( + CommitteeRoleDoesNotExistError.DEPENDENT_TASKS.union( + CommitteeRoleDoesNotExistError.DEPENDENT_EVENTS + ) + ) + ) + ] + def test_committee_role_does_not_exist_dependent_commands(self) -> None: """Test that the dependent commands are set correctly.""" assert ( @@ -1001,13 +1018,22 @@ def test_channel_does_not_exist_default_message(self) -> None: class TestRolesChannelDoesNotExistError: - """ - Test case to unit-test the `RolesChannelDoesNotExistError` exception. + """Test case to unit-test the `RolesChannelDoesNotExistError` exception.""" - If there are no unit-tests within this test case, - it is because all the functionality of `RolesChannelDoesNotExistError` is inherited - from its parent class so is already unit-tested in the parent class's dedicated test case. - """ + def test_roles_channel_does_not_exist_error_code(self) -> None: + """Test that the error code is set correctly.""" + assert "E1031" in (RolesChannelDoesNotExistError.ERROR_CODE) + + def test_roles_channel_does_not_exist_error_default_message(self) -> None: + """Test that the default message is correct.""" + assert ( + RolesChannelDoesNotExistError().message + == '"#roles" channel must exist in order to use the "/writeroles" command.' + ) + + def test_roles_channel_does_not_exist_dependent_commands(self) -> None: + """Test that the dependent commands are set correctly.""" + assert frozenset({"writeroles"}) == (RolesChannelDoesNotExistError.DEPENDENT_COMMANDS) class TestGeneralChannelDoesNotExistError: From 0d169e1acb64276362f112a904c2e7663b7c317a Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 1 Jun 2025 23:09:24 +0000 Subject: [PATCH 14/22] improve all tests --- tests/test_exceptions.py | 75 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 50b35880a..dc2dbb496 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -826,6 +826,21 @@ def test_committee_elect_role_does_not_exist_error_default_message(self) -> None == 'Role with name "Committee-Elect" does not exist.' ) + assert CommitteeElectRoleDoesNotExistError().message.startswith( + '"Committee-Elect" role must exist in order to use the' + ) + + assert [ + command in CommitteeElectRoleDoesNotExistError().message + for command in ( + CommitteeElectRoleDoesNotExistError.DEPENDENT_COMMANDS.union( + CommitteeElectRoleDoesNotExistError.DEPENDENT_TASKS.union( + CommitteeElectRoleDoesNotExistError.DEPENDENT_EVENTS + ) + ) + ) + ] + def test_committee_elect_role_does_not_exist_dependent_commands(self) -> None: """Test that the dependent commands are set correctly.""" assert frozenset({"handover"}) == ( @@ -851,6 +866,21 @@ def test_applicant_role_does_not_exist_error_default_message(self) -> None: == 'Role with name "Applicant" does not exist.' ) + assert ApplicantRoleDoesNotExistError().message.startswith( + '"Applicant" role must exist in order to use the', + ) + + assert [ + command in ApplicantRoleDoesNotExistError().message + for command in ( + ApplicantRoleDoesNotExistError.DEPENDENT_COMMANDS.union( + ApplicantRoleDoesNotExistError.DEPENDENT_TASKS.union( + ApplicantRoleDoesNotExistError.DEPENDENT_EVENTS + ) + ) + ) + ] + def test_applicant_role_does_not_exist_dependent_commands(self) -> None: """Test that the dependent commands are set correctly.""" assert frozenset({"make_applicant"}) == ( @@ -880,6 +910,21 @@ def test_guest_role_does_not_exist_error_default_message(self) -> None: == 'Role with name "Guest" does not exist.' ) + assert GuestRoleDoesNotExistError().message.startswith( + '"Guest" role must exist in order to use the', + ) + + assert [ + command in GuestRoleDoesNotExistError().message + for command in ( + GuestRoleDoesNotExistError.DEPENDENT_COMMANDS.union( + GuestRoleDoesNotExistError.DEPENDENT_TASKS.union( + GuestRoleDoesNotExistError.DEPENDENT_EVENTS + ) + ) + ) + ] + def test_guest_role_does_not_exist_dependent_commands(self) -> None: """Test that the dependent commands are set correctly.""" assert ( @@ -920,6 +965,21 @@ def test_member_role_does_not_exist_error_default_message(self) -> None: == 'Role with name "Member" does not exist.' ) + assert MemberRoleDoesNotExistError().message.startswith( + '"Member" role must exist in order to use the', + ) + + assert [ + command in MemberRoleDoesNotExistError().message + for command in ( + MemberRoleDoesNotExistError.DEPENDENT_COMMANDS.union( + MemberRoleDoesNotExistError.DEPENDENT_TASKS.union( + MemberRoleDoesNotExistError.DEPENDENT_EVENTS + ) + ) + ) + ] + def test_member_role_does_not_exist_dependent_commands(self) -> None: """Test that the dependent commands are set correctly.""" assert frozenset({"makemember", "ensure-members-inducted", "annual-roles-reset"}) == ( @@ -949,6 +1009,21 @@ def test_archivist_role_does_not_exist_error_default_message(self) -> None: == 'Role with name "Archivist" does not exist.' ) + assert ArchivistRoleDoesNotExistError().message.startswith( + '"Archivist" role must exist in order to use the', + ) + + assert [ + command in ArchivistRoleDoesNotExistError().message + for command in ( + ArchivistRoleDoesNotExistError.DEPENDENT_COMMANDS.union( + ArchivistRoleDoesNotExistError.DEPENDENT_TASKS.union( + ArchivistRoleDoesNotExistError.DEPENDENT_EVENTS + ) + ) + ) + ] + def test_archivist_role_does_not_exist_dependent_commands(self) -> None: """Test that the dependent commands are set correctly.""" assert frozenset({"archive", "increment-year-channels"}) == ( From b0de5a89a20d8918b4a7f51ea044e2c27ee8bc49 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 1 Jun 2025 23:13:29 +0000 Subject: [PATCH 15/22] Add GeneralChannel tests --- tests/test_exceptions.py | 44 ++++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index dc2dbb496..8cbfb7476 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -12,6 +12,7 @@ CommitteeElectRoleDoesNotExistError, CommitteeRoleDoesNotExistError, DiscordMemberNotInMainGuildError, + GeneralChannelDoesNotExistError, GuestRoleDoesNotExistError, GuildDoesNotExistError, ImproperlyConfiguredError, @@ -1112,10 +1113,41 @@ def test_roles_channel_does_not_exist_dependent_commands(self) -> None: class TestGeneralChannelDoesNotExistError: - """ - Test case to unit-test the `GeneralChannelDoesNotExistError` exception. + """Test case to unit-test the `GeneralChannelDoesNotExistError` exception.""" - If there are no unit-tests within this test case, - it is because all the functionality of `GeneralChannelDoesNotExistError` is inherited - from its parent class so is already unit-tested in the parent class's dedicated test case. - """ + def test_general_channel_does_not_exist_error_code(self) -> None: + """Test that the error code is set correctly.""" + assert "E1032" in (GeneralChannelDoesNotExistError.ERROR_CODE) + + def test_general_channel_does_not_exist_error_default_message(self) -> None: + """Test that the default message is correct.""" + assert GeneralChannelDoesNotExistError.DEFAULT_MESSAGE == ( + 'Channel with name "general" does not exist.' + ) + + assert GeneralChannelDoesNotExistError().message.startswith( + '"#general" channel must exist in order to' + ) + + assert [ + command in GeneralChannelDoesNotExistError().message + for command in ( + GeneralChannelDoesNotExistError.DEPENDENT_COMMANDS.union( + GeneralChannelDoesNotExistError.DEPENDENT_TASKS.union( + GeneralChannelDoesNotExistError.DEPENDENT_EVENTS + ) + ) + ) + ] + + def test_general_channel_does_not_exist_dependent_commands(self) -> None: + """Test that the dependent commands are set correctly.""" + assert frozenset({"induct"}) == (GeneralChannelDoesNotExistError.DEPENDENT_COMMANDS) + + def test_general_channel_does_not_exist_dependent_tasks(self) -> None: + """Test that the dependent tasks are set correctly.""" + assert frozenset() == (GeneralChannelDoesNotExistError.DEPENDENT_TASKS) + + def test_general_channel_does_not_exist_dependent_events(self) -> None: + """Test that the dependent events are set correctly.""" + assert frozenset() == (GeneralChannelDoesNotExistError.DEPENDENT_EVENTS) From 4dce6b4dd6a6b84cdc12d0d6a00b1346fad56732 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 1 Jun 2025 23:24:54 +0000 Subject: [PATCH 16/22] More tests --- tests/test_exceptions.py | 41 ++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 8cbfb7476..025c3f71a 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -12,6 +12,7 @@ CommitteeElectRoleDoesNotExistError, CommitteeRoleDoesNotExistError, DiscordMemberNotInMainGuildError, + EveryoneRoleCouldNotBeRetrievedError, GeneralChannelDoesNotExistError, GuestRoleDoesNotExistError, GuildDoesNotExistError, @@ -20,6 +21,7 @@ MemberRoleDoesNotExistError, MessagesJSONFileMissingKeyError, MessagesJSONFileValueError, + RestartRequiredDueToConfigChange, RoleDoesNotExistError, RolesChannelDoesNotExistError, ) @@ -45,6 +47,24 @@ def test_message_when_raised(self, test_exception_message: str) -> None: raise ImproperlyConfiguredError(test_exception_message) +class TestRestartRequiredDueToConfigChange: + """Test case to unit-test the `RestartRequiredDueToConfigChange` exception.""" + + @pytest.mark.parametrize("test_exception_message", ("Error 1 occurred",)) + def test_message(self, test_exception_message: str) -> None: + """Test that the custom error message is used in the `__str__` representation.""" + assert ( + str(RestartRequiredDueToConfigChange(test_exception_message)) + == test_exception_message + ) + + @pytest.mark.parametrize("test_exception_message", ("Error 1 occurred",)) + def test_message_when_raised(self, test_exception_message: str) -> None: + """Test that the custom error message is shown when the exception is raised.""" + with pytest.raises(RestartRequiredDueToConfigChange, match=test_exception_message): + raise RestartRequiredDueToConfigChange(test_exception_message) + + class TestBaseTeXBotError: """Test case to unit-test the `BaseTeXBotError` exception.""" @@ -562,13 +582,22 @@ def test_user_id_in_repr(self, test_user_id: int) -> None: class TestEveryoneRoleCouldNotBeRetrievedError: - """ - Test case to unit-test the `EveryoneRoleCouldNotBeRetrievedError` exception. + """Test case to unit-test the `EveryoneRoleCouldNotBeRetrievedError` exception.""" - If there are no unit-tests within this test case, - it is because all the functionality of `EveryoneRoleCouldNotBeRetrievedError` is inherited - from its parent class so is already unit-tested in the parent class's dedicated test case. - """ + def test_everyone_role_default_message(self) -> None: + """Test that the default message is correct.""" + assert ( + EveryoneRoleCouldNotBeRetrievedError.DEFAULT_MESSAGE + == 'The reference to the "@everyone" role could not be correctly retrieved.' + ) + + assert EveryoneRoleCouldNotBeRetrievedError().message == ( + 'The reference to the "@everyone" role could not be correctly retrieved.' + ) + + def test_everyone_role_error_code(self) -> None: + """Test that the error code is set correctly.""" + assert EveryoneRoleCouldNotBeRetrievedError.ERROR_CODE == "E1042" class TestInvalidMessagesJSONFileError: From bb927c110f4cd5390e8e0cb2e724ce5c78c14905 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Mon, 2 Jun 2025 10:47:45 +0100 Subject: [PATCH 17/22] Update .github/workflows/check-build-deploy.yaml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> --- .github/workflows/check-build-deploy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-build-deploy.yaml b/.github/workflows/check-build-deploy.yaml index bbda5f820..d9d205a83 100644 --- a/.github/workflows/check-build-deploy.yaml +++ b/.github/workflows/check-build-deploy.yaml @@ -165,7 +165,7 @@ jobs: key: pytest|${{steps.store-hashed-python-version.outputs.hashed_python_version}} - name: Run pytest - run: uv run pytest --cov --cov-branch --cov-report=xml --junitxml=junit.xml + run: uv run -- pytest --cov --cov-branch --cov-report=xml --junitxml=junit.xml - name: Upload test results to Codecov if: ${{ !cancelled() }} From 1a6ff7595f5786cee7ca7acea5d30d500764b0bb Mon Sep 17 00:00:00 2001 From: Holly <25277367+Thatsmusic99@users.noreply.github.com> Date: Sun, 15 Jun 2025 13:49:43 +0100 Subject: [PATCH 18/22] Allow committee-elect to update actions (and appear in auto-complete) (#508) Signed-off-by: Holly <25277367+Thatsmusic99@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> --- cogs/committee_actions_tracking.py | 52 ++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/cogs/committee_actions_tracking.py b/cogs/committee_actions_tracking.py index 35c1fee9e..ccea5bc98 100644 --- a/cogs/committee_actions_tracking.py +++ b/cogs/committee_actions_tracking.py @@ -1,5 +1,6 @@ """Contains cog classes for tracking committee-actions.""" +import contextlib import logging import random from enum import Enum @@ -11,6 +12,7 @@ from db.core.models import AssignedCommitteeAction, DiscordMember from exceptions import ( + CommitteeElectRoleDoesNotExistError, CommitteeRoleDoesNotExistError, InvalidActionDescriptionError, InvalidActionTargetError, @@ -129,11 +131,22 @@ async def autocomplete_get_committee_members( except CommitteeRoleDoesNotExistError: return set() + committee_elect_role: discord.Role | None = None + with contextlib.suppress(CommitteeElectRoleDoesNotExistError): + committee_elect_role = await ctx.bot.committee_elect_role + return { discord.OptionChoice( name=f"{member.display_name} ({member.global_name})", value=str(member.id) ) - for member in committee_role.members + for member in ( + set(committee_role.members) + | ( + set(committee_elect_role.members) + if committee_elect_role is not None + else set() + ) + ) if not member.bot } @@ -281,9 +294,7 @@ async def create( required=True, parameter_name="status", ) - @CommandChecks.check_interaction_user_has_committee_role - @CommandChecks.check_interaction_user_in_main_guild - async def update_status( + async def update_status( # NOTE: Committee role check is not present because non-committee can have actions, and need to be able to list their own actions. self, ctx: "TeXBotApplicationContext", action_id: str, status: str ) -> None: """ @@ -561,9 +572,7 @@ async def action_all_committee( default=None, parameter_name="status", ) - @CommandChecks.check_interaction_user_has_committee_role - @CommandChecks.check_interaction_user_in_main_guild - async def list_user_actions( + async def list_user_actions( # NOTE: Committee role check is not present because non-committee can have actions, and need to be able to list their own actions. self, ctx: "TeXBotApplicationContext", *, @@ -575,15 +584,32 @@ async def list_user_actions( Definition and callback of the "/list" command. Takes in a user and lists out their current actions. + If no user is specified, the user issuing the command will be used. + If a user has the committee role, they can list actions for other users. + If a user does not have the committee role, they can only list their own actions. """ - action_member: discord.Member | discord.User + action_member_id = action_member_id.strip() + + action_member: discord.Member | discord.User = ( + await self.bot.get_member_from_str_id(action_member_id) + if action_member_id + else ctx.user + ) - if action_member_id: - action_member = await self.bot.get_member_from_str_id( - action_member_id, + if action_member != ctx.user and not await self.bot.check_user_has_committee_role( + ctx.user + ): + await ctx.respond( + content="Committee role is required to list actions for other users.", + ephemeral=True, ) - else: - action_member = ctx.user + logger.debug( + "User: %s, tried to list actions for user: %s, " + "but did not have the committee role.", + ctx.user, + action_member, + ) + return user_actions: list[AssignedCommitteeAction] From 8f1d49edb4cdd287e6257eb35a89b482ab8f6294 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sat, 21 Jun 2025 22:45:15 +0000 Subject: [PATCH 19/22] merge main --- .github/workflows/check-build-deploy.yaml | 10 +- pyproject.toml | 4 +- uv.lock | 159 +++++++++------------- 3 files changed, 69 insertions(+), 104 deletions(-) diff --git a/.github/workflows/check-build-deploy.yaml b/.github/workflows/check-build-deploy.yaml index 4d86a0b6a..e2a901f64 100644 --- a/.github/workflows/check-build-deploy.yaml +++ b/.github/workflows/check-build-deploy.yaml @@ -44,7 +44,7 @@ jobs: enable-cache: true - name: Install mypy From Locked Dependencies - run: uv sync --no-group dev --group type-check --group test + run: uv sync --no-group dev --group type-check - name: Store Hashed Python Version id: store-hashed-python-version @@ -133,6 +133,8 @@ jobs: pytest: needs: [uv-check] runs-on: ubuntu-latest + permissions: + id-token: write env: UV_NO_SYNC: true UV_FROZEN: true @@ -165,19 +167,19 @@ jobs: key: pytest|${{steps.store-hashed-python-version.outputs.hashed_python_version}} - name: Run pytest - run: uv run -- pytest --cov --cov-branch --cov-report=xml --junitxml=junit.xml + run: uv run pytest --cov --cov-branch --cov-report=xml --junitxml=junit.xml - name: Upload test results to Codecov if: ${{ !cancelled() }} uses: codecov/test-results-action@v1 with: - token: ${{ secrets.CODECOV_TOKEN }} + use_oidc: true - name: Upload coverage report to Codecov uses: codecov/codecov-action@v5 if: ${{ !cancelled() }} with: - token: ${{ secrets.CODECOV_TOKEN }} + use_oidc: true ruff-lint: runs-on: ubuntu-latest diff --git a/pyproject.toml b/pyproject.toml index 83a4c080d..9e3122ec3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ dev = [ { include-group = "test" }, { include-group = "type-check" }, ] -lint-format = ["ccft-pymarkdown>=2.0", "ruff>=0.9"] +lint-format = ["ccft-pymarkdown>=2.0", "ruff>=0.12"] main = [ "asyncstdlib>=3.13", "beautifulsoup4>=4.12", @@ -21,7 +21,7 @@ main = [ "validators>=0.34", ] pre-commit = ["pre-commit>=4.0"] -test = ["gitpython>=3.1.44", "pytest-cov>=6.1.1", "pytest>=8.3"] +test = ["pytest-cov>=6.1", "pytest>=8.3"] type-check = ["django-stubs[compatible-mypy]>=5.1", "mypy>=1.13", "types-beautifulsoup4>=4.12"] [project] # TODO: Remove [project] table once https://github.com/astral-sh/uv/issues/8582 is completed diff --git a/uv.lock b/uv.lock index 3bee3f586..35efd0022 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.12.13" +version = "3.12.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -24,25 +24,25 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/6e/ab88e7cb2a4058bed2f7870276454f85a7c56cd6da79349eb314fc7bbcaa/aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce", size = 7819160, upload-time = "2025-06-14T15:15:41.354Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/84/ea27e6ad14747d8c51afe201fb88a5c8282b6278256d30a6f71f730add88/aiohttp-3.12.12.tar.gz", hash = "sha256:05875595d2483d96cb61fa9f64e75262d7ac6251a7e3c811d8e26f7d721760bd", size = 7818643, upload-time = "2025-06-10T05:22:00.247Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/6a/ce40e329788013cd190b1d62bbabb2b6a9673ecb6d836298635b939562ef/aiohttp-3.12.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0aa580cf80558557285b49452151b9c69f2fa3ad94c5c9e76e684719a8791b73", size = 700491, upload-time = "2025-06-14T15:14:00.048Z" }, - { url = "https://files.pythonhosted.org/packages/28/d9/7150d5cf9163e05081f1c5c64a0cdf3c32d2f56e2ac95db2a28fe90eca69/aiohttp-3.12.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b103a7e414b57e6939cc4dece8e282cfb22043efd0c7298044f6594cf83ab347", size = 475104, upload-time = "2025-06-14T15:14:01.691Z" }, - { url = "https://files.pythonhosted.org/packages/f8/91/d42ba4aed039ce6e449b3e2db694328756c152a79804e64e3da5bc19dffc/aiohttp-3.12.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f64e748e9e741d2eccff9597d09fb3cd962210e5b5716047cbb646dc8fe06f", size = 467948, upload-time = "2025-06-14T15:14:03.561Z" }, - { url = "https://files.pythonhosted.org/packages/99/3b/06f0a632775946981d7c4e5a865cddb6e8dfdbaed2f56f9ade7bb4a1039b/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c955989bf4c696d2ededc6b0ccb85a73623ae6e112439398935362bacfaaf6", size = 1714742, upload-time = "2025-06-14T15:14:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/92/a6/2552eebad9ec5e3581a89256276009e6a974dc0793632796af144df8b740/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d640191016763fab76072c87d8854a19e8e65d7a6fcfcbf017926bdbbb30a7e5", size = 1697393, upload-time = "2025-06-14T15:14:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/d8/9f/bd08fdde114b3fec7a021381b537b21920cdd2aa29ad48c5dffd8ee314f1/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dc507481266b410dede95dd9f26c8d6f5a14315372cc48a6e43eac652237d9b", size = 1752486, upload-time = "2025-06-14T15:14:08.808Z" }, - { url = "https://files.pythonhosted.org/packages/f7/e1/affdea8723aec5bd0959171b5490dccd9a91fcc505c8c26c9f1dca73474d/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a94daa873465d518db073bd95d75f14302e0208a08e8c942b2f3f1c07288a75", size = 1798643, upload-time = "2025-06-14T15:14:10.767Z" }, - { url = "https://files.pythonhosted.org/packages/f3/9d/666d856cc3af3a62ae86393baa3074cc1d591a47d89dc3bf16f6eb2c8d32/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f52420cde4ce0bb9425a375d95577fe082cb5721ecb61da3049b55189e4e6", size = 1718082, upload-time = "2025-06-14T15:14:12.38Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ce/3c185293843d17be063dada45efd2712bb6bf6370b37104b4eda908ffdbd/aiohttp-3.12.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f7df1f620ec40f1a7fbcb99ea17d7326ea6996715e78f71a1c9a021e31b96b8", size = 1633884, upload-time = "2025-06-14T15:14:14.415Z" }, - { url = "https://files.pythonhosted.org/packages/3a/5b/f3413f4b238113be35dfd6794e65029250d4b93caa0974ca572217745bdb/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3062d4ad53b36e17796dce1c0d6da0ad27a015c321e663657ba1cc7659cfc710", size = 1694943, upload-time = "2025-06-14T15:14:16.48Z" }, - { url = "https://files.pythonhosted.org/packages/82/c8/0e56e8bf12081faca85d14a6929ad5c1263c146149cd66caa7bc12255b6d/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8605e22d2a86b8e51ffb5253d9045ea73683d92d47c0b1438e11a359bdb94462", size = 1716398, upload-time = "2025-06-14T15:14:18.589Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f3/33192b4761f7f9b2f7f4281365d925d663629cfaea093a64b658b94fc8e1/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54fbbe6beafc2820de71ece2198458a711e224e116efefa01b7969f3e2b3ddae", size = 1657051, upload-time = "2025-06-14T15:14:20.223Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0b/26ddd91ca8f84c48452431cb4c5dd9523b13bc0c9766bda468e072ac9e29/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:050bd277dfc3768b606fd4eae79dd58ceda67d8b0b3c565656a89ae34525d15e", size = 1736611, upload-time = "2025-06-14T15:14:21.988Z" }, - { url = "https://files.pythonhosted.org/packages/c3/8d/e04569aae853302648e2c138a680a6a2f02e374c5b6711732b29f1e129cc/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2637a60910b58f50f22379b6797466c3aa6ae28a6ab6404e09175ce4955b4e6a", size = 1764586, upload-time = "2025-06-14T15:14:23.979Z" }, - { url = "https://files.pythonhosted.org/packages/ac/98/c193c1d1198571d988454e4ed75adc21c55af247a9fda08236602921c8c8/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e986067357550d1aaa21cfe9897fa19e680110551518a5a7cf44e6c5638cb8b5", size = 1724197, upload-time = "2025-06-14T15:14:25.692Z" }, - { url = "https://files.pythonhosted.org/packages/e7/9e/07bb8aa11eec762c6b1ff61575eeeb2657df11ab3d3abfa528d95f3e9337/aiohttp-3.12.13-cp312-cp312-win32.whl", hash = "sha256:ac941a80aeea2aaae2875c9500861a3ba356f9ff17b9cb2dbfb5cbf91baaf5bf", size = 421771, upload-time = "2025-06-14T15:14:27.364Z" }, - { url = "https://files.pythonhosted.org/packages/52/66/3ce877e56ec0813069cdc9607cd979575859c597b6fb9b4182c6d5f31886/aiohttp-3.12.13-cp312-cp312-win_amd64.whl", hash = "sha256:671f41e6146a749b6c81cb7fd07f5a8356d46febdaaaf07b0e774ff04830461e", size = 447869, upload-time = "2025-06-14T15:14:29.05Z" }, + { url = "https://files.pythonhosted.org/packages/df/e6/df14ec151942818ecc5e685fa8a4b07d3d3d8a9e4a7d2701047c89290551/aiohttp-3.12.12-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:98451ce9ce229d092f278a74a7c2a06b3aa72984673c87796126d7ccade893e9", size = 700494, upload-time = "2025-06-10T05:19:46.18Z" }, + { url = "https://files.pythonhosted.org/packages/4f/dc/7bc6e17adcd7a82b0d0317ad3e792ac22c93fb672077f0eade93e8d70182/aiohttp-3.12.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:adbac7286d89245e1aff42e948503fdc6edf6d5d65c8e305a67c40f6a8fb95f4", size = 475095, upload-time = "2025-06-10T05:19:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/80/fd/c4e8846ad9d9ecdb7d5ba96de65b7bf2c1582f0b2732f2023080c1c05255/aiohttp-3.12.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0728882115bfa85cbd8d0f664c8ccc0cfd5bd3789dd837596785450ae52fac31", size = 467929, upload-time = "2025-06-10T05:19:50.79Z" }, + { url = "https://files.pythonhosted.org/packages/70/40/abebcf5c81f5e65b4379c05929773be2731ce12414264d3e0fe09ee241eb/aiohttp-3.12.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf3b9d9e767f9d0e09fb1a31516410fc741a62cc08754578c40abc497d09540", size = 1714729, upload-time = "2025-06-10T05:19:52.989Z" }, + { url = "https://files.pythonhosted.org/packages/8e/67/4c4f96ef6f16405e7c5205ab3c28852c7e904493b6ddc1c744dda1c97a81/aiohttp-3.12.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c944860e86b9f77a462321a440ccf6fa10f5719bb9d026f6b0b11307b1c96c7b", size = 1697380, upload-time = "2025-06-10T05:19:55.832Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a2/dae9ebea4caa8030170c0237e55fa0960df44b3596a849ab9ea621964054/aiohttp-3.12.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b1979e1f0c98c06fd0cd940988833b102fa3aa56751f6c40ffe85cabc51f6fd", size = 1752474, upload-time = "2025-06-10T05:19:58.007Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/f3d9073565ac7ad5257aaa1490ebfc2f182dfc817d3ccfd38c8ab35b2247/aiohttp-3.12.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:120b7dd084e96cfdad85acea2ce1e7708c70a26db913eabb8d7b417c728f5d84", size = 1798631, upload-time = "2025-06-10T05:20:00.393Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0b/8b1978662274c80c8e4a739d9be1ae9ef25e5ce42b55838d6a9d1a4e3497/aiohttp-3.12.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e58f5ae79649ffa247081c2e8c85e31d29623cf2a3137dda985ae05c9478aae", size = 1718071, upload-time = "2025-06-10T05:20:02.812Z" }, + { url = "https://files.pythonhosted.org/packages/56/aa/35786137db867901b41cb3d2c19c0f4c56dfe581694dba99dec2683d8f8d/aiohttp-3.12.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aa5f049e3e2745b0141f13e5a64e7c48b1a1427ed18bbb7957b348f282fee56", size = 1633871, upload-time = "2025-06-10T05:20:05.127Z" }, + { url = "https://files.pythonhosted.org/packages/63/1d/34d45497dd04d08d662ecda875c44e91d271bbc5d21f4c9e4cbd3ddf7ae2/aiohttp-3.12.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7163cc9cf3722d90f1822f8a38b211e3ae2fc651c63bb55449f03dc1b3ff1d44", size = 1694933, upload-time = "2025-06-10T05:20:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/41e09a4517449eabbb0a7fe6d60f584fe5b21d4bff761197eb0b81e70034/aiohttp-3.12.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ef97c4d035b721de6607f3980fa3e4ef0ec3aca76474b5789b7fac286a8c4e23", size = 1716386, upload-time = "2025-06-10T05:20:09.787Z" }, + { url = "https://files.pythonhosted.org/packages/3a/32/907bd2010b51b70de5314ad707dfc4e898ea0011ff3d678cdf43d6f8980a/aiohttp-3.12.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1c14448d6a86acadc3f7b2f4cc385d1fb390acb6f37dce27f86fe629410d92e3", size = 1657039, upload-time = "2025-06-10T05:20:12.198Z" }, + { url = "https://files.pythonhosted.org/packages/60/27/8d87344a33346dcd39273adc33060aeb135e0ef70d1d6e71a3b03894a8e9/aiohttp-3.12.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a1b6df6255cfc493454c79221183d64007dd5080bcda100db29b7ff181b8832c", size = 1736599, upload-time = "2025-06-10T05:20:14.519Z" }, + { url = "https://files.pythonhosted.org/packages/ca/45/57c7ef1af694a6d0906abab6edde03787c8c6b0cf5d8359b69d1eb0679df/aiohttp-3.12.12-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:60fc7338dfb0626c2927bfbac4785de3ea2e2bbe3d328ba5f3ece123edda4977", size = 1764575, upload-time = "2025-06-10T05:20:16.993Z" }, + { url = "https://files.pythonhosted.org/packages/2a/cc/b1f918cd702efa9ead9d41f89214e9225cda4e5d013d6eed7f1915c17d0a/aiohttp-3.12.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d2afc72207ef4c9d4ca9fcd00689a6a37ef2d625600c3d757b5c2b80c9d0cf9a", size = 1724184, upload-time = "2025-06-10T05:20:19.296Z" }, + { url = "https://files.pythonhosted.org/packages/47/55/089762ee32c2a2e0f523d9ab38c9da2a344cac0e0cc8d16ecf206517ef7e/aiohttp-3.12.12-cp312-cp312-win32.whl", hash = "sha256:8098a48f93b2cbcdb5778e7c9a0e0375363e40ad692348e6e65c3b70d593b27c", size = 421762, upload-time = "2025-06-10T05:20:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/ab/47/151f657e429972916f61399bd52b410e9072d5a2cae1b794f890930e5797/aiohttp-3.12.12-cp312-cp312-win_amd64.whl", hash = "sha256:d1c1879b2e0fc337d7a1b63fe950553c2b9e93c071cf95928aeea1902d441403", size = 447863, upload-time = "2025-06-10T05:20:24.326Z" }, ] [[package]] @@ -59,16 +59,16 @@ wheels = [ [[package]] name = "application-properties" -version = "0.8.3" +version = "0.8.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, { name = "tomli" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/fd/b0274c6585512bbda09ca50887299cefa05584d86eef4f5b80b4d5658d24/application_properties-0.8.3.tar.gz", hash = "sha256:aabb54e26cdc37ba73f5b02ef453b7738cca94468f706c8522876a08164c188a", size = 29763, upload-time = "2025-06-15T23:37:40.369Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/5e/29ec0fa553ee5befc6a1e47eb0c8ffea75eed941251524678700e2d3e747/application_properties-0.8.2.tar.gz", hash = "sha256:e5e6918c8e29ab57175567d51dfa39c00a1d75b3205625559bb02250f50f0420", size = 29595, upload-time = "2024-01-28T23:43:07.403Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/8d/78b9c6364cbeb40c57cea9e3877447b078455036d298dfdc332f8e0054ea/application_properties-0.8.3-py3-none-any.whl", hash = "sha256:7913ac84408051f095e19e72eda3cba627c3a14d25737620dfb05cbedddb992d", size = 17456, upload-time = "2025-06-15T23:37:39.536Z" }, + { url = "https://files.pythonhosted.org/packages/06/27/ea2b77232385ec1c4b5cb766ee13b5a3085ed2fa789d61374e7af36b79e1/application_properties-0.8.2-py3-none-any.whl", hash = "sha256:a4fe684e4d95fc45054d3316acf763a7b0f29342ccea02eee09de53004f0139c", size = 18399, upload-time = "2024-01-28T23:43:05.631Z" }, ] [[package]] @@ -127,11 +127,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.6.15" +version = "2025.4.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, ] [[package]] @@ -338,19 +338,19 @@ wheels = [ [[package]] name = "fonttools" -version = "4.58.4" +version = "4.58.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/5a/1124b2c8cb3a8015faf552e92714040bcdbc145dfa29928891b02d147a18/fonttools-4.58.4.tar.gz", hash = "sha256:928a8009b9884ed3aae17724b960987575155ca23c6f0b8146e400cc9e0d44ba", size = 3525026, upload-time = "2025-06-13T17:25:15.426Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/a9/3319c6ae07fd9dde51064ddc6d82a2b707efad8ed407d700a01091121bbc/fonttools-4.58.2.tar.gz", hash = "sha256:4b491ddbfd50b856e84b0648b5f7941af918f6d32f938f18e62b58426a8d50e2", size = 3524285, upload-time = "2025-06-06T14:50:58.643Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/3c/1d1792bfe91ef46f22a3d23b4deb514c325e73c17d4f196b385b5e2faf1c/fonttools-4.58.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:462211c0f37a278494e74267a994f6be9a2023d0557aaa9ecbcbfce0f403b5a6", size = 2754082, upload-time = "2025-06-13T17:24:24.862Z" }, - { url = "https://files.pythonhosted.org/packages/2a/1f/2b261689c901a1c3bc57a6690b0b9fc21a9a93a8b0c83aae911d3149f34e/fonttools-4.58.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0c7a12fb6f769165547f00fcaa8d0df9517603ae7e04b625e5acb8639809b82d", size = 2321677, upload-time = "2025-06-13T17:24:26.815Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6b/4607add1755a1e6581ae1fc0c9a640648e0d9cdd6591cc2d581c2e07b8c3/fonttools-4.58.4-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d42c63020a922154add0a326388a60a55504629edc3274bc273cd3806b4659f", size = 4896354, upload-time = "2025-06-13T17:24:28.428Z" }, - { url = "https://files.pythonhosted.org/packages/cd/95/34b4f483643d0cb11a1f830b72c03fdd18dbd3792d77a2eb2e130a96fada/fonttools-4.58.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f2b4e6fd45edc6805f5f2c355590b092ffc7e10a945bd6a569fc66c1d2ae7aa", size = 4941633, upload-time = "2025-06-13T17:24:30.568Z" }, - { url = "https://files.pythonhosted.org/packages/81/ac/9bafbdb7694059c960de523e643fa5a61dd2f698f3f72c0ca18ae99257c7/fonttools-4.58.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f155b927f6efb1213a79334e4cb9904d1e18973376ffc17a0d7cd43d31981f1e", size = 4886170, upload-time = "2025-06-13T17:24:32.724Z" }, - { url = "https://files.pythonhosted.org/packages/ae/44/a3a3b70d5709405f7525bb7cb497b4e46151e0c02e3c8a0e40e5e9fe030b/fonttools-4.58.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e38f687d5de97c7fb7da3e58169fb5ba349e464e141f83c3c2e2beb91d317816", size = 5037851, upload-time = "2025-06-13T17:24:35.034Z" }, - { url = "https://files.pythonhosted.org/packages/21/cb/e8923d197c78969454eb876a4a55a07b59c9c4c46598f02b02411dc3b45c/fonttools-4.58.4-cp312-cp312-win32.whl", hash = "sha256:636c073b4da9db053aa683db99580cac0f7c213a953b678f69acbca3443c12cc", size = 2187428, upload-time = "2025-06-13T17:24:36.996Z" }, - { url = "https://files.pythonhosted.org/packages/46/e6/fe50183b1a0e1018e7487ee740fa8bb127b9f5075a41e20d017201e8ab14/fonttools-4.58.4-cp312-cp312-win_amd64.whl", hash = "sha256:82e8470535743409b30913ba2822e20077acf9ea70acec40b10fcf5671dceb58", size = 2236649, upload-time = "2025-06-13T17:24:38.985Z" }, - { url = "https://files.pythonhosted.org/packages/0b/2f/c536b5b9bb3c071e91d536a4d11f969e911dbb6b227939f4c5b0bca090df/fonttools-4.58.4-py3-none-any.whl", hash = "sha256:a10ce13a13f26cbb9f37512a4346bb437ad7e002ff6fa966a7ce7ff5ac3528bd", size = 1114660, upload-time = "2025-06-13T17:25:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/eb/68/7ec64584dc592faf944d540307c3562cd893256c48bb028c90de489e4750/fonttools-4.58.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c6eeaed9c54c1d33c1db928eb92b4e180c7cb93b50b1ee3e79b2395cb01f25e9", size = 2741645, upload-time = "2025-06-06T14:50:08.706Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0c/b327838f63baa7ebdd6db3ffdf5aff638e883f9236d928be4f32c692e1bd/fonttools-4.58.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbe1d9c72b7f981bed5c2a61443d5e3127c1b3aca28ca76386d1ad93268a803f", size = 2311100, upload-time = "2025-06-06T14:50:10.401Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c7/dec024a1c873c79a4db98fe0104755fa62ec2b4518e09d6fda28246c3c9b/fonttools-4.58.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85babe5b3ce2cbe57fc0d09c0ee92bbd4d594fd7ea46a65eb43510a74a4ce773", size = 4815841, upload-time = "2025-06-06T14:50:12.496Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/57c81abad641d6ec9c8b06c99cd28d687cb4849efb6168625b5c6b8f9fa4/fonttools-4.58.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:918a2854537fcdc662938057ad58b633bc9e0698f04a2f4894258213283a7932", size = 4882659, upload-time = "2025-06-06T14:50:14.361Z" }, + { url = "https://files.pythonhosted.org/packages/a5/37/2f8faa2bf8bd1ba016ea86a94c72a5e8ef8ea1c52ec64dada617191f0515/fonttools-4.58.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b379cf05bf776c336a0205632596b1c7d7ab5f7135e3935f2ca2a0596d2d092", size = 4876128, upload-time = "2025-06-06T14:50:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ca/f1caac24ae7028a33f2a95e66c640571ff0ce5cb06c4c9ca1f632e98e22c/fonttools-4.58.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99ab3547a15a5d168c265e139e21756bbae1de04782ac9445c9ef61b8c0a32ce", size = 5027843, upload-time = "2025-06-06T14:50:18.582Z" }, + { url = "https://files.pythonhosted.org/packages/52/6e/3200fa2bafeed748a3017e4e6594751fd50cce544270919265451b21b75c/fonttools-4.58.2-cp312-cp312-win32.whl", hash = "sha256:6764e7a3188ce36eea37b477cdeca602ae62e63ae9fc768ebc176518072deb04", size = 2177374, upload-time = "2025-06-06T14:50:20.454Z" }, + { url = "https://files.pythonhosted.org/packages/55/ab/8f3e726f3f3ef3062ce9bbb615727c55beb11eea96d1f443f79cafca93ee/fonttools-4.58.2-cp312-cp312-win_amd64.whl", hash = "sha256:41f02182a1d41b79bae93c1551855146868b04ec3e7f9c57d6fef41a124e6b29", size = 2226685, upload-time = "2025-06-06T14:50:22.087Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e5/c1cb8ebabb80be76d4d28995da9416816653f8f572920ab5e3d2e3ac8285/fonttools-4.58.2-py3-none-any.whl", hash = "sha256:84f4b0bcfa046254a65ee7117094b4907e22dc98097a220ef108030eb3c15596", size = 1114597, upload-time = "2025-06-06T14:50:56.619Z" }, ] [[package]] @@ -379,30 +379,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, ] -[[package]] -name = "gitdb" -version = "4.0.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "smmap" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, -] - -[[package]] -name = "gitpython" -version = "3.1.44" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "gitdb" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196, upload-time = "2025-01-02T07:32:43.59Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599, upload-time = "2025-01-02T07:32:40.731Z" }, -] - [[package]] name = "identify" version = "2.6.12" @@ -831,27 +807,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.11.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054, upload-time = "2025-06-05T21:00:15.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516, upload-time = "2025-06-05T20:59:32.944Z" }, - { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083, upload-time = "2025-06-05T20:59:37.03Z" }, - { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024, upload-time = "2025-06-05T20:59:39.741Z" }, - { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324, upload-time = "2025-06-05T20:59:42.185Z" }, - { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416, upload-time = "2025-06-05T20:59:44.319Z" }, - { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197, upload-time = "2025-06-05T20:59:46.935Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615, upload-time = "2025-06-05T20:59:49.534Z" }, - { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080, upload-time = "2025-06-05T20:59:51.654Z" }, - { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315, upload-time = "2025-06-05T20:59:54.469Z" }, - { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640, upload-time = "2025-06-05T20:59:56.986Z" }, - { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364, upload-time = "2025-06-05T20:59:59.154Z" }, - { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462, upload-time = "2025-06-05T21:00:01.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028, upload-time = "2025-06-05T21:00:04.06Z" }, - { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992, upload-time = "2025-06-05T21:00:06.249Z" }, - { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944, upload-time = "2025-06-05T21:00:08.459Z" }, - { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669, upload-time = "2025-06-05T21:00:11.147Z" }, - { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928, upload-time = "2025-06-05T21:00:13.758Z" }, +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/90/5255432602c0b196a0da6720f6f76b93eb50baef46d3c9b0025e2f9acbf3/ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c", size = 4376101, upload-time = "2025-06-17T15:19:26.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/fd/b46bb20e14b11ff49dbc74c61de352e0dc07fb650189513631f6fb5fc69f/ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848", size = 10311554, upload-time = "2025-06-17T15:18:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d3/021dde5a988fa3e25d2468d1dadeea0ae89dc4bc67d0140c6e68818a12a1/ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6", size = 11118435, upload-time = "2025-06-17T15:18:49.064Z" }, + { url = "https://files.pythonhosted.org/packages/07/a2/01a5acf495265c667686ec418f19fd5c32bcc326d4c79ac28824aecd6a32/ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0", size = 10466010, upload-time = "2025-06-17T15:18:51.341Z" }, + { url = "https://files.pythonhosted.org/packages/4c/57/7caf31dd947d72e7aa06c60ecb19c135cad871a0a8a251723088132ce801/ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48", size = 10661366, upload-time = "2025-06-17T15:18:53.29Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ba/aa393b972a782b4bc9ea121e0e358a18981980856190d7d2b6187f63e03a/ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807", size = 10173492, upload-time = "2025-06-17T15:18:55.262Z" }, + { url = "https://files.pythonhosted.org/packages/d7/50/9349ee777614bc3062fc6b038503a59b2034d09dd259daf8192f56c06720/ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82", size = 11761739, upload-time = "2025-06-17T15:18:58.906Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/ad459de67c70ec112e2ba7206841c8f4eb340a03ee6a5cabc159fe558b8e/ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c", size = 12537098, upload-time = "2025-06-17T15:19:01.316Z" }, + { url = "https://files.pythonhosted.org/packages/ed/50/15ad9c80ebd3c4819f5bd8883e57329f538704ed57bac680d95cb6627527/ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165", size = 12154122, upload-time = "2025-06-17T15:19:03.727Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/79b91e41bc8cc3e78ee95c87093c6cacfa275c786e53c9b11b9358026b3d/ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2", size = 11363374, upload-time = "2025-06-17T15:19:05.875Z" }, + { url = "https://files.pythonhosted.org/packages/db/c3/82b292ff8a561850934549aa9dc39e2c4e783ab3c21debe55a495ddf7827/ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4", size = 11587647, upload-time = "2025-06-17T15:19:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/2b/42/d5760d742669f285909de1bbf50289baccb647b53e99b8a3b4f7ce1b2001/ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514", size = 10527284, upload-time = "2025-06-17T15:19:10.37Z" }, + { url = "https://files.pythonhosted.org/packages/19/f6/fcee9935f25a8a8bba4adbae62495c39ef281256693962c2159e8b284c5f/ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88", size = 10158609, upload-time = "2025-06-17T15:19:12.286Z" }, + { url = "https://files.pythonhosted.org/packages/37/fb/057febf0eea07b9384787bfe197e8b3384aa05faa0d6bd844b94ceb29945/ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51", size = 11141462, upload-time = "2025-06-17T15:19:15.195Z" }, + { url = "https://files.pythonhosted.org/packages/10/7c/1be8571011585914b9d23c95b15d07eec2d2303e94a03df58294bc9274d4/ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a", size = 11641616, upload-time = "2025-06-17T15:19:17.6Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/b960ab4818f90ff59e571d03c3f992828d4683561095e80f9ef31f3d58b7/ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb", size = 10525289, upload-time = "2025-06-17T15:19:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/34/93/8b16034d493ef958a500f17cda3496c63a537ce9d5a6479feec9558f1695/ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0", size = 11598311, upload-time = "2025-06-17T15:19:21.785Z" }, + { url = "https://files.pythonhosted.org/packages/d0/33/4d3e79e4a84533d6cd526bfb42c020a23256ae5e4265d858bd1287831f7d/ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b", size = 10724946, upload-time = "2025-06-17T15:19:23.952Z" }, ] [[package]] @@ -863,15 +839,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "smmap" -version = "5.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, -] - [[package]] name = "soupsieve" version = "2.7" @@ -899,7 +866,6 @@ source = { virtual = "." } dev = [ { name = "ccft-pymarkdown" }, { name = "django-stubs", extra = ["compatible-mypy"] }, - { name = "gitpython" }, { name = "mypy" }, { name = "pre-commit" }, { name = "pytest" }, @@ -929,7 +895,6 @@ pre-commit = [ { name = "pre-commit" }, ] test = [ - { name = "gitpython" }, { name = "pytest" }, { name = "pytest-cov" }, ] @@ -945,17 +910,16 @@ type-check = [ dev = [ { name = "ccft-pymarkdown", specifier = ">=2.0" }, { name = "django-stubs", extras = ["compatible-mypy"], specifier = ">=5.1" }, - { name = "gitpython", specifier = ">=3.1.44" }, { name = "mypy", specifier = ">=1.13" }, { name = "pre-commit", specifier = ">=4.0" }, { name = "pytest", specifier = ">=8.3" }, - { name = "pytest-cov", specifier = ">=6.1.1" }, - { name = "ruff", specifier = ">=0.9" }, + { name = "pytest-cov", specifier = ">=6.1" }, + { name = "ruff", specifier = ">=0.12" }, { name = "types-beautifulsoup4", specifier = ">=4.12" }, ] lint-format = [ { name = "ccft-pymarkdown", specifier = ">=2.0" }, - { name = "ruff", specifier = ">=0.9" }, + { name = "ruff", specifier = ">=0.12" }, ] main = [ { name = "asyncstdlib", specifier = ">=3.13" }, @@ -973,9 +937,8 @@ main = [ ] pre-commit = [{ name = "pre-commit", specifier = ">=4.0" }] test = [ - { name = "gitpython", specifier = ">=3.1.44" }, { name = "pytest", specifier = ">=8.3" }, - { name = "pytest-cov", specifier = ">=6.1.1" }, + { name = "pytest-cov", specifier = ">=6.1" }, ] type-check = [ { name = "django-stubs", extras = ["compatible-mypy"], specifier = ">=5.1" }, From 5c3104249f5d9892d16917af635546109dffca0d Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 22 Jun 2025 19:25:24 +0000 Subject: [PATCH 20/22] update pyproject.toml --- pyproject.toml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9e3122ec3..1f71e812a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,8 +21,14 @@ main = [ "validators>=0.34", ] pre-commit = ["pre-commit>=4.0"] -test = ["pytest-cov>=6.1", "pytest>=8.3"] -type-check = ["django-stubs[compatible-mypy]>=5.1", "mypy>=1.13", "types-beautifulsoup4>=4.12"] +test = ["pytest-cov>=6.1.1", { include-group = "test-core" }] +test-core = ["gitpython>=3.1.44", "pytest>=8.3"] +type-check = [ + "django-stubs[compatible-mypy]>=5.1", + "mypy>=1.13", + "types-beautifulsoup4>=4.12", + { include-group = "test-core" }, +] [project] # TODO: Remove [project] table once https://github.com/astral-sh/uv/issues/8582 is completed name = "TeX-Bot-Py-V2" @@ -167,7 +173,7 @@ banned-aliases = { "regex" = [ banned-from = ["abc", "re", "regex"] [tool.ruff.lint.per-file-ignores] -"tests/**/test_*.py" = ["S101"] +"tests/**/test_*.py" = ["S101", "S311", "SLF001"] [tool.ruff.lint.flake8-self] extend-ignore-names = ["_base_manager", "_default_manager", "_get_wrap_line_width", "_meta"] @@ -206,13 +212,11 @@ parametrize-values-type = "tuple" [tool.ruff.lint.pyupgrade] keep-runtime-typing = true - [tool.coverage.report] exclude_also = ["if TYPE_CHECKING:"] skip_covered = true sort = "cover" - [tool.pymarkdown] extensions.front-matter.enabled = true mode.strict-config = true From 22f4712a22567f9200a0a8d4b2f917422552c437 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 22 Jun 2025 19:26:21 +0000 Subject: [PATCH 21/22] update lock file --- uv.lock | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index 512e66dd3..8830f2fe9 100644 --- a/uv.lock +++ b/uv.lock @@ -378,6 +378,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, ] +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.44" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196, upload-time = "2025-01-02T07:32:43.59Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599, upload-time = "2025-01-02T07:32:40.731Z" }, +] + [[package]] name = "identify" version = "2.6.12" @@ -850,6 +874,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, +] + [[package]] name = "soupsieve" version = "2.7" @@ -877,6 +910,7 @@ source = { virtual = "." } dev = [ { name = "ccft-pymarkdown" }, { name = "django-stubs", extra = ["compatible-mypy"] }, + { name = "gitpython" }, { name = "mypy" }, { name = "pre-commit" }, { name = "pytest" }, @@ -906,12 +940,19 @@ pre-commit = [ { name = "pre-commit" }, ] test = [ + { name = "gitpython" }, { name = "pytest" }, { name = "pytest-cov" }, ] +test-core = [ + { name = "gitpython" }, + { name = "pytest" }, +] type-check = [ { name = "django-stubs", extra = ["compatible-mypy"] }, + { name = "gitpython" }, { name = "mypy" }, + { name = "pytest" }, { name = "types-beautifulsoup4" }, ] @@ -921,10 +962,11 @@ type-check = [ dev = [ { name = "ccft-pymarkdown", specifier = ">=2.0" }, { name = "django-stubs", extras = ["compatible-mypy"], specifier = ">=5.1" }, + { name = "gitpython", specifier = ">=3.1.44" }, { name = "mypy", specifier = ">=1.13" }, { name = "pre-commit", specifier = ">=4.0" }, { name = "pytest", specifier = ">=8.3" }, - { name = "pytest-cov", specifier = ">=6.1" }, + { name = "pytest-cov", specifier = ">=6.1.1" }, { name = "ruff", specifier = ">=0.12" }, { name = "types-beautifulsoup4", specifier = ">=4.12" }, ] @@ -948,12 +990,19 @@ main = [ ] pre-commit = [{ name = "pre-commit", specifier = ">=4.0" }] test = [ + { name = "gitpython", specifier = ">=3.1.44" }, + { name = "pytest", specifier = ">=8.3" }, + { name = "pytest-cov", specifier = ">=6.1.1" }, +] +test-core = [ + { name = "gitpython", specifier = ">=3.1.44" }, { name = "pytest", specifier = ">=8.3" }, - { name = "pytest-cov", specifier = ">=6.1" }, ] type-check = [ { name = "django-stubs", extras = ["compatible-mypy"], specifier = ">=5.1" }, + { name = "gitpython", specifier = ">=3.1.44" }, { name = "mypy", specifier = ">=1.13" }, + { name = "pytest", specifier = ">=8.3" }, { name = "types-beautifulsoup4", specifier = ">=4.12" }, ] From 871b8252e8101f07f599d233d8c4bd1b3b28efbe Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 22 Jun 2025 19:27:05 +0000 Subject: [PATCH 22/22] fix ruff error --- tests/test_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 599ae0f94..b970157f7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -85,11 +85,11 @@ class TestGenerateInviteURL: @staticmethod def test_url_generates() -> None: """Test that the invite URL generates successfully when valid arguments are passed.""" - DISCORD_BOT_APPLICATION_ID: Final[int] = random.randint( # noqa: S311 + DISCORD_BOT_APPLICATION_ID: Final[int] = random.randint( 10000000000000000, 99999999999999999999, ) - DISCORD_MAIN_GUILD_ID: Final[int] = random.randint( # noqa: S311 + DISCORD_MAIN_GUILD_ID: Final[int] = random.randint( 10000000000000000, 99999999999999999999, )