From de6826d7282e96a4645005ae7e43c3a0f0001381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=B1=CE=B5=D1=8F=D1=8F=CE=B5?= <47398145+AiroPi@users.noreply.github.com> Date: Fri, 15 Mar 2024 23:02:19 +0100 Subject: [PATCH 1/4] Fix linting, formatting and github actions (#133) --- .github/workflows/ci.yml | 6 +++--- .github/workflows/codeql-analysis.yml | 12 ++++++------ .github/workflows/crowdin-download.yml | 4 ++-- .github/workflows/crowdin-upload.yml | 4 ++-- src/cogs/calculator/calcul.py | 3 +-- src/cogs/config/__init__.py | 6 ++---- src/cogs/game/__init__.py | 15 +++++---------- src/cogs/translate/_types.py | 6 ++---- src/cogs/translate/translator_abc.py | 12 ++++-------- src/commands_exporter.py | 6 ++---- src/core/_config.py | 1 + src/core/caches.py | 6 ++---- src/mybot.py | 2 +- 13 files changed, 33 insertions(+), 50 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10ca47c..363343c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: 3.11.0 cache: pip @@ -25,4 +25,4 @@ jobs: # uses: actions/upload-artifact@v3 # with: # name: pytest_results - # path: junit/test-results.xml + # path: junit/test-results.xml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 93f9a04..0785542 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -6,7 +6,7 @@ on: pull_request: branches: [master] schedule: - - cron: '30 1 * * 0' + - cron: "30 1 * * 0" jobs: codeql_build: @@ -16,24 +16,24 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'python' ] + language: ["python"] permissions: security-events: write steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # This should not be needed - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/crowdin-download.yml b/.github/workflows/crowdin-download.yml index f41e619..653c901 100644 --- a/.github/workflows/crowdin-download.yml +++ b/.github/workflows/crowdin-download.yml @@ -11,7 +11,7 @@ jobs: # secrets cannot be accessed inside an `if` so this needs to be checked in separate job name: dowload steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 ref: master @@ -29,7 +29,7 @@ jobs: env: CROWDIN_API_KEY: ${{ secrets.CROWDIN_API_KEY }} - - uses: tibdex/github-app-token@v1 + - uses: tibdex/github-app-token@v2 id: generate-token with: app_id: ${{ secrets.APP_ID }} diff --git a/.github/workflows/crowdin-upload.yml b/.github/workflows/crowdin-upload.yml index 4dac312..3099332 100644 --- a/.github/workflows/crowdin-upload.yml +++ b/.github/workflows/crowdin-upload.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: master @@ -22,7 +22,7 @@ jobs: echo "deb https://artifacts.crowdin.com/repo/deb/ /" | sudo tee -a /etc/apt/sources.list.d/crowdin.list sudo apt-get update && sudo apt-get install crowdin3 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" cache: pip diff --git a/src/cogs/calculator/calcul.py b/src/cogs/calculator/calcul.py index 09e8878..1db139d 100644 --- a/src/cogs/calculator/calcul.py +++ b/src/cogs/calculator/calcul.py @@ -30,8 +30,7 @@ def regex_builder(sign: str) -> re.Pattern[str]: power = (regex_builder("^"), op.pow) -class UnclosedParentheses(Exception): - ... +class UnclosedParentheses(Exception): ... class Calcul: diff --git a/src/cogs/config/__init__.py b/src/cogs/config/__init__.py index 8866ef8..1dd0699 100644 --- a/src/cogs/config/__init__.py +++ b/src/cogs/config/__init__.py @@ -42,12 +42,10 @@ class Config( ) @cog_property("config_guild") - def guild_cog(self) -> ConfigGuild: - ... + def guild_cog(self) -> ConfigGuild: ... @cog_property("config_bot") - def bot_cog(self) -> ConfigBot: - ... + def bot_cog(self) -> ConfigBot: ... @guild_group.command( name=__("emote"), diff --git a/src/cogs/game/__init__.py b/src/cogs/game/__init__.py index 01da427..b9a51c7 100644 --- a/src/cogs/game/__init__.py +++ b/src/cogs/game/__init__.py @@ -35,24 +35,19 @@ class Game( group_extras={"soon": True}, ): @cog_property("game_connect4") - def connect4_cog(self) -> GameConnect4: - ... + def connect4_cog(self) -> GameConnect4: ... @cog_property("game_rpc") - def rpc_cog(self) -> GameRPC: - ... + def rpc_cog(self) -> GameRPC: ... @cog_property("game_tictactoe") - def tictactoe_cog(self) -> GameTictactoe: - ... + def tictactoe_cog(self) -> GameTictactoe: ... @cog_property("minesweeper") - def minesweeper_cog(self) -> MinesweeperCog: - ... + def minesweeper_cog(self) -> MinesweeperCog: ... @cog_property("game_2048") - def two048_cog(self) -> Two048Cog: - ... + def two048_cog(self) -> Two048Cog: ... @app_commands.command( name=__("connect4"), diff --git a/src/cogs/translate/_types.py b/src/cogs/translate/_types.py index e88db29..d760921 100644 --- a/src/cogs/translate/_types.py +++ b/src/cogs/translate/_types.py @@ -7,10 +7,8 @@ class SendStrategy(Protocol): - async def __call__(self, *, content: str = ..., embeds: Sequence[Embed] = ..., view: ui.View = MISSING) -> Any: - ... + async def __call__(self, *, content: str = ..., embeds: Sequence[Embed] = ..., view: ui.View = MISSING) -> Any: ... class PreSendStrategy(Protocol): - async def __call__(self) -> Any: - ... + async def __call__(self) -> Any: ... diff --git a/src/cogs/translate/translator_abc.py b/src/cogs/translate/translator_abc.py index 12502f4..fe7de18 100644 --- a/src/cogs/translate/translator_abc.py +++ b/src/cogs/translate/translator_abc.py @@ -12,17 +12,13 @@ async def close(self): pass @abstractmethod - async def translate(self, text: str, to: Language, from_: Language | None = None) -> str: - ... + async def translate(self, text: str, to: Language, from_: Language | None = None) -> str: ... @abstractmethod - async def batch_translate(self, texts: Sequence[str], to: Language, from_: Language | None = None) -> list[str]: - ... + async def batch_translate(self, texts: Sequence[str], to: Language, from_: Language | None = None) -> list[str]: ... @abstractmethod - async def detect(self, text: str) -> Language | None: - ... + async def detect(self, text: str) -> Language | None: ... @abstractmethod - async def available_languages(self) -> Languages: - ... + async def available_languages(self) -> Languages: ... diff --git a/src/commands_exporter.py b/src/commands_exporter.py index 4f4b562..53f72b0 100644 --- a/src/commands_exporter.py +++ b/src/commands_exporter.py @@ -79,13 +79,11 @@ def fill_features( child: app_commands.Group | app_commands.Command[Any, ..., Any], features: list[SlashCommand], parent: SlashCommand, -) -> None: - ... +) -> None: ... @overload -def fill_features(child: FeatureCodebaseTypes, features: list[Feature], parent: SlashCommand | None = None) -> None: - ... +def fill_features(child: FeatureCodebaseTypes, features: list[Feature], parent: SlashCommand | None = None) -> None: ... def fill_features( diff --git a/src/core/_config.py b/src/core/_config.py index d0b6d30..53aa98b 100644 --- a/src/core/_config.py +++ b/src/core/_config.py @@ -4,6 +4,7 @@ correctly. A warning should be raised if we try to access config while it is not defined. """ + from __future__ import annotations import logging diff --git a/src/core/caches.py b/src/core/caches.py index 9d71580..a1c545a 100644 --- a/src/core/caches.py +++ b/src/core/caches.py @@ -151,12 +151,10 @@ def __init__(self, max_size: int, init: Sequence[T] | None = None) -> None: self._internal = list(init) @overload - def __getitem__(self, i: SupportsIndex) -> T: - ... + def __getitem__(self, i: SupportsIndex) -> T: ... @overload - def __getitem__(self, i: slice) -> Sequence[T]: - ... + def __getitem__(self, i: slice) -> Sequence[T]: ... def __getitem__(self, i: SupportsIndex | slice) -> T | Sequence[T]: return self._internal.__getitem__(i) diff --git a/src/mybot.py b/src/mybot.py index 59741f0..6a00962 100644 --- a/src/mybot.py +++ b/src/mybot.py @@ -323,7 +323,7 @@ async def get_or_create_db[T: Type[Base]](self, session: AsyncSession, table: T, """Get an object from the database. If it doesn't exist, it is created. It is CREATEd if the guild doesn't exist in the database. """ - guild = await session.get(table, tuple(key.values())) # pyright: ignore[reportGeneralTypeIssues] + guild = await session.get(table, tuple(key.values())) # pyright: ignore[reportArgumentType] if guild is None: guild = table(**key) From d020f69054b083a1bdf7ce17dd1f16ef6b41f67e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=B1=CE=B5=D1=8F=D1=8F=CE=B5?= <47398145+AiroPi@users.noreply.github.com> Date: Fri, 15 Mar 2024 23:08:44 +0100 Subject: [PATCH 2/4] Missing commit of #133 (#134) --- .github/workflows/docker-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index f4e7233..19f1397 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -14,7 +14,7 @@ jobs: os: [ubuntu-latest] steps: - name: Check out the repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Log in to Docker Hub uses: docker/login-action@v2 From 058566a63a0e632e28b4898f8957bd29e97aa9d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=B1=CE=B5=D1=8F=D1=8F=CE=B5?= <47398145+AiroPi@users.noreply.github.com> Date: Sat, 16 Mar 2024 13:06:15 +0100 Subject: [PATCH 3/4] Update peter-evans/create-pull-request@v6 This fix a [bug](https://github.com/peter-evans/create-pull-request/issues/2790) that occurs on PR creation when the "same" pull request already exist. --- .github/workflows/crowdin-download.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/crowdin-download.yml b/.github/workflows/crowdin-download.yml index 653c901..a2e34a7 100644 --- a/.github/workflows/crowdin-download.yml +++ b/.github/workflows/crowdin-download.yml @@ -36,7 +36,7 @@ jobs: private_key: ${{ secrets.APP_PRIVATE_KEY }} - name: Create Pull Request - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: token: ${{ steps.generate-token.outputs.token }} commit-message: "PO files added." From 66f3d12a0c665afc5d945b4f4f984cf97328fe05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=B1=CE=B5=D1=8F=D1=8F=CE=B5?= <47398145+AiroPi@users.noreply.github.com> Date: Sun, 17 Mar 2024 01:04:19 +0100 Subject: [PATCH 4/4] Cleanup the codebase, add documentation, explanations, etc... (#135) This pull request updates and upgrades the entire project from both a documentation and codebase perspective. Its aim is to enhance the project's contribution-friendliness by removing unnecessary elements, renaming components in a more convenient manner, and adding extensive general explanations. Here's a non-exhaustive list of changes: - Removed the "beta"-tagged compose file. This can now be manually adjusted during bot deployment, eliminating the need for a separate file. - Replaced pylint (which wasn't enabled anyway), isort, bandit, and black with ruff. Ruff offers significant speed improvements and additional functionalities. - Ensured compliance with new rules specified in pyproject.toml throughout the entire codebase. - Implemented pip-tools to streamline dependency management. - Updated versions of tools used in Github Actions. - Added useful information in CONTRIBUTING.md - And more... --- .github/CONTRIBUTING.md | 115 +++++++++++++++-- Dockerfile | 4 +- bin/pot-generation.sh | 2 +- compose.debug.yml | 13 ++ docker-compose.yml => compose.yml | 5 +- docker-compose.beta.yml | 3 - docker-compose.dev.yml | 14 -- pyproject.toml | 122 ++++++++++-------- requirements-dev.txt | 5 - requirements.dev.in | 5 + requirements.dev.txt | 59 +++++++++ requirements.in | 13 ++ requirements.txt | 79 ++++++++++-- src/cogs/api.py | 7 +- src/cogs/calculator/__init__.py | 25 +++- src/cogs/calculator/calcul.py | 5 +- src/cogs/clear/__init__.py | 5 +- src/cogs/clear/filters.py | 14 +- src/cogs/config/config_bot.py | 2 +- src/cogs/config/config_guild.py | 2 +- src/cogs/eval.py | 6 +- src/cogs/game/connect4.py | 2 +- src/cogs/game/game_2084.py | 2 +- src/cogs/game/minesweeper/__init__.py | 14 +- src/cogs/game/minesweeper/minesweeper_game.py | 52 ++++---- src/cogs/game/rpc.py | 2 +- src/cogs/game/tictactoe.py | 2 +- src/cogs/help.py | 3 +- src/cogs/ping.py | 2 +- src/cogs/poll/display.py | 16 +-- src/cogs/poll/edit.py | 10 +- src/cogs/poll/vote_menus.py | 9 +- src/cogs/stats.py | 3 +- src/cogs/translate/__init__.py | 11 +- src/cogs/translate/_types.py | 3 +- src/cogs/translate/adapters/libretranslate.py | 7 +- src/cogs/translate/adapters/microsoft.py | 4 +- src/cogs/translate/languages.py | 29 +++-- src/cogs/translate/translator_abc.py | 1 - src/commands_exporter.py | 2 +- src/core/_config.py | 4 +- src/core/_logger.py | 10 +- src/core/_types.py | 5 +- src/core/caches.py | 17 +-- src/core/checkers/app.py | 2 +- src/core/checkers/base.py | 7 +- src/core/checkers/max_concurrency.py | 7 +- src/core/checkers/misc.py | 3 +- src/core/constants.py | 2 + src/core/db/tables.py | 7 +- src/core/error_handler.py | 2 +- src/core/errors.py | 5 +- src/core/extended_commands.py | 20 +-- src/core/response.py | 3 +- src/core/transformers.py | 4 +- src/core/utils.py | 5 +- src/core/view_menus.py | 9 +- src/libraries/libre_translate/_types.py | 8 +- src/libraries/libre_translate/main.py | 2 +- .../microsoft_translation/translator.py | 3 +- src/main.py | 2 +- src/mybot.py | 18 +-- 62 files changed, 531 insertions(+), 293 deletions(-) create mode 100644 compose.debug.yml rename docker-compose.yml => compose.yml (92%) delete mode 100644 docker-compose.beta.yml delete mode 100644 docker-compose.dev.yml delete mode 100644 requirements-dev.txt create mode 100644 requirements.dev.in create mode 100644 requirements.dev.txt create mode 100644 requirements.in diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index df06c06..47dc27c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,33 +1,122 @@ +# Contribution Guidelines + Feel free to contribute to MyBot. -There is mainly three ways to contribute : - 1. Source code contributions - 2. Translations contributions - 3. Financial contributions +There is mainly three ways to contribute: + +1. Source code contributions +2. Translations contributions +3. Financial contributions ## Source code contributions -If you have any suggestion for the bot, and you think you have the ability to do it yourself, start by contacting me through Discord! -It would be a pleasure to count you from the Mybot's contibutors! +If you have any suggestions for the bot and you think you have the ability to do it yourself, consider contacting me through [Discord](https://discord.gg/GRsAy4aUgu)! +It would be a pleasure to count you to the Mybot's contributors! Then, fork this project, make the changes you want, and open a Pull Request on `master`. -Before open a Pull Request, you can start by running `tox`. This will ensure your code respect the style, etc... + +## Good practices and requirements + +### Dependencies + +The project is using [pip-tools](https://github.com/jazzband/pip-tools) to manage its dependencies. The requirements are declared in [requirements.in](/requirements.in) and developers requirements are in [requirements.dev.in](/requirements.dev.in). + +Install the dependencies using `pip-sync` or simply `pip install -r requirements.txt`, as well as the developer dependencies with `pip install -r requirements.dev.txt`. + +If you add or change dependencies, edit the corresponding `.in` file, then use `pip-compile` (`pip-compile requirements.dev.in` for developer deps). + +### Lint, formatting... + +The project use [pyright](https://github.com/microsoft/pyright) for static type checking and [ruff](https://github.com/astral-sh/ruff) for general formatting, import sorting, security scans and static code analyses. + +Please use these tool to avoid Github Actions failure. [tox](https://github.com/tox-dev/tox) can be used to run every checks before committing by running `tox`. + +## Docker watch + +The [compose file](/compose.yml) implements [watch](https://docs.docker.com/compose/file-watch/) to help debugging the code. By running `docker compose watch`, the bot will be executed while observing for files changes or deps updates. This will allow extensions reload, and speed up restarts (it avoids rebuilds). + +## Run the code on debug version + +You can use [debugpy](https://github.com/microsoft/debugpy) easily when running MyBot locally, for dev and debug purposes. +Replace `docker compose` with `docker compose -f compose.yml -f compose.debug.yml` when using Docker Compose. You can then use debugpy with the port `5678`. + +Additionally, this will setup `config.DEBUG` to `True` from the code perspective, which will also set the logger level to `DEBUG`, and expose the PostgresSQL port to the host (`5432`). + +### VSCode debug config + +As an example, here is a json configuration that can be added inside your local `.vscode/launch.py` to use the integrated debugger: +```json +{ + "name": "debug", + "type": "debugpy", + "request": "attach", + "connect": { + "host": "localhost", + "port": 5678 + }, + "pathMappings": [ + { + "localRoot": "${workspaceFolder}/src", + "remoteRoot": "/app" + } + ] +} +``` + +After you run the code in debug mode, click on the "play" icon inside VSCode to attach the debug console. You can then use breakpoints, etc. +To make the restart button actually restart the bot and not just re-attach the debugger, you can add pre&post tasks: +```json + ... + "preLaunchTask": "bot up", + "postDebugTask": "bot restart", +``` +And in `.vscode/tasks.json`, add the tasks: +```json +{ + "label": "bot up", + "type": "shell", + "presentation": { + "reveal": "silent" + }, + "command": "docker compose -f compose.yml -f compose.debug.yml up -d", +}, +{ + "label": "bot restart", + "type": "shell", + "presentation": { + "reveal": "silent" + }, + "command": "docker-compose restart mybot" +} +``` + +If the bot is executed with the up task, you should then use the `watch` command with `--no-up`. +More information here: https://code.visualstudio.com/docs/python/debugging + +## Database revisions + +The project use [alembic](https://github.com/sqlalchemy/alembic) to manage database revisions. +When the bot is started using Docker, `alembic upgrade head` is [automatically executed](https://github.com/mybot-organization/mybot/blob/cleanup/Dockerfile#L30). +To create revisions, you can use [`alembic.sh`](bin/alembic.sh) in the `bin` directory. This script allow you to use the alembic CLI inside the container, and will mount the [`/alembic`](/alembic/) directory. + +If you are unfamiliar with alembic, [`here is some information`](/alembic/README). Check also the [documentation](https://alembic.sqlalchemy.org/en/latest/tutorial.html#create-a-migration-script) as well. ## Translations contributions -MyBot is a multi-language bot! The codebase is in english, which is then translated in several languages. +MyBot is a multi-language bot! The codebase is in English, which is then translated in several languages. Currently, MyBot is translated in : - - French + +- French If you know one of these languages, you can contribute to translations here: https://crowdin.com/project/mybot-discord -If you are able to add a new language to the bot, please contact me on Discord! It would be a pleasure to add a new language to MyBot! +If you want to add a new language to the bot, please contact me on Discord! It would be a pleasure to add a new language to MyBot! However, this language must also be available on the Discord application. -## Financial contributions +## Financial contributions -You can make voluntary donation at https://www.buymeacoffee.com/airopi +You can make voluntary donations at https://www.buymeacoffee.com/airopi ----- +--- Thanks ! diff --git a/Dockerfile b/Dockerfile index 4c5d2cf..37f5e0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,11 +11,13 @@ RUN --mount=type=cache,target=/var/cache/apk/ \ --mount=type=bind,source=./bin/msgfmt.py,target=./msgfmt.py \ : \ && apk add gcc musl-dev linux-headers \ + && pip install -U pip \ && pip install -U -r requirements.txt \ && python ./msgfmt.py ./locale/**/LC_MESSAGES/*.po \ && : FROM python:3.12.0-alpine as base +# https://docs.docker.com/reference/dockerfile/#copy---parents COPY --parents --from=build /opt/venv /app/locale/**/LC_MESSAGES/*.mo / WORKDIR /app COPY ./src ./ @@ -24,7 +26,7 @@ ENV PATH="/opt/venv/bin:$PATH" ENV PYTHONUNBUFFERED=0 -FROM base as prod +FROM base as production CMD ["/bin/sh", "-c", "alembic upgrade head && python ./main.py bot --sync -c ./config.toml"] diff --git a/bin/pot-generation.sh b/bin/pot-generation.sh index 3a9c241..17d1931 100755 --- a/bin/pot-generation.sh +++ b/bin/pot-generation.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash pybabel extract \ - --msgid-bugs-address="contact@mybot-discord.com" \ + --msgid-bugs-address="pi@airopi.dev" \ --project="MyBot" \ --version="1.0" \ -k "_ __" \ diff --git a/compose.debug.yml b/compose.debug.yml new file mode 100644 index 0000000..a3298d1 --- /dev/null +++ b/compose.debug.yml @@ -0,0 +1,13 @@ +version: '3.4' + +services: + mybot: + build: + target: debug + restart: "no" + ports: + - 5678:5678 # for debugpy; see .github/CONTRIBUTING/md + + database: + ports: + - 5432:5432 # publish database port to the host diff --git a/docker-compose.yml b/compose.yml similarity index 92% rename from docker-compose.yml rename to compose.yml index d6693d1..376b3b3 100644 --- a/docker-compose.yml +++ b/compose.yml @@ -4,14 +4,15 @@ services: mybot: image: airopi/mybot:stable build: - context: . dockerfile: ./Dockerfile - target: prod + target: production develop: watch: - action: sync path: ./src target: /app + - action: rebuild + path: ./requirements.txt env_file: - .env tty: true diff --git a/docker-compose.beta.yml b/docker-compose.beta.yml deleted file mode 100644 index edba554..0000000 --- a/docker-compose.beta.yml +++ /dev/null @@ -1,3 +0,0 @@ -services: - mybot: - image: airopi/mybot:beta diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index 1c2b025..0000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -1,14 +0,0 @@ -version: '3.4' - -services: - mybot: - build: - target: debug - restart: "no" - ports: - - 5678:5678 # for debugging - - 8080:8080 - - database: - ports: - - 5432:5432 diff --git a/pyproject.toml b/pyproject.toml index 927156c..2e24dab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,3 @@ -[tool.bandit] -skips = ["B101"] - - -[tool.black] -line-length = 120 - -[tool.isort] -profile = "black" -combine_as_imports = true -combine_star = true -line_length = 120 - [tool.tox] legacy_tox_ini = """ [tox] @@ -19,50 +6,79 @@ skipsdist = true [testenv] deps = -r requirements.txt - -r requirements-dev.txt + -r requirements.dev.txt commands = # pytest - black --check src/ - bandit -r src/ tests/ -c pyproject.toml - isort ./src/ --check + ruff format --check src + ruff check src pyright src/ """ -[tool.pylint] -max-line-length = 120 -allow-reexport-from-package = true -ignore-patterns = [".*.pyi"] -jobs = 4 -unsafe-load-any-extension = false -disable = [ - "missing-module-docstring", - "missing-class-docstring", - "missing-function-docstring", - "abstract-method", - # "arguments-differ", - # "attribute-defined-outside-init", - # "duplicate-code", - # "eq-without-hash", - # "fixme", - # "global-statement", - # "implicit-str-concat", - # "import-error", - # "import-self", - # "import-star-module-level", - # "inconsistent-return-statements", - # "invalid-str-codec", +[tool.ruff] +line-length = 120 +indent-width = 4 +target-version = "py312" +src = ["src"] +exclude = ["bin/msgfmt.py", "alembic/**"] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle Error + "F", # pyflakes + "UP", # pyupgrade + "SIM", # flake8-simplify + "I", # imports + "S", # bandit (security) + "N", # pep8-naming + "ASYNC", # flake8-async + "C4", # flake8-compehensions + "FA", # flake8-future-annotations + "ISC", # flake8-implicit-str-concat + "ICN", # flake8-import-conventions + "G", # flake8-logging-format + "PIE", # flake8-pie + "PYI", # flake8-pyi + "RSE", # flake8-raise + "SLOT", # flake8-slots + "INT", # flake8-gettext + "TRY", # tryceratops + "FLY", # flynt + "PERF", # Perflint + "FURB", # refurb + "LOG", # flake8-logging + "RUF", # Ruff-specific-rules + # "ERA", # locate commented codes + # "FIX", # locate TODOs and FIXME + # "PTH", # flake8-use-pathlib (maybe todo ?) + # "TID", # flake8-tidy-imports (maybe todo ?) + # "SLF", # flake8-self (managed by pyright) + # "RET", # flake8-return + # "Q", # flake8-quotes + # "T20", # flake8-print + # "DTZ", # flake8-datetimez (TODO) + # "B", # flake8-bugbear +] + +ignore = [ + "E501", # line too long (we relate on the formater) + "N818", # Error suffix for exceptions names + "PIE796", # Enum contains duplicate value + "TRY003", # Avoid specifying long messages outsides the exception class + "ISC001", # To avoid conflicts with the formatter ] -argument-rgx = "^[a-z_][a-z0-9_]{0,30}$" -attr-rgx = "^[a-z_][a-z0-9_]{0,30}$" -function-rgx = "^[a-z_][a-z0-9_]{0,30}$" -method-rgx = '[a-z_][a-z0-9_]{0,30}$' -variable-rgx = '[a-z_][a-z0-9_]{0,30}$' -ignore-long-lines = '''(?x)( -^\s*(\#\ )??$| -^\s*(from\s+\S+\s+)?import\s+.+$)''' -indent-string = ' ' -dummy-variables-rgx = '^\*{0,2}(_$|unused_|dummy_)' -callbacks = ['cb_', '_cb'] +dummy-variable-rgx = '^\*{0,2}(_$|__$|unused_|dummy_)' + +[tool.ruff.lint.pyflakes] +extend-generics = ["core.Menu"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false + +[tool.ruff.lint.isort] +combine-as-imports = true -# [tool.pyright] -# reportImportCycles = false +[tool.setuptools.dynamic] +dependencies = { file = ["requirements.in"] } +optional-dependencies.dev = { file = ["requirements.dev.in"] } diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 3c77292..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,5 +0,0 @@ -black -bandit -tox -pyright -isort diff --git a/requirements.dev.in b/requirements.dev.in new file mode 100644 index 0000000..4b51f58 --- /dev/null +++ b/requirements.dev.in @@ -0,0 +1,5 @@ +tox +pyright +pip-tools +debugpy +ruff diff --git a/requirements.dev.txt b/requirements.dev.txt new file mode 100644 index 0000000..8939731 --- /dev/null +++ b/requirements.dev.txt @@ -0,0 +1,59 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile requirements.dev.in +# +build==1.1.1 + # via pip-tools +cachetools==5.3.3 + # via tox +chardet==5.2.0 + # via tox +click==8.1.7 + # via pip-tools +colorama==0.4.6 + # via tox +debugpy==1.8.1 + # via -r requirements.dev.in +distlib==0.3.8 + # via virtualenv +filelock==3.13.1 + # via + # tox + # virtualenv +nodeenv==1.8.0 + # via pyright +packaging==24.0 + # via + # build + # pyproject-api + # tox +pip-tools==7.4.1 + # via -r requirements.dev.in +platformdirs==4.2.0 + # via + # tox + # virtualenv +pluggy==1.4.0 + # via tox +pyproject-api==1.6.1 + # via tox +pyproject-hooks==1.0.0 + # via + # build + # pip-tools +pyright==1.1.354 + # via -r requirements.dev.in +ruff==0.3.3 + # via -r requirements.dev.in +tox==4.14.1 + # via -r requirements.dev.in +virtualenv==20.25.1 + # via tox +wheel==0.43.0 + # via pip-tools + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/requirements.in b/requirements.in new file mode 100644 index 0000000..2117bbd --- /dev/null +++ b/requirements.in @@ -0,0 +1,13 @@ +wheel +asyncpg +sqlalchemy[asyncio] +typing_extensions +discord.py +click +psutil +alembic +two048 # a personal library +topggpy==2.0.0a0 +lingua-language-detector==1.3.4 # WAITFOR: https://github.com/pemistahl/lingua-py/issues/213 +aiohttp +python-dateutil diff --git a/requirements.txt b/requirements.txt index a9a0c45..a658076 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,68 @@ -wheel -asyncpg -sqlalchemy[asyncio] -typing_extensions -discord.py -click -psutil -alembic -two048 # a personal library +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile +# +aiohttp==3.9.3 + # via + # -r requirements.in + # discord-py + # topggpy +aiosignal==1.3.1 + # via aiohttp +alembic==1.13.1 + # via -r requirements.in +asyncpg==0.29.0 + # via -r requirements.in +attrs==23.2.0 + # via aiohttp +click==8.1.7 + # via -r requirements.in +discord-py==2.3.2 + # via -r requirements.in +frozenlist==1.4.1 + # via + # aiohttp + # aiosignal +greenlet==3.0.3 + # via sqlalchemy +idna==3.6 + # via yarl +lingua-language-detector==1.3.4 + # via -r requirements.in +mako==1.3.2 + # via alembic +markupsafe==2.1.5 + # via mako +multidict==6.0.5 + # via + # aiohttp + # yarl +numpy==1.26.4 + # via lingua-language-detector +psutil==5.9.8 + # via -r requirements.in +python-dateutil==2.9.0.post0 + # via -r requirements.in +regex==2023.12.25 + # via lingua-language-detector +six==1.16.0 + # via python-dateutil +sqlalchemy[asyncio]==2.0.28 + # via + # -r requirements.in + # alembic topggpy==2.0.0a0 -lingua-language-detector -aiohttp -python-dateutil + # via -r requirements.in +two048==1.0.2 + # via -r requirements.in +typing-extensions==4.10.0 + # via + # -r requirements.in + # alembic + # sqlalchemy +wheel==0.43.0 + # via -r requirements.in +yarl==1.9.4 + # via aiohttp diff --git a/src/cogs/api.py b/src/cogs/api.py index 5584d97..79fcc4b 100644 --- a/src/cogs/api.py +++ b/src/cogs/api.py @@ -1,9 +1,10 @@ from __future__ import annotations import logging +from collections.abc import Awaitable, Callable from functools import partial from os import getpid -from typing import TYPE_CHECKING, Awaitable, Callable, Concatenate, ParamSpec, TypeVar, cast +from typing import TYPE_CHECKING, Concatenate, ParamSpec, TypeVar, cast from aiohttp import hdrs, web from psutil import Process @@ -49,7 +50,7 @@ async def start(self) -> None: await self.bot.wait_until_ready() await self.runner.setup() - site = web.TCPSite(self.runner, "0.0.0.0", 8080) # nosec : B104 # in a docker container + site = web.TCPSite(self.runner, "0.0.0.0", 8080) # noqa: S104 # in a docker container await site.start() async def cog_unload(self) -> None: @@ -59,7 +60,7 @@ async def cog_unload(self) -> None: @route(hdrs.METH_GET, "/memory") async def test(self, request: web.Request): rss = cast(int, Process(getpid()).memory_info().rss) # pyright: ignore[reportUnknownMemberType] - return web.Response(text=f"{round(rss/1024/1024, 2)} MB") + return web.Response(text=f"{round(rss / 1024 / 1024, 2)} MB") async def setup(bot: MyBot): diff --git a/src/cogs/calculator/__init__.py b/src/cogs/calculator/__init__.py index fcc9b5e..fd37d94 100644 --- a/src/cogs/calculator/__init__.py +++ b/src/cogs/calculator/__init__.py @@ -13,7 +13,6 @@ from discord.app_commands import locale_str as __ from core import ExtendedCog -from core.i18n import _ from .calcul import Calcul, UnclosedParentheses @@ -28,11 +27,25 @@ def display_calcul(calcul: Calcul) -> str: if calcul.just_calculated: - display = f"> ```py\n" f"> {calcul.expr} =\n" f"> {calcul.answer: <41}\n" f"> ```" + # fmt: off + display = ( + f"> ```py\n" + f"> {calcul.expr} =\n" + f"> {calcul.answer: <41}\n" + f"> ```" + ) + # fmt: on calcul.expr = calcul.answer calcul.new = True else: - display = f"> ```py\n" f"> Ans = {calcul.answer}\n" f"> {calcul.expr: <41}\n" f"> ```" + # fmt: off + display = ( + f"> ```py\n" + f"> Ans = {calcul.answer}\n" + f"> {calcul.expr: <41}\n" + f"> ```" + ) + # fmt: on return display @@ -99,7 +112,7 @@ def __init__(self, parent: Calculator, inter: Interaction, calcul: Calcul): (ButtonStyle.secondary, "3", "3"), (ButtonStyle.danger, "+", " + "), (ButtonStyle.primary, "Ans", "Ans"), - (ButtonStyle.secondary, "⁺∕₋", "opposite"), + (ButtonStyle.secondary, "⁺∕₋", "opposite"), # noqa: RUF001 (ButtonStyle.secondary, "0", "0"), (ButtonStyle.secondary, ",", "."), (ButtonStyle.success, "=", "result"), @@ -137,7 +150,7 @@ async def compute(self, button: ui.Button[Self], interaction: discord.Interactio avertissements: list[str] = [] - if selection in numbers + ("π", "Ans"): + if selection in (*numbers, "π", "Ans"): if calcul.new: calcul.expr = "" calcul.new = False @@ -147,7 +160,7 @@ async def compute(self, button: ui.Button[Self], interaction: discord.Interactio if calcul.new: calcul.expr = "" calcul.new = False - if any(calcul.expr.endswith(nb) for nb in numbers + (".",)): + if any(calcul.expr.endswith(nb) for nb in (*numbers, ".")): if "." not in calcul.expr.split(" ")[-1]: calcul.expr += "." else: diff --git a/src/cogs/calculator/calcul.py b/src/cogs/calculator/calcul.py index 1db139d..61b4f88 100644 --- a/src/cogs/calculator/calcul.py +++ b/src/cogs/calculator/calcul.py @@ -7,9 +7,8 @@ import decimal import operator as op import re -from collections.abc import Sequence +from collections.abc import Callable, Sequence from math import pi -from typing import Callable Decimal = decimal.Decimal @@ -98,7 +97,7 @@ def string_process(calcul: str) -> str: decimal.getcontext().prec = 10 if calcul.count("(") != calcul.count(")"): - raise UnclosedParentheses() + raise UnclosedParentheses while match := re.search(r"([)\d])(\()", calcul): calcul = match.re.sub(r"\1 * \2", calcul) while match := re.search(r"(\))(\d)", calcul): diff --git a/src/cogs/clear/__init__.py b/src/cogs/clear/__init__.py index 4ca2cb2..a61e715 100644 --- a/src/cogs/clear/__init__.py +++ b/src/cogs/clear/__init__.py @@ -3,8 +3,9 @@ import asyncio import logging import time +from collections.abc import AsyncGenerator, Awaitable, Callable from datetime import datetime -from typing import TYPE_CHECKING, AsyncGenerator, Awaitable, Callable, Self, cast +from typing import TYPE_CHECKING, Self, cast import discord from discord import app_commands, ui @@ -60,7 +61,7 @@ def __init__(self, bot: MyBot): user=__("messages from the user {{}}"), role=__("messages whose user has the role {{}}"), pattern=__("messages that match {{}} (regex, multiline, case sensitive, not anchored)"), - has=__("messages that has {{}}"), # e.g. attachement:image will delete messages that has an image attached. + has=__("messages that has {{}}"), # e.g. attachment:image will delete messages that has an image attached. max_length=__("messages longer or equal to {{}} (blank spaces included) (empty messages included)"), min_length=__("messages shorter or equal to {{}} (blank spaces included)"), before=__("messages sent before {{}} (yyyy-mm-dd or message ID)"), diff --git a/src/cogs/clear/filters.py b/src/cogs/clear/filters.py index 335f70a..06f77b3 100644 --- a/src/cogs/clear/filters.py +++ b/src/cogs/clear/filters.py @@ -2,7 +2,8 @@ import re from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Callable, cast +from collections.abc import Callable +from typing import TYPE_CHECKING, cast import discord from discord.utils import get @@ -94,10 +95,8 @@ class HasFilter(Filter): video_content_type_re = re.compile(r"^video\/.*") audio_content_type_re = re.compile(r"^audio\/.*") has_link_re = re.compile( - ( - r"(?i)\b(?:(?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\((?:[^\s()<>]+|" - r"(?:\([^\s()<>]+\)))*\))+(?:\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'\".,<>?«»“”‘’]))" - ) + r"(?i)\b(?:(?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\((?:[^\s()<>]+|" + r"(?:\([^\s()<>]+\)))*\))+(?:\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'\".,<>?«»“”‘’]))" # noqa: RUF001 ) has_discord_invite_re = re.compile(r"discord(?:app)?\.(?:gg|com/invite)\/([a-zA-Z0-9]+)") @@ -112,10 +111,7 @@ async def check_types( continue if content_type_re.match(attachment.content_type): return True - for embed in message.embeds: - if any(embed.type == t for t in embed_types): - return True - return False + return any(any(embed.type == t for t in embed_types) for embed in message.embeds) async def test(self, message: discord.Message) -> bool: match self.has: diff --git a/src/cogs/config/config_bot.py b/src/cogs/config/config_bot.py index 26eac54..b6675d0 100644 --- a/src/cogs/config/config_bot.py +++ b/src/cogs/config/config_bot.py @@ -17,7 +17,7 @@ class ConfigBot(ExtendedCog, name="config_bot"): async def public_translation(self, inter: Interaction, value: bool) -> None: if inter.guild_id is None: - raise UnexpectedError() + raise UnexpectedError async with self.bot.async_session.begin() as session: guild_db = await self.bot.get_or_create_db(session, db.GuildDB, guild_id=inter.guild_id) diff --git a/src/cogs/config/config_guild.py b/src/cogs/config/config_guild.py index 42ecc57..bfa4886 100644 --- a/src/cogs/config/config_guild.py +++ b/src/cogs/config/config_guild.py @@ -14,4 +14,4 @@ class ConfigGuild(ExtendedCog, name="config_guild"): async def emote(self, inter: Interaction) -> None: - raise NotImplementedError() + raise NotImplementedError diff --git a/src/cogs/eval.py b/src/cogs/eval.py index 9a57968..c77abc3 100644 --- a/src/cogs/eval.py +++ b/src/cogs/eval.py @@ -111,7 +111,7 @@ def set_embeds_color(color: Color) -> None: set_embeds_color(Color.orange()) else: result, errored = task.result() - embeds[1].description = f"```py\n{size_text(result, 4000, 'middle')}\n```" + embeds[1].description = f"```py\n{size_text(result, 4000, "middle")}\n```" if errored: set_embeds_color(Color.red()) else: @@ -201,8 +201,8 @@ async def code_evaluation(code: str, inter: Interaction, bot: MyBot) -> tuple[st body: Any = parsed.body[0].body insert_returns(body) - exec(compile(parsed, "", "exec"), env) # nosec - output: Any = await eval("_eval()", env) # nosec + exec(compile(parsed, "", "exec"), env) # noqa: S102 + output: Any = await eval("_eval()", env) # noqa: S307 except Exception as _: lines = [line + "\n" for line in str_body.splitlines()] diff --git a/src/cogs/game/connect4.py b/src/cogs/game/connect4.py index 18f24dc..ceac97a 100644 --- a/src/cogs/game/connect4.py +++ b/src/cogs/game/connect4.py @@ -14,4 +14,4 @@ class GameConnect4(ExtendedCog, name="game_connect4"): async def connect4(self, inter: Interaction) -> None: - raise NotImplementedError() + raise NotImplementedError diff --git a/src/cogs/game/game_2084.py b/src/cogs/game/game_2084.py index ae0aeb6..3d21447 100644 --- a/src/cogs/game/game_2084.py +++ b/src/cogs/game/game_2084.py @@ -68,7 +68,7 @@ def str_board(board: list[list[Tile]]) -> str: return "\n".join("".join(tile_value_to_emoji[t.value] for t in row) for row in board) async def interaction_check(self, interaction: Interaction, /) -> bool: - if not interaction.user == self.user: + if interaction.user != self.user: await interaction.response.send_message( **response_constructor(ResponseType.error, "You are not the owner of this game.") ) diff --git a/src/cogs/game/minesweeper/__init__.py b/src/cogs/game/minesweeper/__init__.py index b233d1a..b3bcb33 100644 --- a/src/cogs/game/minesweeper/__init__.py +++ b/src/cogs/game/minesweeper/__init__.py @@ -19,7 +19,7 @@ logger = logging.getLogger(__name__) -corner = " " +corner = " " # noqa: RUF001 row_denominators = ["⒈", "⒉", "⒊", "⒋", "⒌", "⒍", "⒎", "⒏", "⒐", "⒑", "⒒", "⒓", "⒔", "⒕", "⒖", "⒗", "⒘", "⒙", "⒚", "⒛"] column_denominators = ascii_uppercase @@ -27,7 +27,7 @@ def build_board_display(game: Minesweeper) -> str: - description = "```" + corner + " " * 3 + " ".join(column_denominators[i] for i in range(0, game.size[1])) + "\n" + description = "```" + corner + " " * 3 + " ".join(column_denominators[i] for i in range(game.size[1])) + "\n" display_chars = { 0: " ", @@ -45,9 +45,9 @@ def get_char(row: int, column: int) -> str: return unrevealed_char - for row in range(0, game.size[0]): + for row in range(game.size[0]): description += row_denominators[row] + " |" - for column in range(0, game.size[1]): + for column in range(game.size[1]): description += " " + get_char(row, column) description += "\n" description += "```" @@ -73,11 +73,9 @@ def __init__(self, game_embed: discord.Embed): self.game: Minesweeper | None = None self.game_embed = game_embed - self.row.options = [ - discord.SelectOption(label=row_denominators[i], value=str(i)) for i in range(0, board_size[0]) - ] + self.row.options = [discord.SelectOption(label=row_denominators[i], value=str(i)) for i in range(board_size[0])] self.column.options = [ - discord.SelectOption(label=column_denominators[i], value=str(i)) for i in range(0, board_size[1]) + discord.SelectOption(label=column_denominators[i], value=str(i)) for i in range(board_size[1]) ] def check_playable(self) -> None: diff --git a/src/cogs/game/minesweeper/minesweeper_game.py b/src/cogs/game/minesweeper/minesweeper_game.py index 1ea8a45..872780d 100644 --- a/src/cogs/game/minesweeper/minesweeper_game.py +++ b/src/cogs/game/minesweeper/minesweeper_game.py @@ -5,14 +5,14 @@ from dataclasses import dataclass from enum import Enum, auto from itertools import chain, permutations -from typing import TypeAlias, cast +from typing import cast -height: TypeAlias = int -width: TypeAlias = int -row: TypeAlias = int -column: TypeAlias = int +type Height = int +type Width = int +type Row = int +type Column = int -boardT = list[list[int]] +BoardT = list[list[int]] @dataclass @@ -20,7 +20,7 @@ class MinesweeperConfig: height: int width: int number_of_mines: int - initial_play: tuple[row, column] | None = None + initial_play: tuple[Row, Column] | None = None class GameOver(Exception): @@ -37,23 +37,23 @@ class PlayType(Enum): @dataclass class Play: type: PlayType - positions: tuple[tuple[row, column], ...] + positions: tuple[tuple[Row, Column], ...] class Minesweeper: def __init__( self, - size: tuple[height, width], + size: tuple[Height, Width], number_of_mines: int, - initial_play: tuple[row, column] | None = None, + initial_play: tuple[Row, Column] | None = None, ): - self.size: tuple[height, width] = size + self.size: tuple[Height, Width] = size self.number_of_mines: int = number_of_mines - self.revealed: list[tuple[height, width]] = [] - self.flags: list[tuple[height, width]] = [] + self.revealed: list[tuple[Height, Width]] = [] + self.flags: list[tuple[Height, Width]] = [] self.game_over: bool = False - self._board: boardT = self.create_board(initial_play) + self._board: BoardT = self.create_board(initial_play) if initial_play is not None: self.play(*initial_play) @@ -62,15 +62,15 @@ def from_config(cls, config: MinesweeperConfig): return cls((config.height, config.width), config.number_of_mines, config.initial_play) @property - def board(self) -> boardT: + def board(self) -> BoardT: return self._board @property - def positions(self) -> set[tuple[height, width]]: + def positions(self) -> set[tuple[Height, Width]]: return {(x, y) for x in range(self.size[0]) for y in range(self.size[1])} @property - def mines_positions(self) -> set[tuple[height, width]]: + def mines_positions(self) -> set[tuple[Height, Width]]: return {(x, y) for x in range(self.size[0]) for y in range(self.size[1]) if self._board[x][y] == -1} def is_inside(self, x: int, y: int) -> bool: @@ -104,10 +104,10 @@ def play(self, x: int, y: int) -> Play: self.revealed.append((x, y)) return Play(PlayType.NUMBERED_SPOT, ((x, y),)) - def diffuse_empty_places(self, x: int, y: int, is_corner: bool = False) -> tuple[tuple[row, column], ...]: + def diffuse_empty_places(self, x: int, y: int, is_corner: bool = False) -> tuple[tuple[Row, Column], ...]: """Diffuse the empty place at the given position. This function is recursive.""" if (x, y) in self.revealed or not self.is_inside(x, y): - return tuple() + return () self.revealed.append((x, y)) @@ -115,24 +115,24 @@ def diffuse_empty_places(self, x: int, y: int, is_corner: bool = False) -> tuple gen: Iterable[tuple[int, int]] = cast( Iterable[tuple[int, int]], chain(permutations(range(-1, 2, 1), 2), ((1, 1), (-1, -1))) ) - return ((x, y),) + tuple( - cpl for dx, dy in gen for cpl in self.diffuse_empty_places(x + dx, y + dy, dx != 0 and dy != 0) + return ( + (x, y), + *tuple(cpl for dx, dy in gen for cpl in self.diffuse_empty_places(x + dx, y + dy, dx != 0 and dy != 0)), ) else: return ((x, y),) - def create_board(self, initial_play: tuple[row, column] | None) -> boardT: - board: boardT = [[0 for _ in range(self.size[1])] for _ in range(self.size[0])] + def create_board(self, initial_play: tuple[Row, Column] | None) -> BoardT: + board: BoardT = [[0 for _ in range(self.size[1])] for _ in range(self.size[0])] def increment_around(x: int, y: int): """Increment the value of the cells around the given position.""" # I think this can be done in a more elegant way def incr(x: int, y: int): - if 0 <= x < self.size[0] and 0 <= y < self.size[1]: - if board[x][y] != -1: - board[x][y] += 1 + if 0 <= x < self.size[0] and 0 <= y < self.size[1] and board[x][y] != -1: + board[x][y] += 1 relative_positions: Iterable[tuple[int, int]] = cast( Iterable[tuple[int, int]], chain(permutations(range(-1, 2, 1), 2), ((1, 1), (-1, -1))) diff --git a/src/cogs/game/rpc.py b/src/cogs/game/rpc.py index bc3728b..aeeeb5f 100644 --- a/src/cogs/game/rpc.py +++ b/src/cogs/game/rpc.py @@ -14,4 +14,4 @@ class GameRPC(ExtendedCog, name="game_rpc"): async def rpc(self, inter: Interaction) -> None: - raise NotImplementedError() + raise NotImplementedError diff --git a/src/cogs/game/tictactoe.py b/src/cogs/game/tictactoe.py index e80f78e..d9c4cae 100644 --- a/src/cogs/game/tictactoe.py +++ b/src/cogs/game/tictactoe.py @@ -14,4 +14,4 @@ class GameTictactoe(ExtendedCog, name="game_tictactoe"): async def tictactoe(self, inter: Interaction) -> None: - raise NotImplementedError() + raise NotImplementedError diff --git a/src/cogs/help.py b/src/cogs/help.py index b37ad3d..2260cb5 100644 --- a/src/cogs/help.py +++ b/src/cogs/help.py @@ -2,7 +2,8 @@ import logging from collections import OrderedDict -from typing import TYPE_CHECKING, Iterable, Self, Sequence, cast +from collections.abc import Iterable, Sequence +from typing import TYPE_CHECKING, Self, cast import discord from discord import app_commands, ui diff --git a/src/cogs/ping.py b/src/cogs/ping.py index d6baafe..40affb5 100644 --- a/src/cogs/ping.py +++ b/src/cogs/ping.py @@ -28,7 +28,7 @@ class Ping(ExtendedCog): extras={"soon": True}, ) async def ping(self, inter: Interaction) -> None: - raise NotImplementedError() + raise NotImplementedError async def setup(bot: MyBot) -> None: diff --git a/src/cogs/poll/display.py b/src/cogs/poll/display.py index 164b0b0..c6ecc5d 100644 --- a/src/cogs/poll/display.py +++ b/src/cogs/poll/display.py @@ -1,5 +1,6 @@ from __future__ import annotations +from itertools import starmap from typing import TYPE_CHECKING import discord @@ -46,10 +47,11 @@ async def build(cls, poll: Poll, bot: MyBot, old_embed: Embed | None = None) -> .where(db.PollAnswer.poll_id == poll.id) .group_by(db.PollAnswer.value) ) - # a generator is used for typing purposes - votes = dict( - (key, value) for key, value in (await session.execute(stmt)).all() # choice_id: vote_count - ) + + votes = { # noqa: C416, dict comprehension used for typing purposes + key: value + for key, value in (await session.execute(stmt)).all() # choice_id: vote_count + } if poll.type == db.PollType.CHOICE: # when we delete a choice from a poll, the votes are still in the db before commit # so we need to filter them @@ -61,9 +63,7 @@ async def build(cls, poll: Poll, bot: MyBot, old_embed: Embed | None = None) -> poll_display = cls(poll, votes) - description_split: list[str] = [] - description_split.append(poll_display.build_end_date()) - description_split.append(poll_display.build_legend()) + description_split: list[str] = [poll_display.build_end_date(), poll_display.build_legend()] embed.description = "\n".join(description_split) @@ -105,7 +105,7 @@ def format_legend_choice(index: int, choice: db.PollChoice) -> str: percent = self.calculate_proportion(str(choice.id)) * 100 return f"{LEGEND_EMOJIS[index]} `{percent:6.2f}%` {choice.label}" - return "\n".join(format_legend_choice(i, choice) for i, choice in enumerate(self.poll.choices)) + return "\n".join(starmap(format_legend_choice, enumerate(self.poll.choices))) case db.PollType.BOOLEAN: def format_legend_boolean(boolean_value: bool) -> str: diff --git a/src/cogs/poll/edit.py b/src/cogs/poll/edit.py index 1687e88..9b170bf 100644 --- a/src/cogs/poll/edit.py +++ b/src/cogs/poll/edit.py @@ -1,7 +1,7 @@ from __future__ import annotations -from datetime import datetime, timedelta, timezone -from typing import TYPE_CHECKING, Self, Type, cast +from datetime import UTC, datetime, timedelta +from typing import TYPE_CHECKING, Self, cast import discord from discord import Interaction, ui @@ -23,7 +23,7 @@ class EditPollMenus(ui.Select["EditPoll"]): def __init__(self, poll: db.Poll): super().__init__(placeholder=_("Select what you want to edit.")) - self.menus: list[Type[EditSubmenu]] = [ + self.menus: list[type[EditSubmenu]] = [ EditTitleAndDescription, EditEndingTime, EditAllowedRoles, @@ -236,7 +236,7 @@ async def build(self) -> Self: ] if self.poll.end_date is not None: - delta = self.poll.end_date - datetime.now(timezone.utc) + delta = self.poll.end_date - datetime.now(UTC) if delta < timedelta(): self.poll.end_date = None @@ -278,7 +278,7 @@ def set_time(self): if ending_time == timedelta(): self.poll.end_date = None else: - self.poll.end_date = datetime.now(timezone.utc) + ending_time + self.poll.end_date = datetime.now(UTC) + ending_time async def callback(self, inter: Interaction): self.set_time() diff --git a/src/cogs/poll/vote_menus.py b/src/cogs/poll/vote_menus.py index 240b2fc..2f02d63 100644 --- a/src/cogs/poll/vote_menus.py +++ b/src/cogs/poll/vote_menus.py @@ -2,8 +2,9 @@ from __future__ import annotations -from datetime import datetime, timezone -from typing import TYPE_CHECKING, Self, Sequence, cast +from collections.abc import Sequence +from datetime import UTC, datetime +from typing import TYPE_CHECKING, Self, cast import discord from discord import ui @@ -65,14 +66,14 @@ async def vote(self, inter: discord.Interaction, button: ui.Button[Self]): ) return - if poll.end_date is not None and poll.end_date < datetime.now(timezone.utc): + if poll.end_date is not None and poll.end_date < datetime.now(UTC): await inter.response.send_message( **response_constructor(ResponseType.error, _("Sorry, this poll is over, you can't vote anymore!")), ephemeral=True, ) return user = cast(discord.Member, inter.user) - if poll.allowed_roles and not set(role.id for role in user.roles) & set(poll.allowed_roles): + if poll.allowed_roles and not {role.id for role in user.roles} & set(poll.allowed_roles): message_display = response_constructor( ResponseType.error, _("Sorry, you need one of the following roles to vote :") ) diff --git a/src/cogs/stats.py b/src/cogs/stats.py index f95115f..ae72e5d 100644 --- a/src/cogs/stats.py +++ b/src/cogs/stats.py @@ -56,7 +56,6 @@ async def on_interaction(self, inter: Interaction) -> None: payload = { "command": parent.name, "exact_command": inter.command.qualified_name, - "namespace": inter.namespace, "type": app_command.type.name, "locale": inter.locale.name, "namespace": inter.namespace.__dict__, @@ -79,7 +78,7 @@ async def on_interaction(self, inter: Interaction) -> None: extras={"soon": True}, ) async def stats(self, inter: Interaction) -> None: - raise NotImplementedError() + raise NotImplementedError async def setup(bot: MyBot): diff --git a/src/cogs/translate/__init__.py b/src/cogs/translate/__init__.py index 167897c..de5b20c 100644 --- a/src/cogs/translate/__init__.py +++ b/src/cogs/translate/__init__.py @@ -2,10 +2,11 @@ import importlib import logging +from collections.abc import Sequence from dataclasses import dataclass, field from datetime import datetime, timedelta from functools import partial -from typing import TYPE_CHECKING, Any, NamedTuple, Sequence, cast +from typing import TYPE_CHECKING, Any, NamedTuple, cast import discord from discord import Embed, Message, app_commands, ui @@ -78,7 +79,7 @@ def inject_translations(self, translation: Sequence[str]): self.content = translation[0][: EmbedsCharLimits.DESCRIPTION.value - 1] i += 1 for tr_embed in self.tr_embeds: - tr_embed.reconstruct(translation[i : i + len(tr_embed)]) # noqa: E203 + tr_embed.reconstruct(translation[i : i + len(tr_embed)]) i += len(tr_embed) @@ -152,7 +153,7 @@ def reconstruct(self, translations: Sequence[str]): obj = obj[key] try: - limit = EmbedsCharLimits["_".join(char_lim_key + [keys[-1]]).upper()] + limit = EmbedsCharLimits["_".join([*char_lim_key, keys[-1]]).upper()] except KeyError: limit = None @@ -229,7 +230,9 @@ async def translate_slash(self, inter: Interaction, to: str, text: str, from_: s if from_ is not None: from_language = available_languages.from_code(from_) if from_language is None: - raise BadArgument(_(f"The language you provided under the argument `from_` is not supported : {from_}")) + raise BadArgument( + _("The language you provided under the argument `from_` is not supported : {}", from_) + ) else: from_language = None diff --git a/src/cogs/translate/_types.py b/src/cogs/translate/_types.py index d760921..8532265 100644 --- a/src/cogs/translate/_types.py +++ b/src/cogs/translate/_types.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Any, Protocol, Sequence +from collections.abc import Sequence +from typing import Any, Protocol from discord import Embed, ui from discord.utils import MISSING diff --git a/src/cogs/translate/adapters/libretranslate.py b/src/cogs/translate/adapters/libretranslate.py index 9e63cc8..4b4ffa9 100644 --- a/src/cogs/translate/adapters/libretranslate.py +++ b/src/cogs/translate/adapters/libretranslate.py @@ -1,7 +1,8 @@ from __future__ import annotations import asyncio -from typing import Sequence, Type, TypeVar +from collections.abc import Sequence +from typing import TypeVar from lingua import Language as LinguaLanguage, LanguageDetectorBuilder @@ -65,7 +66,7 @@ def __init__(self): ) async def available_languages(self) -> Languages: - return Languages(x.value for x in language_to_libre.keys()) + return Languages(x.value for x in language_to_libre) async def translate(self, text: str, to: Language, from_: Language | None = None) -> str: return await self.instance.translate( @@ -90,5 +91,5 @@ async def detect(self, text: str) -> Language | None: return lingua_to_language[result].value -def get_translator() -> Type[TranslatorAdapter]: +def get_translator() -> type[TranslatorAdapter]: return Translator diff --git a/src/cogs/translate/adapters/microsoft.py b/src/cogs/translate/adapters/microsoft.py index 10bc3bd..3086815 100644 --- a/src/cogs/translate/adapters/microsoft.py +++ b/src/cogs/translate/adapters/microsoft.py @@ -1,4 +1,4 @@ -from typing import Sequence, Type +from collections.abc import Sequence from lingua import Language as LinguaLanguage, LanguageDetectorBuilder @@ -61,5 +61,5 @@ async def available_languages(self) -> Languages: return Languages(x.value for x in LanguagesEnum) -def get_translator() -> Type[TranslatorAdapter]: +def get_translator() -> type[TranslatorAdapter]: return Translator diff --git a/src/cogs/translate/languages.py b/src/cogs/translate/languages.py index d0c7d2f..ae465f3 100644 --- a/src/cogs/translate/languages.py +++ b/src/cogs/translate/languages.py @@ -1,5 +1,6 @@ +from collections.abc import Iterable, Iterator, Sequence from enum import Enum -from typing import Iterable, Iterator, NamedTuple, Sequence +from typing import NamedTuple from discord import Locale @@ -24,7 +25,7 @@ def __hash__(self) -> int: class LanguagesEnum(Enum): # fmt: off british_english = Language( - name='british english', + name="british english", discord_locale=Locale.british_english, unicode_flag_emotes=("🇦🇮", "🇦🇬", "🇦🇺", "🇧🇸", "🇧🇧", "🇧🇿", "🇧🇲", "🇧🇼", "🇮🇴", "🇨🇦", "🇰🇾", "🇨🇽", "🇨🇨", "🇨🇰", "🇩🇲", "🇫🇰", "🇫🇯", "🇬🇲", "🇬🇭", "🇬🇮", "🇬🇩", "🇬🇺", @@ -34,51 +35,51 @@ class LanguagesEnum(Enum): "🇹🇴", "🇹🇹", "🇹🇨", "🇹🇻", "🇬🇧", "🇻🇬", "🇻🇮", "🇿🇲", "🏴󠁧󠁢󠁥󠁮󠁧󠁿"), ) american_english = Language( - name='american english', + name="american english", discord_locale=Locale.american_english, unicode_flag_emotes=("🇺🇸", "🇺🇲"), ) arabic = Language( - name='arabic', - ietf_bcp_47='ar-SA', + name="arabic", + ietf_bcp_47="ar-SA", unicode_flag_emotes=("🇩🇿", "🇧🇭", "🇰🇲", "🇩🇯", "🇪🇬", "🇪🇷", "🇯🇴", "🇰🇼", "🇱🇧", "🇱🇾", "🇲🇷", "🇲🇦", "🇴🇲", "🇶🇦", "🇸🇦", "🇸🇩", "🇸🇾", "🇹🇳", "🇦🇪", "🇪🇭", "🇾🇪"), ) chinese = Language( - name='chinese', + name="chinese", discord_locale=Locale.chinese, unicode_flag_emotes=("🇨🇳", "🇭🇰", "🇲🇴", "🇹🇼") ) taiwan_chinese = Language( - name='chinese (taiwan)', + name="chinese (taiwan)", discord_locale=Locale.taiwan_chinese, unicode_flag_emotes=() ) french = Language( - name='french', + name="french", discord_locale=Locale.french, unicode_flag_emotes=("🇧🇯", "🇧🇫", "🇧🇮", "🇨🇲", "🇨🇫", "🇹🇩", "🇨🇩", "🇨🇬", "🇨🇮", "🇬🇶", "🇫🇷", "🇬🇫", "🇵🇫", "🇹🇫", "🇬🇦", "🇬🇵", "🇬🇳", "🇲🇱", "🇲🇶", "🇾🇹", "🇲🇨", "🇳🇨", "🇳🇪", "🇷🇪", "🇧🇱", "🇲🇫", "🇵🇲", "🇸🇳", "🇸🇨", "🇹🇬", "🇻🇺", "🇼🇫") ) german = Language( - name='german', + name="german", discord_locale=Locale.german, unicode_flag_emotes=("🇦🇹", "🇩🇪", "🇱🇮", "🇨🇭") ) hindi = Language( - name='hindi', + name="hindi", discord_locale=Locale.hindi, unicode_flag_emotes=("🇮🇳",) ) indonesian = Language( - name='indonesian', - ietf_bcp_47='id-ID', + name="indonesian", + ietf_bcp_47="id-ID", unicode_flag_emotes=("🇮🇩",) ) irish = Language( - name='irish', - ietf_bcp_47='en-IE', + name="irish", + ietf_bcp_47="en-IE", unicode_flag_emotes=("🇮🇪",) ) italian = Language( diff --git a/src/cogs/translate/translator_abc.py b/src/cogs/translate/translator_abc.py index fe7de18..b4d07ca 100644 --- a/src/cogs/translate/translator_abc.py +++ b/src/cogs/translate/translator_abc.py @@ -9,7 +9,6 @@ async def close(self): """ The method is called if the Cog is closed. """ - pass @abstractmethod async def translate(self, text: str, to: Language, from_: Language | None = None) -> str: ... diff --git a/src/commands_exporter.py b/src/commands_exporter.py index 53f72b0..61098b0 100644 --- a/src/commands_exporter.py +++ b/src/commands_exporter.py @@ -182,7 +182,7 @@ def default(o: Any): return JSONEncoder().default(o) - with open(filename, "w", encoding="utf-8") as file: + with open(filename, "w", encoding="utf-8") as file: # noqa: ASYNC101 json.dump(features, file, indent=4, default=default) diff --git a/src/core/_config.py b/src/core/_config.py index 53aa98b..d3c1803 100644 --- a/src/core/_config.py +++ b/src/core/_config.py @@ -17,7 +17,7 @@ class Config: SUPPORT_GUILD_ID: int = 332209340780118016 BOT_ID: int = 500023552905314304 # this should be retrieved from bot.client.id, but anyway. - OWNERS_IDS: list[int] = [341550709193441280, 329710312880340992] + OWNERS_IDS: ClassVar[list[int]] = [341550709193441280, 329710312880340992] POSTGRES_USER: str = "postgres" POSTGRES_DB: str = "mybot" POSTGRES_PASSWORD: str | None = None @@ -61,7 +61,7 @@ def __getattribute__(self, name: str) -> Any: def define_config(config_path: str | None = None, **kwargs: Any): if config_path: - with open(config_path, mode="r", encoding="utf-8") as f: + with open(config_path, encoding="utf-8") as f: kwargs |= tomllib.load(f.buffer) Config(**kwargs) # it is a singleton, so it will directly affect the instance. diff --git a/src/core/_logger.py b/src/core/_logger.py index 420e844..62bb0d6 100644 --- a/src/core/_logger.py +++ b/src/core/_logger.py @@ -5,7 +5,7 @@ import os import sys import traceback -from typing import Any, NamedTuple, cast +from typing import Any, ClassVar, NamedTuple, cast import aiohttp import discord @@ -36,7 +36,7 @@ class AdditionalContext(NamedTuple): class DiscordLogHandler(logging.Handler): - bind_colors = { + bind_colors: ClassVar = { logging.WARNING: discord.Color.yellow(), logging.INFO: discord.Color.blue(), logging.ERROR: discord.Color.red(), @@ -44,7 +44,7 @@ class DiscordLogHandler(logging.Handler): logging.DEBUG: discord.Color.orange(), } - tasks: list[asyncio.Task[Any]] = [] + tasks: ClassVar[list[asyncio.Task[Any]]] = [] def __init__(self) -> None: super().__init__(level=logging.WARNING) @@ -121,7 +121,7 @@ class _ColorFormatter(logging.Formatter): # 100-107 are the same as the bright ones but for the background. # 1 means bold, 2 means dim, 0 means reset, and 4 means underline. - LEVEL_COLOURS = [ + LEVEL_COLOURS: ClassVar = [ (logging.DEBUG, "\x1b[40;1m"), (logging.INFO, "\x1b[34;1m"), (logging.WARNING, "\x1b[33;1m"), @@ -129,7 +129,7 @@ class _ColorFormatter(logging.Formatter): (logging.CRITICAL, "\x1b[41m"), ] - FORMATS = { + FORMATS: ClassVar = { level: logging.Formatter( f"\x1b[30;1m%(asctime)s\x1b[0m {colour}%(levelname)-8s\x1b[0m \x1b[35m%(name)s\x1b[0m %(message)s", dt_fmt, diff --git a/src/core/_types.py b/src/core/_types.py index a63dee8..c3a1f6d 100644 --- a/src/core/_types.py +++ b/src/core/_types.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Coroutine, ParamSpec, TypeAlias, TypeVar, Union +from collections.abc import Coroutine +from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar from discord import Message from discord.ext import commands @@ -8,7 +9,7 @@ if TYPE_CHECKING: from .extended_commands import ExtendedCog, MiscCommandContextFilled, MiscCommandContextRaw -UnresolvedContext: TypeAlias = Union["MiscCommandContextRaw", "MiscCommandContextFilled", Message] +type UnresolvedContext = MiscCommandContextRaw | MiscCommandContextFilled | Message UnresolvedContextT = TypeVar("UnresolvedContextT", bound=UnresolvedContext) P = ParamSpec("P") diff --git a/src/core/caches.py b/src/core/caches.py index a1c545a..84d47a4 100644 --- a/src/core/caches.py +++ b/src/core/caches.py @@ -1,21 +1,8 @@ from collections import OrderedDict -from collections.abc import Mapping +from collections.abc import Callable, Iterator, Mapping, MutableMapping, Sequence from datetime import datetime, timedelta from functools import wraps -from typing import ( - Any, - Callable, - Concatenate, - Generic, - Iterator, - MutableMapping, - NamedTuple, - ParamSpec, - Sequence, - SupportsIndex, - TypeVar, - overload, -) +from typing import Any, Concatenate, Generic, NamedTuple, ParamSpec, SupportsIndex, TypeVar, overload _K = TypeVar("_K") # Type for Keys _V = TypeVar("_V") # Type for Values diff --git a/src/core/checkers/app.py b/src/core/checkers/app.py index e5aa26e..55b0d04 100644 --- a/src/core/checkers/app.py +++ b/src/core/checkers/app.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Callable +from collections.abc import Callable from discord.app_commands import check diff --git a/src/core/checkers/base.py b/src/core/checkers/base.py index fe0f52c..f6adc72 100644 --- a/src/core/checkers/base.py +++ b/src/core/checkers/base.py @@ -1,7 +1,8 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any, Callable, TypeVar +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, TypeVar import discord from discord.app_commands import Command, ContextMenu, check as app_check @@ -26,7 +27,7 @@ def add_extra(type_: CommandType, func: T, name: str, value: Any) -> T: copy_func = func # typing behavior if type_ is CommandType.APP: - if isinstance(func, (Command, ContextMenu)): + if isinstance(func, Command | ContextMenu): func.extras[name] = value else: logger.critical( @@ -66,7 +67,7 @@ def predicate(ctx: Interaction | MiscCommandContext[MyBot]): def bot_required_permissions_base(type_: CommandType, **perms: bool) -> Callable[[T], T]: invalid = set(perms) - set(discord.Permissions.VALID_FLAGS) if invalid: - raise TypeError(f"Invalid permission(s): {', '.join(invalid)}") + raise TypeError(f"Invalid permission(s): {", ".join(invalid)}") def decorator(func: T) -> T: match type_: diff --git a/src/core/checkers/max_concurrency.py b/src/core/checkers/max_concurrency.py index 6f195b0..fe1bbb6 100644 --- a/src/core/checkers/max_concurrency.py +++ b/src/core/checkers/max_concurrency.py @@ -3,7 +3,8 @@ import asyncio import logging from collections import deque -from typing import TYPE_CHECKING, Any, Callable, Deque, Hashable, Self, TypeVar, Union +from collections.abc import Callable, Hashable +from typing import TYPE_CHECKING, Any, Self, TypeVar from ..errors import MaxConcurrencyReached from ..extended_commands import misc_check as misc_check @@ -19,7 +20,7 @@ from .._types import CoroT - MaxConcurrencyFunction = Union[Callable[[Interaction], CoroT[T]], Callable[[Interaction], T]] + MaxConcurrencyFunction = Callable[[Interaction], CoroT[T]] | Callable[[Interaction], T] class MaxConcurrency: @@ -90,7 +91,7 @@ class _Semaphore: def __init__(self, number: int) -> None: self.value: int = number self.loop: asyncio.AbstractEventLoop = asyncio.get_running_loop() - self._waiters: Deque[asyncio.Future[Any]] = deque() + self._waiters: deque[asyncio.Future[Any]] = deque() def __repr__(self) -> str: return f"<_Semaphore value={self.value} waiters={len(self._waiters)}>" diff --git a/src/core/checkers/misc.py b/src/core/checkers/misc.py index d3882f7..4ea5346 100644 --- a/src/core/checkers/misc.py +++ b/src/core/checkers/misc.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable +from collections.abc import Callable +from typing import TYPE_CHECKING from ..extended_commands import MiscCommandContext, misc_check as misc_check from ..utils import CommandType diff --git a/src/core/constants.py b/src/core/constants.py index 191f3f0..ff76152 100644 --- a/src/core/constants.py +++ b/src/core/constants.py @@ -17,6 +17,8 @@ def _identity[T](x: T) -> T: class Emoji(str): + __slots__ = ("id",) + def __new__(cls, id: Snowflake) -> Self: return super().__new__(cls, f"<:_:{id}>") diff --git a/src/core/db/tables.py b/src/core/db/tables.py index 515c9da..588e3aa 100644 --- a/src/core/db/tables.py +++ b/src/core/db/tables.py @@ -1,9 +1,10 @@ from __future__ import annotations import enum +from collections.abc import Iterable, Sequence from datetime import datetime from functools import partial -from typing import Annotated, Any, Iterable, Sequence, TypeVar +from typing import Annotated, Any, ClassVar, TypeVar from sqlalchemy import ARRAY, BigInteger, DateTime, Enum, ForeignKey from sqlalchemy.dialects.postgresql import BIGINT, BOOLEAN, INTEGER, JSONB, SMALLINT, VARCHAR @@ -30,7 +31,7 @@ def remove(self, value: T): self.changed() def clear(self): - list.clear(self) + list[T].clear(self) self.changed() def extend(self, value: Iterable[T]): @@ -59,7 +60,7 @@ class PremiumType(enum.Enum): class Base(MappedAsDataclass, AsyncAttrs, DeclarativeBase): - type_annotation_map = { + type_annotation_map: ClassVar = { bool: BOOLEAN, int: INTEGER, } diff --git a/src/core/error_handler.py b/src/core/error_handler.py index a788cb0..9cc796e 100644 --- a/src/core/error_handler.py +++ b/src/core/error_handler.py @@ -54,7 +54,7 @@ async def send_error(self, ctx: Interaction | MiscCommandContext[MyBot], error_m async def handle(self, ctx: Interaction | MiscCommandContext[MyBot], error: Exception) -> None | Literal[False]: match error: case CommandNotFound(): # Interactions only - return + return None case NonSpecificError(): return await self.send_error(ctx, str(error)) case MaxConcurrencyReached(): # Interactions only (atm) diff --git a/src/core/errors.py b/src/core/errors.py index bf2bf51..56c5ab5 100644 --- a/src/core/errors.py +++ b/src/core/errors.py @@ -1,7 +1,8 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any, Iterable +from collections.abc import Iterable +from typing import TYPE_CHECKING, Any from discord.app_commands import errors @@ -66,7 +67,7 @@ class NonSpecificError(MixedError): class BotMissingPermissions(MixedError): def __init__(self, perms: Iterable[str]) -> None: self.missing_perms = set(perms) - super().__init__(f"Bot is missing the following permissions: {', '.join(perms)}") + super().__init__(f"Bot is missing the following permissions: {", ".join(perms)}") class NotAllowedUser(MixedError): diff --git a/src/core/extended_commands.py b/src/core/extended_commands.py index 6c9fdb5..754e078 100644 --- a/src/core/extended_commands.py +++ b/src/core/extended_commands.py @@ -1,11 +1,11 @@ from __future__ import annotations +from collections.abc import Callable, Sequence from enum import Enum from functools import wraps from typing import ( TYPE_CHECKING, Any, - Callable, ClassVar, Concatenate, Generic, @@ -13,7 +13,6 @@ ParamSpec, Protocol, Self, - Sequence, TypeVar, cast, runtime_checkable, @@ -41,7 +40,6 @@ P = ParamSpec("P") T = TypeVar("T") C = TypeVar("C", bound="commands.Cog") -_BotType = TypeVar("_BotType", bound="Bot | AutoShardedBot") LiteralNames = Literal["raw_reaction_add", "message"] @@ -128,18 +126,22 @@ def __init__( async def do_call(self, cog: CogT, context: UnresolvedContext, *args: P.args, **kwargs: P.kwargs) -> T: if self.trigger_condition: trigger_condition = await discord.utils.maybe_coroutine( - self.trigger_condition, cog, context, *args, **kwargs # type: ignore + self.trigger_condition, + cog, + context, + *args, + **kwargs, # type: ignore ) if not trigger_condition: - return # type: ignore - resolved_context = await MiscCommandContext.resolve(self.bot, context, self) + return None # type: ignore + resolved_context = await MiscCommandContext[Any].resolve(self.bot, context, self) try: for checker in self.checks: if not await maybe_coroutine(checker, resolved_context): - raise MiscCheckFailure() + raise MiscCheckFailure except MiscCommandError as e: self.bot.dispatch("misc_command_error", resolved_context, e) - return # type: ignore + return None # type: ignore return await self._callback(cog, context, *args, **kwargs) # type: ignore @@ -269,7 +271,7 @@ def bot_permissions(self) -> Permissions: def misc_guild_only() -> Callable[[T], T]: def predicate(ctx: MiscCommandContext[Any]) -> bool: if ctx.channel.guild is None: - raise MiscNoPrivateMessage() + raise MiscNoPrivateMessage return True def decorator(func: T) -> T: diff --git a/src/core/response.py b/src/core/response.py index a3f3aed..12d6684 100644 --- a/src/core/response.py +++ b/src/core/response.py @@ -1,7 +1,8 @@ import logging +from collections.abc import Iterator, Mapping from dataclasses import dataclass from enum import Enum, auto -from typing import Any, Iterator, Mapping +from typing import Any import discord from discord import Color, Embed diff --git a/src/core/transformers.py b/src/core/transformers.py index b4d00e4..542204c 100644 --- a/src/core/transformers.py +++ b/src/core/transformers.py @@ -33,8 +33,8 @@ async def transform(self, inter: Interaction, value: str) -> datetime.datetime: if value.isdigit(): if len(value) > 17: return snowflake_time(int(value)) - return datetime.datetime.fromtimestamp(int(value), tz=datetime.timezone.utc) + return datetime.datetime.fromtimestamp(int(value), tz=datetime.UTC) dt = parse(value) if dt.tzinfo is None: - dt = dt.replace(tzinfo=datetime.timezone.utc) + dt = dt.replace(tzinfo=datetime.UTC) return dt diff --git a/src/core/utils.py b/src/core/utils.py index baf24d6..ae732e9 100644 --- a/src/core/utils.py +++ b/src/core/utils.py @@ -1,5 +1,6 @@ +from collections.abc import AsyncIterable, Iterator, Sequence from enum import Enum, auto -from typing import AsyncIterable, Iterator, Literal, Sequence, TypeVar +from typing import Literal, TypeVar T = TypeVar("T") @@ -36,6 +37,6 @@ def size_text(string: str, size: int, mode: Literal["end", "middle"] = "end") -> if len(string) < size: return string if mode == "end": - return f"{string[:size - 1]}…" + return f"{string[: size - 1]}…" elif mode == "middle": return f"{string[: size // 2 - 10]}\n… {len(string) - size} more …{string[-size // 2 :]}" diff --git a/src/core/view_menus.py b/src/core/view_menus.py index c075e64..dd2fefa 100644 --- a/src/core/view_menus.py +++ b/src/core/view_menus.py @@ -1,7 +1,8 @@ from __future__ import annotations +import contextlib import os -from typing import TYPE_CHECKING, Any, Generic, Self, TypeVar +from typing import TYPE_CHECKING, Any, Generic, Self import discord from discord import ui @@ -68,16 +69,14 @@ async def update(self) -> None: def disable_view(self): for item in self.children: - if isinstance(item, (ui.Button, ui.Select)): + if isinstance(item, ui.Button | ui.Select): item.disabled = True async def on_timeout(self) -> None: if self.message_attached_to: self.disable_view() - try: + with contextlib.suppress(discord.NotFound, discord.HTTPException): await self.message_attached_to.edit(view=self) - except (discord.NotFound, discord.HTTPException): - pass async def message_display(self) -> MessageDisplay | UneditedMessageDisplay: """This function can be defined and used in order to add a message content (embeds, etc...) within the menu.""" diff --git a/src/libraries/libre_translate/_types.py b/src/libraries/libre_translate/_types.py index e244247..32824ec 100644 --- a/src/libraries/libre_translate/_types.py +++ b/src/libraries/libre_translate/_types.py @@ -1,12 +1,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING, TypeAlias, TypedDict +from typing import TYPE_CHECKING, TypedDict if TYPE_CHECKING: from .languages import Language -RawDetectionsResponse: TypeAlias = list["RawDetections"] +type RawDetectionsResponse = list["RawDetections"] +type Detections = list["Detection"] class RawDetections(TypedDict): @@ -14,9 +15,6 @@ class RawDetections(TypedDict): language: str -Detections: TypeAlias = list["Detection"] - - class Detection(TypedDict): confidence: float language: Language diff --git a/src/libraries/libre_translate/main.py b/src/libraries/libre_translate/main.py index b8c638b..e4bb952 100644 --- a/src/libraries/libre_translate/main.py +++ b/src/libraries/libre_translate/main.py @@ -49,4 +49,4 @@ async def translate(self, text: str, to: Language, from_: Language | Literal["au return raw["translatedText"] async def detect(self) -> Never: - raise NotImplementedError() + raise NotImplementedError diff --git a/src/libraries/microsoft_translation/translator.py b/src/libraries/microsoft_translation/translator.py index 381529a..3804999 100644 --- a/src/libraries/microsoft_translation/translator.py +++ b/src/libraries/microsoft_translation/translator.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Any, Iterable +from collections.abc import Iterable +from typing import Any from urllib.parse import urljoin import aiohttp diff --git a/src/main.py b/src/main.py index 6dd8c44..384d1e3 100644 --- a/src/main.py +++ b/src/main.py @@ -53,7 +53,7 @@ def bot( kwargs: dict[str, Any] = {} if missing_env_var := required_env_var - set(os.environ): - raise RuntimeError(f"The following environment variables are missing: {', '.join(missing_env_var)}") + raise RuntimeError(f"The following environment variables are missing: {", ".join(missing_env_var)}") if len({"MS_TRANSLATE_KEY", "MS_TRANSLATE_REGION"} & set(os.environ)) == 1: raise RuntimeError("MS_TRANSLATE_KEY and MS_TRANSLATE_REGION should be either both defined or both undefined.") diff --git a/src/mybot.py b/src/mybot.py index 6a00962..d91aae2 100644 --- a/src/mybot.py +++ b/src/mybot.py @@ -3,7 +3,7 @@ import logging import re import sys -from typing import TYPE_CHECKING, Any, Type, cast +from typing import TYPE_CHECKING, Any, cast import discord import topgg as topggpy @@ -126,7 +126,7 @@ async def update_guild_count_on_bot_lists(self): try: await self.topgg.post_guild_count(guild_count=len(self.guilds), shard_count=self.shard_count) except Exception as e: - logger.error("Failed to post guild count to top.gg.", exc_info=e) + logger.exception("Failed to post guild count to top.gg.", exc_info=e) async def topgg_endpoint(self, vote_data: topggpy.types.BotVoteData) -> None: logger.debug("Received vote from top.gg", extra=vote_data) @@ -154,7 +154,7 @@ async def sync_tree(self) -> None: try: await self.tree.sync(guild=discord.Object(guild_id)) except discord.Forbidden as e: - logger.error("Failed to sync guild %s.", guild_id, exc_info=e) + logger.exception("Failed to sync guild %s.", guild_id, exc_info=e) self.app_commands = await self.tree.sync() async def on_ready(self) -> None: @@ -268,10 +268,7 @@ async def support_invite(self) -> discord.Invite: self._invite = get(await self.support.invites(), max_age=0, max_uses=0, inviter=self.user) if self._invite is None: # If invite is STILL None - if tmp := self.support.rules_channel: - channel = tmp - else: - channel = self.support.channels[0] + channel = tmp if (tmp := self.support.rules_channel) else self.support.channels[0] self._invite = await channel.create_invite(reason="Support guild invite.") @@ -285,7 +282,7 @@ async def load_extensions(self) -> None: try: await self.load_extension(ext) except errors.ExtensionError as e: - logger.error("Failed to load extension %s.", ext, exc_info=e) + logger.exception("Failed to load extension %s.", ext, exc_info=e) else: logger.info("Extension %s loaded successfully.", ext) @@ -319,7 +316,7 @@ async def getch_channel(self, id: int, /) -> GuildChannel | PrivateChannel | Thr return None return channel - async def get_or_create_db[T: Type[Base]](self, session: AsyncSession, table: T, **key: Any) -> T: + async def get_or_create_db[T: type[Base]](self, session: AsyncSession, table: T, **key: Any) -> T: """Get an object from the database. If it doesn't exist, it is created. It is CREATEd if the guild doesn't exist in the database. """ @@ -341,8 +338,7 @@ def misc_commands(self): misc_commands: list[MiscCommand[Any, ..., Any]] = [] for cog in self.cogs.values(): if isinstance(cog, ExtendedCog): - for misc_command in cog.get_misc_commands(): - misc_commands.append(misc_command) + misc_commands.extend(cog.get_misc_commands()) return misc_commands async def on_error(self, event_method: str, /, *args: Any, **kwargs: Any) -> None: