diff --git a/.flake8 b/.flake8 new file mode 100644 index 000000000..c3c9502cd --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +select = CAR,TXB +extend_exclude = migrations,.venv +require_plugins = flake8-carrot diff --git a/.github/workflows/check-build-deploy.yaml b/.github/workflows/check-build-deploy.yaml index e5e677895..bcd91df6f 100644 --- a/.github/workflows/check-build-deploy.yaml +++ b/.github/workflows/check-build-deploy.yaml @@ -1,11 +1,11 @@ name: Check, Build and Deploy -on: +"on": pull_request: branches: [main] push: branches: [main] - tags: ["v*"] + tags: [v*] jobs: uv-check: @@ -22,13 +22,36 @@ jobs: - name: Check uv.lock (ensure all dependencies up to date) run: uv lock --check - mypy: - needs: [uv-check] - runs-on: ubuntu-latest + flake8: # yamllint disable-line rule:key-ordering env: + UV_FROZEN: true UV_NO_SYNC: true + UV_PYTHON_DOWNLOADS: never + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + + - name: Set Up Python + uses: actions/setup-python@v6 + with: + python-version: 3.14 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + + - name: Run Flake8 + run: uvx --python 3.14 --with flake8-carrot -- flake8 + + mypy: # yamllint disable-line rule:key-ordering + env: UV_FROZEN: true + UV_NO_SYNC: true UV_PYTHON_DOWNLOADS: never + needs: [uv-check] + runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -46,29 +69,36 @@ jobs: - name: Install mypy From Locked Dependencies run: uv sync --no-group dev --group type-check - - name: Store Hashed Python Version - id: store-hashed-python-version + - id: store-hashed-python-version + name: Store Hashed Python Version run: echo "hashed_python_version=$(uv run -- python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_OUTPUT - uses: actions/cache@v4 with: - path: ./.mypy_cache key: mypy|${{steps.store-hashed-python-version.outputs.hashed_python_version}} + path: ./.mypy_cache - name: Run mypy run: uv run -- mypy . # TODO: Add GitHub workflows output format - pre-commit: - runs-on: ubuntu-latest + pre-commit: # yamllint disable-line rule:key-ordering env: - UV_NO_SYNC: true UV_FROZEN: true + UV_NO_SYNC: true UV_PYTHON_DOWNLOADS: never + runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 + - name: Add GB Locale + run: | + sudo apt-get update + sudo apt-get install -y locales + sudo locale-gen en_GB.UTF-8 + shell: bash + - name: Set Up Python uses: actions/setup-python@v6 with: @@ -82,15 +112,15 @@ jobs: - name: Install pre-commit From Locked Dependencies run: uv sync --only-group pre-commit - - name: Store Hashed Python Version - id: store-hashed-python-version + - id: store-hashed-python-version + name: Store Hashed Python Version run: echo "hashed_python_version=$(uv run -- python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_OUTPUT - uses: actions/cache@v4 with: - path: ~/.cache/pre-commit key: pre-commit|${{steps.store-hashed-python-version.outputs.hashed_python_version}}|${{hashFiles('.pre-commit-config.yaml')}} + path: ~/.cache/pre-commit - name: Setup pre-commit Environments run: uv run -- pre-commit install-hooks @@ -98,7 +128,7 @@ jobs: - name: Save pre-commit Checks Which Require Skipping run: | if [[ "${{ github.event_name }}" == "push" && "${{ github.ref_name }}" == "${{ github.event.repository.default_branch }}" ]]; then - echo "SKIP=check-github-workflows,ruff,uv-lock,gitlint-ci" >> $GITHUB_ENV + echo "SKIP=check-github-workflows,ruff-check,uv-lock,gitlint-ci" >> $GITHUB_ENV else echo "SKIP=check-github-workflows,ruff,uv-lock" >> $GITHUB_ENV fi @@ -106,15 +136,15 @@ jobs: - name: Run pre-commit run: uv run -- pre-commit run --all-files --hook-stage manual # TODO: Add GitHub workflows output format - - uses: pre-commit-ci/lite-action@v1.1.0 - if: ${{!cancelled()}} + - if: ${{!cancelled()}} + uses: pre-commit-ci/lite-action@v1.1.0 - pymarkdown: - runs-on: ubuntu-latest + pymarkdown: # yamllint disable-line rule:key-ordering env: - UV_NO_SYNC: true UV_FROZEN: true + UV_NO_SYNC: true UV_PYTHON_DOWNLOADS: never + runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -135,15 +165,15 @@ jobs: - name: Run PyMarkdown scan run: uv run -- pymarkdown scan . - pytest: - needs: [uv-check] - runs-on: ubuntu-latest - permissions: - id-token: write + pytest: # yamllint disable-line rule:key-ordering env: - UV_NO_SYNC: true UV_FROZEN: true + UV_NO_SYNC: true UV_PYTHON_DOWNLOADS: never + needs: [uv-check] + permissions: + id-token: write + runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -161,37 +191,37 @@ jobs: - name: Install pytest From Locked Dependencies run: uv sync --no-group dev --group test - - name: Store Hashed Python Version - id: store-hashed-python-version + - id: store-hashed-python-version + name: Store Hashed Python Version run: echo "hashed_python_version=$(uv run -- python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_OUTPUT - uses: actions/cache@v4 with: - path: ./.pytest_cache key: pytest|${{steps.store-hashed-python-version.outputs.hashed_python_version}} + path: ./.pytest_cache - name: Run pytest run: uv run pytest --cov --cov-branch --cov-report=xml --junitxml=junit.xml - - name: Upload test results to Codecov - if: ${{ !cancelled() }} + - if: ${{ !cancelled() }} + name: Upload test results to Codecov uses: codecov/test-results-action@v1 with: use_oidc: true - - name: Upload coverage report to Codecov + - if: ${{ !cancelled() }} + name: Upload coverage report to Codecov uses: codecov/codecov-action@v5 - if: ${{ !cancelled() }} with: use_oidc: true - ruff-lint: - runs-on: ubuntu-latest + ruff-lint: # yamllint disable-line rule:key-ordering env: - UV_NO_SYNC: true UV_FROZEN: true + UV_NO_SYNC: true UV_PYTHON_DOWNLOADS: never + runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -209,49 +239,48 @@ jobs: - name: Install ruff From Locked Dependencies run: uv sync --only-group lint-format - - name: Store Hashed Python Version - id: store-hashed-python-version + - id: store-hashed-python-version + name: Store Hashed Python Version run: echo "hashed_python_version=$(uv run -- python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_OUTPUT - uses: actions/cache@v4 with: - path: ./.ruff_cache key: ruff|${{steps.store-hashed-python-version.outputs.hashed_python_version}} + path: ./.ruff_cache - name: Run Ruff run: uv run -- ruff check --no-fix --output-format=github - build-and-publish: + build-and-publish: # yamllint disable-line rule:key-ordering + env: + IMAGE_NAME: ${{github.repository}} + REGISTRY: ghcr.io + environment: publish if: | github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'CSSUoB/TeX-Bot-Py-V2' - runs-on: ubuntu-latest - environment: publish needs: [mypy, pre-commit, pymarkdown, pytest, ruff-lint, uv-check] permissions: - contents: read - packages: write attestations: write + contents: read id-token: write - - env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{github.repository}} + packages: write + runs-on: ubuntu-latest steps: - name: Log in to the Container registry uses: docker/login-action@v3.6.0 with: + password: ${{secrets.GITHUB_TOKEN}} registry: ${{env.REGISTRY}} username: ${{github.actor}} - password: ${{secrets.GITHUB_TOKEN}} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Extract metadata (tags, labels) for Docker - id: docker-extract-metadata + - id: docker-extract-metadata + name: Extract metadata (tags, labels) for Docker uses: docker/metadata-action@v5.8.0 with: images: ${{env.REGISTRY}}/${{env.IMAGE_NAME}} @@ -262,33 +291,32 @@ jobs: type=semver,pattern={{major}}.{{minor}} type=semver,pattern=v{{major}},enable=${{!startsWith(github.ref, 'refs/tags/v0.')}} - - name: Build and Publish - id: build-and-publish + - id: build-and-publish + name: Build and Publish uses: docker/build-push-action@v6 with: + labels: ${{steps.docker-extract-metadata.outputs.labels}} push: true tags: ${{steps.docker-extract-metadata.outputs.tags}} - labels: ${{steps.docker-extract-metadata.outputs.labels}} - name: Generate Artifact Attestation uses: actions/attest-build-provenance@v3 with: - subject-name: ${{env.REGISTRY}}/${{env.IMAGE_NAME}} - subject-digest: ${{steps.build-and-publish.outputs.digest}} push-to-registry: true + subject-digest: ${{steps.build-and-publish.outputs.digest}} + subject-name: ${{env.REGISTRY}}/${{env.IMAGE_NAME}} - release: + release: # yamllint disable-line rule:key-ordering + if: github.ref_type == 'tag' needs: [build-and-publish] - runs-on: ubuntu-latest permissions: contents: write id-token: write - - if: github.ref_type == 'tag' + runs-on: ubuntu-latest steps: - name: Create GitHub Release - env: + env: # yamllint disable-line rule:key-ordering GITHUB_TOKEN: ${{ github.token }} run: gh release create '${{ github.ref_name }}' --repo '${{github.repository}}' --verify-tag --generate-notes diff --git a/.github/workflows/pr-auto-updater.yaml b/.github/workflows/pr-auto-updater.yaml index 80e788fe5..b73d0fbdd 100644 --- a/.github/workflows/pr-auto-updater.yaml +++ b/.github/workflows/pr-auto-updater.yaml @@ -1,26 +1,27 @@ name: Automatic PR Updater -on: - push: {} + +"on": + push: ~ jobs: pr-auto-update: - runs-on: ubuntu-latest permissions: - pull-requests: write contents: write + pull-requests: write + runs-on: ubuntu-latest steps: - - name: Generate Access Token + - id: generate-token + name: Generate Access Token uses: actions/create-github-app-token@v2 - id: generate-token with: app-id: ${{ vars.PR_AUTO_UPDATE_CLIENT_ID }} private-key: ${{ secrets.PR_AUTO_UPDATE_PRIVATE_KEY }} - uses: CSSUoB/pr-auto-updater@v2.2.4 - env: + env: # yamllint disable-line rule:key-ordering GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} - PR_FILTER: 'labelled' - PR_LABELS: 'sync' - MERGE_CONFLICT_ACTION: 'label' - MERGE_CONFLICT_LABEL: 'conflict' + MERGE_CONFLICT_ACTION: label + MERGE_CONFLICT_LABEL: conflict + PR_FILTER: labelled + PR_LABELS: sync diff --git a/.github/workflows/prevent-migrations-deletion.yaml b/.github/workflows/prevent-migrations-deletion.yaml index 307f0a4c8..34fc3a083 100644 --- a/.github/workflows/prevent-migrations-deletion.yaml +++ b/.github/workflows/prevent-migrations-deletion.yaml @@ -1,20 +1,19 @@ name: Prevent Database Migration Files Deletion -on: +"on": pull_request_target: branches: [main] jobs: prevent-migrations-deletion: - runs-on: ubuntu-latest - permissions: pull-requests: read + runs-on: ubuntu-latest steps: - name: Prevent migrations files being changed or deleted uses: xalvarez/prevent-file-change-action@v3.0.0 with: - githubToken: ${{ secrets.GITHUB_TOKEN }} - pattern: '.*\/db\/.+\/migrations\/\d{4}\w*\.py$' allowNewFiles: true + githubToken: ${{ secrets.GITHUB_TOKEN }} + pattern: .*\/db\/.+\/migrations\/\d{4}\w*\.py$ diff --git a/.hadolint.yaml b/.hadolint.yaml index 2b39b4d63..4a2024977 100644 --- a/.hadolint.yaml +++ b/.hadolint.yaml @@ -1,5 +1,5 @@ failure-threshold: style -strict-labels: true label-schema: - org.opencontainers.image.source: url org.opencontainers.image.licenses: spdx + org.opencontainers.image.source: url +strict-labels: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5f82b7993..28f348613 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,7 @@ default_install_hook_types: [pre-commit, commit-msg] -default_stages: [pre-commit, pre-merge-commit, manual] - default_language_version: python: python3.12 +default_stages: [pre-commit, pre-merge-commit, manual] repos: - repo: https://github.com/hadolint/hadolint @@ -10,6 +9,12 @@ repos: hooks: - id: hadolint-docker + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.37.1 + hooks: + - id: yamllint + args: [--strict] + - repo: https://github.com/renovatebot/pre-commit-hooks rev: 41.156.1 hooks: @@ -56,7 +61,7 @@ repos: - id: name-tests-test args: [--pytest-test-first] - id: pretty-format-json - args: [--autofix, --indent, '4'] + args: [--autofix, --indent, "4"] - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks rev: v2.15.0 @@ -76,7 +81,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.14.1 hooks: - - id: ruff + - id: ruff-check args: [--fix] - id: ruff-format @@ -90,7 +95,7 @@ repos: - repo: https://github.com/google/yamlfmt rev: v0.19.0 hooks: - - id: yamlfmt - entry: yamlfmt - types: [yaml] + - entry: yamlfmt + id: yamlfmt pass_filenames: true + types: [yaml] diff --git a/.yamlfmt.yaml b/.yamlfmt.yaml index c00dbf7d1..5e988b842 100644 --- a/.yamlfmt.yaml +++ b/.yamlfmt.yaml @@ -1,10 +1,10 @@ -line_ending: lf -gitignore_excludes: true formatter: - indent: 4 - retain_line_breaks_single: true disallow_anchors: true - max_line_length: 95 - trim_trailing_whitespace: true eof_newline: true + indent: 4 + max_line_length: 95 pad_line_comments: 2 + retain_line_breaks_single: true + trim_trailing_whitespace: true +gitignore_excludes: true +line_ending: lf diff --git a/.yamllint.yaml b/.yamllint.yaml new file mode 100644 index 000000000..cd13ce8a8 --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,40 @@ +extends: default + +locale: en_GB.UTF-8 + +rules: + anchors: + forbid-duplicated-anchors: true + forbid-unused-anchors: true + braces: + forbid: true + brackets: + max-spaces-inside-empty: 0 + min-spaces-inside-empty: 0 + document-end: + level: warning + present: false + document-start: + present: false + empty-lines: + max: 2 + empty-values: enable + float-values: + forbid-inf: true + forbid-nan: true + require-numeral-before-decimal: true + indentation: disable + key-ordering: + ignored-keys: + - always_run + - args + - hooks + - jobs + - steps + line-length: disable + new-lines: disable + octal-values: enable + quoted-strings: + allow-quoted-quotes: true + quote-type: double + required: only-when-needed diff --git a/cogs/__init__.py b/cogs/__init__.py index beca94749..0c74de61e 100644 --- a/cogs/__init__.py +++ b/cogs/__init__.py @@ -7,20 +7,20 @@ from typing import TYPE_CHECKING -from .add_users_to_threads_and_channels import AddUsersToThreadsAndChannelsCommandCog +from .add_users_to_threads_and_channels import AddUsersToThreadsAndChannelsCommandsCog from .annual_handover_and_reset import ( AnnualRolesResetCommandCog, AnnualYearChannelsIncrementCommandCog, CommitteeHandoverCommandCog, ) -from .archive import ArchiveCommandCog +from .archive import ArchiveCommandsCog from .check_su_platform_authorisation import ( CheckSUPlatformAuthorisationCommandCog, CheckSUPlatformAuthorisationTaskCog, ) from .command_error import CommandErrorCog from .committee_actions_tracking import ( - CommitteeActionsTrackingContextCommandsCog, + CommitteeActionsTrackingContextCommandCog, CommitteeActionsTrackingSlashCommandsCog, ) from .delete_all import DeleteAllCommandsCog @@ -43,7 +43,7 @@ from .source import SourceCommandCog from .startup import StartupCog from .stats import StatsCommandsCog -from .strike import ManualModerationCog, StrikeCommandCog, StrikeContextCommandsCog +from .strike import ManualModerationCog, StrikeCommandsCog, StrikeContextCommandsCog from .write_roles import WriteRolesCommandCog if TYPE_CHECKING: @@ -94,14 +94,14 @@ def setup(bot: "TeXBot") -> None: """Add all the cogs to the bot, at bot startup.""" cogs: Iterable[type[TeXBotBaseCog]] = ( - AddUsersToThreadsAndChannelsCommandCog, + AddUsersToThreadsAndChannelsCommandsCog, AnnualRolesResetCommandCog, AnnualYearChannelsIncrementCommandCog, - ArchiveCommandCog, + ArchiveCommandsCog, ClearRemindersBacklogTaskCog, CommandErrorCog, CommitteeActionsTrackingSlashCommandsCog, - CommitteeActionsTrackingContextCommandsCog, + CommitteeActionsTrackingContextCommandCog, CommitteeHandoverCommandCog, DeleteAllCommandsCog, EditMessageCommandCog, @@ -125,7 +125,7 @@ def setup(bot: "TeXBot") -> None: SourceCommandCog, StartupCog, StatsCommandsCog, - StrikeCommandCog, + StrikeCommandsCog, StrikeContextCommandsCog, CheckSUPlatformAuthorisationTaskCog, WriteRolesCommandCog, diff --git a/cogs/add_users_to_threads_and_channels.py b/cogs/add_users_to_threads_and_channels.py index 05b49dc34..0eb478965 100644 --- a/cogs/add_users_to_threads_and_channels.py +++ b/cogs/add_users_to_threads_and_channels.py @@ -20,13 +20,13 @@ from utils import TeXBotApplicationContext, TeXBotAutocompleteContext -__all__: "Sequence[str]" = ("AddUsersToThreadsAndChannelsCommandCog",) +__all__: "Sequence[str]" = ("AddUsersToThreadsAndChannelsCommandsCog",) logger: "Final[Logger]" = logging.getLogger("TeX-Bot") -class AddUsersToThreadsAndChannelsCommandCog(TeXBotBaseCog): +class AddUsersToThreadsAndChannelsCommandsCog(TeXBotBaseCog): """Cog for adding users to threads.""" @staticmethod @@ -172,7 +172,7 @@ async def on_thread_create(self, thread: discord.Thread) -> None: committee_elect_role: discord.Role = await self.bot.committee_elect_role if ( - thread.parent is None + thread.parent is None # noqa: CAR180 or thread.parent.category is None or "committee" not in thread.parent.category.name.lower() or not settings["AUTO_ADD_COMMITTEE_TO_THREADS"] diff --git a/cogs/annual_handover_and_reset.py b/cogs/annual_handover_and_reset.py index 355babbe9..75db3fd56 100644 --- a/cogs/annual_handover_and_reset.py +++ b/cogs/annual_handover_and_reset.py @@ -23,6 +23,7 @@ "CommitteeHandoverCommandCog", ) + logger: "Final[Logger]" = logging.getLogger("TeX-Bot") diff --git a/cogs/archive.py b/cogs/archive.py index 0a0029c08..1980a560c 100644 --- a/cogs/archive.py +++ b/cogs/archive.py @@ -17,12 +17,13 @@ from utils import AllChannelTypes, TeXBotApplicationContext, TeXBotAutocompleteContext -__all__: "Sequence[str]" = ("ArchiveCommandCog",) +__all__: "Sequence[str]" = ("ArchiveCommandsCog",) + logger: "Final[Logger]" = logging.getLogger("TeX-Bot") -class ArchiveCommandCog(TeXBotBaseCog): +class ArchiveCommandsCog(TeXBotBaseCog): """Cog class that defines the "/archive" command and its call-back method.""" @staticmethod @@ -89,7 +90,7 @@ async def autocomplete_get_non_archived_channels( discord.OptionChoice(name=channel.name, value=str(channel.id)) for channel in main_guild.channels if ( - not isinstance(channel, discord.CategoryChannel) + not isinstance(channel, discord.CategoryChannel) # noqa: CAR180 and channel.category and "archive" not in channel.category.name.lower() and isinstance(interaction_user, discord.Member) diff --git a/cogs/check_su_platform_authorisation.py b/cogs/check_su_platform_authorisation.py index b47a2253a..e0b8a3cc6 100644 --- a/cogs/check_su_platform_authorisation.py +++ b/cogs/check_su_platform_authorisation.py @@ -28,6 +28,7 @@ "CheckSUPlatformAuthorisationTaskCog", ) + logger: "Final[Logger]" = logging.getLogger("TeX-Bot") REQUEST_HEADERS: "Final[Mapping[str, str]]" = { diff --git a/cogs/command_error.py b/cogs/command_error.py index 669f0c14a..fcac8ee12 100644 --- a/cogs/command_error.py +++ b/cogs/command_error.py @@ -20,6 +20,7 @@ __all__: "Sequence[str]" = ("CommandErrorCog",) + logger: "Final[Logger]" = logging.getLogger("TeX-Bot") diff --git a/cogs/committee_actions_tracking.py b/cogs/committee_actions_tracking.py index 122269b5a..c19643cf6 100644 --- a/cogs/committee_actions_tracking.py +++ b/cogs/committee_actions_tracking.py @@ -29,10 +29,11 @@ __all__: "Sequence[str]" = ( "CommitteeActionsTrackingBaseCog", - "CommitteeActionsTrackingContextCommandsCog", + "CommitteeActionsTrackingContextCommandCog", "CommitteeActionsTrackingSlashCommandsCog", ) + logger: "Final[Logger]" = logging.getLogger("TeX-Bot") @@ -348,7 +349,7 @@ async def update_status( # NOTE: Committee role check is not present because no ) @discord.option( name="description", - description="The description to be used for the action", + description="The description to be used for the action.", input_type=str, required=True, parameter_name="action_description", @@ -402,7 +403,7 @@ async def update_description( ) @discord.option( name="description", - description="The description to be used for the action", + description="The description to be used for the action.", input_type=str, required=True, parameter_name="action_description", @@ -461,11 +462,11 @@ async def action_random_user( @committee_actions.command( name="action-all-committee", - description="Creates an action with the description for every committee member", + description="Creates an action with the description for every committee member.", ) @discord.option( name="description", - description="The description to be used for the actions", + description="The description to be used for the actions.", input_type=str, required=True, parameter_name="action_description", @@ -526,7 +527,7 @@ async def action_all_committee( await ctx.respond(content=response_message) @committee_actions.command( - name="list", description="Lists all actions for a specified user" + name="list", description="Lists all actions for a specified user." ) @discord.option( name="user", @@ -556,9 +557,9 @@ async def action_all_committee( 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", - action_member_id: "None | str", + action_member_id: str | None, ping: bool, # noqa: FBT001 - status: "None | str", + status: str | None, ) -> None: """ Definition and callback of the "/list" command. @@ -838,7 +839,7 @@ async def delete_action(self, ctx: "TeXBotApplicationContext", action_id: str) - await ctx.respond(content=f"Action `{action_description}` successfully deleted.") -class CommitteeActionsTrackingContextCommandsCog(CommitteeActionsTrackingBaseCog): +class CommitteeActionsTrackingContextCommandCog(CommitteeActionsTrackingBaseCog): """Cog class to define the actions tracking message context commands.""" @discord.message_command( diff --git a/cogs/edit_message.py b/cogs/edit_message.py index 66b1405e0..7d2b78a35 100644 --- a/cogs/edit_message.py +++ b/cogs/edit_message.py @@ -56,7 +56,7 @@ async def autocomplete_get_text_channels( parameter_name="str_channel_id", ) @discord.option( - name="message_id", + name="message-id", input_type=str, description="The ID of the message you wish to edit.", required=True, diff --git a/cogs/everest.py b/cogs/everest.py index 7e7856666..b14ea265f 100644 --- a/cogs/everest.py +++ b/cogs/everest.py @@ -18,6 +18,7 @@ __all__: "Sequence[str]" = ("EverestCommandCog",) + logger: "Final[Logger]" = logging.getLogger("TeX-Bot") MOUNT_EVEREST_TOTAL_STEPS: "Final[int]" = 44250 diff --git a/cogs/induct.py b/cogs/induct.py index d4363a825..dd52a9380 100644 --- a/cogs/induct.py +++ b/cogs/induct.py @@ -35,6 +35,7 @@ "InductSlashCommandCog", ) + logger: "Final[Logger]" = logging.getLogger("TeX-Bot") @@ -53,7 +54,7 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) # NOTE: Shortcut accessors are placed at the top of the function so that the exceptions they raise are displayed before any further errors may be sent main_guild: discord.Guild = self.bot.main_guild - if before.guild != main_guild or after.guild != main_guild or before.bot or after.bot: + if before.guild != main_guild or after.guild != main_guild or before.bot or after.bot: # noqa: CAR180 return try: diff --git a/cogs/kill.py b/cogs/kill.py index aafa01609..2ca4e3b37 100644 --- a/cogs/kill.py +++ b/cogs/kill.py @@ -18,6 +18,7 @@ __all__: "Sequence[str]" = ("ConfirmKillView", "KillCommandCog") + logger: "Final[Logger]" = logging.getLogger("TeX-Bot") @@ -82,7 +83,7 @@ async def kill(self, ctx: "TeXBotApplicationContext") -> None: button_interaction: discord.Interaction = await self.bot.wait_for( "interaction", check=lambda interaction: ( - interaction.type == discord.InteractionType.component + interaction.type == discord.InteractionType.component # noqa: CAR180 and interaction.message.id == confirmation_message.id and ((committee_role in interaction.user.roles) if committee_role else True) and "custom_id" in interaction.data diff --git a/cogs/make_applicant.py b/cogs/make_applicant.py index 72045354b..619d77568 100644 --- a/cogs/make_applicant.py +++ b/cogs/make_applicant.py @@ -21,6 +21,7 @@ "MakeApplicantSlashCommandCog", ) + logger: "Final[Logger]" = logging.getLogger("TeX-Bot") diff --git a/cogs/make_member.py b/cogs/make_member.py index 007a04f8e..6139b2099 100644 --- a/cogs/make_member.py +++ b/cogs/make_member.py @@ -32,7 +32,7 @@ if ( settings["_GROUP_FULL_NAME"] and ( - "computer science society" in settings["_GROUP_FULL_NAME"].lower() + "computer science society" in settings["_GROUP_FULL_NAME"].lower() # noqa: CAR180 or "css" in settings["_GROUP_FULL_NAME"].lower() or "uob" in settings["_GROUP_FULL_NAME"].lower() or "university of birmingham" in settings["_GROUP_FULL_NAME"].lower() @@ -69,7 +69,7 @@ class MakeMemberCommandCog(TeXBotBaseCog): if ( settings["_GROUP_FULL_NAME"] and ( - "computer science society" in settings["_GROUP_FULL_NAME"].lower() + "computer science society" in settings["_GROUP_FULL_NAME"].lower() # noqa: CAR180 or "css" in settings["_GROUP_FULL_NAME"].lower() or "uob" in settings["_GROUP_FULL_NAME"].lower() or "university of birmingham" in settings["_GROUP_FULL_NAME"].lower() diff --git a/cogs/send_get_roles_reminders.py b/cogs/send_get_roles_reminders.py index 606f1bfb6..3add49801 100644 --- a/cogs/send_get_roles_reminders.py +++ b/cogs/send_get_roles_reminders.py @@ -28,6 +28,7 @@ __all__: "Sequence[str]" = ("SendGetRolesRemindersTaskCog",) + logger: "Final[Logger]" = logging.getLogger("TeX-Bot") diff --git a/cogs/send_introduction_reminders.py b/cogs/send_introduction_reminders.py index 720bc6169..67480680d 100644 --- a/cogs/send_introduction_reminders.py +++ b/cogs/send_introduction_reminders.py @@ -34,7 +34,8 @@ __all__: "Sequence[str]" = ("SendIntroductionRemindersTaskCog",) -logger: "Logger" = logging.getLogger("TeX-Bot") + +logger: "Final[Logger]" = logging.getLogger("TeX-Bot") class SendIntroductionRemindersTaskCog(TeXBotBaseCog): @@ -131,14 +132,15 @@ async def send_introduction_reminders(self) -> None: continue async for message in member.history(): - MESSAGE_CONTAINS_OPT_IN_OUT_BUTTON: bool = bool( - bool(message.components) + if ( + message.components # noqa: CAR180 and isinstance(message.components[0], discord.ActionRow) and isinstance(message.components[0].children[0], discord.Button) - and message.components[0].children[0].custom_id - == "opt_out_introduction_reminders_button" - ) - if MESSAGE_CONTAINS_OPT_IN_OUT_BUTTON: + and ( + message.components[0].children[0].custom_id + == "opt_out_introduction_reminders_button" + ) + ): await message.edit(view=None) if ( diff --git a/cogs/startup.py b/cogs/startup.py index 0051db07f..d84430c61 100644 --- a/cogs/startup.py +++ b/cogs/startup.py @@ -26,6 +26,7 @@ __all__: "Sequence[str]" = ("StartupCog",) + logger: "Final[Logger]" = logging.getLogger("TeX-Bot") diff --git a/cogs/strike.py b/cogs/strike.py index c51136836..70bfcbe8d 100644 --- a/cogs/strike.py +++ b/cogs/strike.py @@ -40,11 +40,12 @@ "ConfirmStrikeMemberView", "ConfirmStrikesOutOfSyncWithBanView", "ManualModerationCog", - "StrikeCommandCog", + "StrikeCommandsCog", "StrikeContextCommandsCog", "perform_moderation_action", ) + logger: "Final[Logger]" = logging.getLogger("TeX-Bot") FORMATTED_MODERATION_ACTIONS: "Final[Mapping[discord.AuditLogAction, str]]" = { @@ -276,7 +277,7 @@ async def _confirm_perform_moderation_action( button_interaction: discord.Interaction = await self.bot.wait_for( "interaction", check=lambda interaction: ( - interaction.type == discord.InteractionType.component + interaction.type == discord.InteractionType.component # noqa: CAR180 and interaction.user == interaction_user and interaction.channel == button_callback_channel and "custom_id" in interaction.data @@ -586,7 +587,7 @@ async def _confirm_manual_add_strike( # noqa: PLR0915 out_of_sync_ban_button_interaction: discord.Interaction = await self.bot.wait_for( "interaction", check=lambda interaction: ( - interaction.type == discord.InteractionType.component + interaction.type == discord.InteractionType.component # noqa: CAR180 and ( (interaction.user == applied_action_user) if not applied_action_user.bot @@ -683,7 +684,7 @@ async def _confirm_manual_add_strike( # noqa: PLR0915 button_interaction: discord.Interaction = await self.bot.wait_for( "interaction", check=lambda interaction: ( - interaction.type == discord.InteractionType.component + interaction.type == discord.InteractionType.component # noqa: CAR180 and ( (interaction.user == applied_action_user) if not applied_action_user.bot @@ -741,7 +742,7 @@ async def on_member_update(self, before: discord.Member, after: discord.Member) # NOTE: Shortcut accessors are placed at the top of the function so that the exceptions they raise are displayed before any further errors may be sent main_guild: discord.Guild = self.bot.main_guild - if before.guild != main_guild or after.guild != main_guild or before.bot or after.bot: + if before.guild != main_guild or after.guild != main_guild or before.bot or after.bot: # noqa: CAR180 return if not after.timed_out or before.timed_out == after.timed_out: @@ -797,7 +798,7 @@ async def on_member_ban( ) -class StrikeCommandCog(BaseStrikeCog): +class StrikeCommandsCog(BaseStrikeCog): """Cog class that defines the "/strike" command and its call-back method.""" @staticmethod diff --git a/config.py b/config.py index 498e06c46..e38a98180 100644 --- a/config.py +++ b/config.py @@ -31,7 +31,7 @@ from collections.abc import Sequence from collections.abc import Set as AbstractSet from logging import Logger - from typing import IO, Any, ClassVar, Final + from typing import IO, Any, ClassVar, Final, LiteralString __all__: "Sequence[str]" = ( "DEFAULT_STATISTICS_ROLES", @@ -43,14 +43,15 @@ "settings", ) + PROJECT_ROOT: "Final[Path]" = Path(__file__).parent.resolve() -TRUE_VALUES: "Final[AbstractSet[str]]" = {"true", "1", "t", "y", "yes", "on"} -FALSE_VALUES: "Final[AbstractSet[str]]" = {"false", "0", "f", "n", "no", "off"} -VALID_SEND_INTRODUCTION_REMINDERS_VALUES: "Final[AbstractSet[str]]" = ( +TRUE_VALUES: "Final[AbstractSet[LiteralString]]" = {"true", "1", "t", "y", "yes", "on"} +FALSE_VALUES: "Final[AbstractSet[LiteralString]]" = {"false", "0", "f", "n", "no", "off"} +VALID_SEND_INTRODUCTION_REMINDERS_VALUES: "Final[AbstractSet[LiteralString]]" = ( {"once", "interval"} | TRUE_VALUES | FALSE_VALUES ) -DEFAULT_STATISTICS_ROLES: "Final[AbstractSet[str]]" = { +DEFAULT_STATISTICS_ROLES: "Final[AbstractSet[LiteralString]]" = { "Committee", "Committee-Elect", "Student Rep", @@ -69,7 +70,13 @@ "Postdoc", "Quiz Victor", } -LOG_LEVEL_CHOICES: "Final[Sequence[str]]" = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL") +LOG_LEVEL_CHOICES: "Final[Sequence[LiteralString]]" = ( + "DEBUG", + "INFO", + "WARNING", + "ERROR", + "CRITICAL", +) logger: "Final[Logger]" = logging.getLogger("TeX-Bot") discord_logger: "Final[Logger]" = logging.getLogger("discord") @@ -989,7 +996,7 @@ def _setup_env_variables(cls) -> None: def _settings_class_factory() -> type[Settings]: @final - class RuntimeSettings(Settings): + class RuntimeSettings(Settings): # noqa: CAR160 """ Settings class that provides access to all settings values. diff --git a/db/__init__.py b/db/__init__.py index 02debfb16..ced013d78 100644 --- a/db/__init__.py +++ b/db/__init__.py @@ -10,6 +10,7 @@ __all__: "Sequence[str]" = () + os.environ["DJANGO_SETTINGS_MODULE"] = "db._settings" diff --git a/db/core/models/__init__.py b/db/core/models/__init__.py index 3978d25d5..d9be7b405 100644 --- a/db/core/models/__init__.py +++ b/db/core/models/__init__.py @@ -211,7 +211,7 @@ class Meta(TypedModelMeta): # noqa: D106 @override def __setattr__(self, name: str, value: object) -> None: if name == "group_member_id": - if not isinstance(value, str | int): + if not isinstance(value, (str, int)): INVALID_GROUP_MEMBER_ID_TYPE_MESSAGE: Final[str] = ( "group_member_id must be an instance of str or int." ) diff --git a/db/core/models/utils.py b/db/core/models/utils.py index 2c024ee8b..dad752185 100644 --- a/db/core/models/utils.py +++ b/db/core/models/utils.py @@ -33,14 +33,14 @@ class Meta(TypedModelMeta): # noqa: D106 abstract: "ClassVar[bool]" = True @override - def __init__(self, *args: object, **kwargs: object) -> None: + def __init__(self, *args: object, **kwargs: object) -> None: # noqa: CAR150 proxy_fields: dict[str, object] = { field_name: kwargs.pop(field_name) for field_name in set(kwargs.keys()) & self._get_proxy_field_names() } with transaction.atomic(): - super().__init__(*args, **kwargs) + super().__init__(*args, **kwargs) # noqa: CAR151 field_name: str value: object @@ -73,7 +73,7 @@ def update( force_update: bool = False, using: str | None = None, update_fields: "Iterable[str] | None" = None, - **kwargs: object, + **kwargs: object, # noqa: CAR150 ) -> None: """ Change an in-memory object's values, then save it to the database. @@ -127,7 +127,7 @@ async def aupdate( force_update: bool = False, using: str | None = None, update_fields: "Iterable[str] | None" = None, - **kwargs: object, + **kwargs: object, # noqa: CAR150 ) -> None: """ Asynchronously change an in-memory object's values, then save it to the database. diff --git a/main.py b/main.py index ab51865c1..548f1e3cd 100755 --- a/main.py +++ b/main.py @@ -21,6 +21,7 @@ __all__: "Sequence[str]" = ("bot",) + with SuppressTraceback(): config.run_setup() diff --git a/pyproject.toml b/pyproject.toml index c8f363225..9fa873159 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,6 +128,7 @@ allowed-confusables = [ "ᴡ", "ᴢ", ] +external = ["CAR", "TXB"] ignore = [ "C90", "COM812", diff --git a/tests/test_utils.py b/tests/test_utils.py index 7d27e4aee..866335f10 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -12,6 +12,7 @@ __all__: "Sequence[str]" = () + # TODO(CarrotManMatt): Move to stats_tests # noqa: FIX002 # https://github.com/CSSUoB/TeX-Bot-Py-V2/issues/57 # class TestPlotBarChart: diff --git a/utils/__init__.py b/utils/__init__.py index 62c3eade4..48ce5285b 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -33,6 +33,7 @@ "is_running_in_async", ) + if TYPE_CHECKING: type AllChannelTypes = ( discord.VoiceChannel diff --git a/utils/error_capture_decorators.py b/utils/error_capture_decorators.py index c2a821ea6..d0a434978 100644 --- a/utils/error_capture_decorators.py +++ b/utils/error_capture_decorators.py @@ -23,6 +23,7 @@ "capture_strike_tracking_error", ) + if TYPE_CHECKING: type WrapperInputFunc[**P, T_ret] = ( Callable[Concatenate[TeXBotBaseCog, P], Coroutine[object, object, T_ret]] @@ -56,7 +57,7 @@ def capture_error_and_close[**P, T_ret, T_cog: TeXBotBaseCog]( """ # noqa: D401 @functools.wraps(func) - async def wrapper(self: T_cog, /, *args: P.args, **kwargs: P.kwargs) -> T_ret | None: # type: ignore[misc] + async def wrapper(self: T_cog, /, *args: P.args, **kwargs: P.kwargs) -> T_ret | None: # type: ignore[misc] # noqa: CAR150 try: return await func(self, *args, **kwargs) except error_type as error: diff --git a/utils/message_sender_components.py b/utils/message_sender_components.py index 7f5493f91..99de05c11 100644 --- a/utils/message_sender_components.py +++ b/utils/message_sender_components.py @@ -19,6 +19,7 @@ "ResponseMessageSender", ) + if TYPE_CHECKING: class _BaseChannelSendKwargs(TypedDict): diff --git a/utils/tex_bot.py b/utils/tex_bot.py index a45c3c3bc..a004318ee 100644 --- a/utils/tex_bot.py +++ b/utils/tex_bot.py @@ -35,6 +35,7 @@ __all__: "Sequence[str]" = ("TeXBot",) + logger: "Final[Logger]" = logging.getLogger("TeX-Bot") @@ -48,7 +49,7 @@ class TeXBot(discord.Bot): """ @override - def __init__(self, *args: object, **options: object) -> None: + def __init__(self, *args: object, **options: object) -> None: # noqa: CAR150 """Initialise a new Pycord Bot subclass with empty shortcut accessors.""" self._main_guild: discord.Guild | None = None self._committee_role: discord.Role | None = None @@ -64,7 +65,7 @@ def __init__(self, *args: object, **options: object) -> None: self._main_guild_set: bool = False - super().__init__(*args, **options) # type: ignore[no-untyped-call] + super().__init__(*args, **options) # type: ignore[no-untyped-call] # noqa: CAR151 @override async def close(self) -> "NoReturn": # type: ignore[misc] @@ -339,7 +340,7 @@ def group_member_id_type(self) -> str: return ( "UoB Student" if ( - "computer science society" in self.group_full_name.lower() + "computer science society" in self.group_full_name.lower() # noqa: CAR180 or "css" in self.group_full_name.lower() or "uob" in self.group_full_name.lower() or "university of birmingham" in self.group_full_name.lower() @@ -362,7 +363,7 @@ def group_moderation_contact(self) -> str: return ( "the Guild of Students" if ( - "computer science society" in self.group_full_name.lower() + "computer science society" in self.group_full_name.lower() # noqa: CAR180 or "css" in self.group_full_name.lower() or "uob" in self.group_full_name.lower() or "university of birmingham" in self.group_full_name.lower()