diff --git a/.github/workflows/lint_python.yaml b/.github/workflows/lint_python.yaml index 06d77250..49f60f63 100644 --- a/.github/workflows/lint_python.yaml +++ b/.github/workflows/lint_python.yaml @@ -17,10 +17,10 @@ jobs: - uses: actions/checkout@v6 with: ref: ${{ env.ref }} - - uses: actions/setup-python@v6 + - uses: astral-sh/ruff-action@v3 with: - python-version: "3.9" - - run: "python -m pip install git+https://github.com/pycqa/pyflakes@1911c20#egg=pyflakes git+https://github.com/pycqa/pycodestyle@d219c68#egg=pycodestyle git+https://github.com/pycqa/flake8@3.7.9#egg=flake8" - name: Install Flake8 - - run: "python -m flake8 . --count --select=E9,F7,F82 --show-source" - name: Flake8 Linting + version: "0.15.9" + - uses: astral-sh/ruff-action@v3 + with: + version: "0.15.9" + args: "format --check" diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..42f20c5d --- /dev/null +++ b/.gitignore @@ -0,0 +1,216 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bfd3fd32..9f228957 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,13 +5,9 @@ repos: - id: mixed-line-ending - id: end-of-file-fixer - id: trailing-whitespace -- repo: https://github.com/psf/black-pre-commit-mirror - rev: 26.3.1 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.9 hooks: - - id: black - args: [--target-version=py39, --line-length=99] -- repo: https://github.com/PyCQA/isort - rev: 8.0.1 - hooks: - - id: isort - args: [--profile=black, --line-length=99] + - id: ruff + args: [--fix, --unsafe-fixes] + - id: ruff-format diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f8074c27..e9df30f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,25 +20,21 @@ All contributions must be your own original work. If i suspect a contribution wa ## Guidelines for Submitting PRs Always use the following: -- Black -- isort +- Ruff -## How to Use Black -- First, install Black: +## How to Use Ruff +- First, install Ruff: ```bash -pip install black +pip install ruff ``` -- Then run +- Then run the linter (fixes imports, style, and common issues automatically): ```bash -black --line-length 99 +ruff check --fix ``` - -## How to use isort? -- First you will have to install it +- Then run the formatter: ```bash -[p]pip install isort +ruff format ``` -- Then run -```bash -isort + +Both commands must be run before submitting a PR. Ruff is configured in `pyproject.toml` at the root of the repo — no extra flags needed beyond what's shown above. ``` diff --git a/autopublisher/__init__.py b/autopublisher/__init__.py index c51ba37e..9548a530 100644 --- a/autopublisher/__init__.py +++ b/autopublisher/__init__.py @@ -2,6 +2,7 @@ from .autopublisher import AutoPublisher + __red_end_user_data_statement__ = "This cog does not persistently store data about users." diff --git a/autopublisher/autopublisher.py b/autopublisher/autopublisher.py index a27bdf13..cfd5561e 100644 --- a/autopublisher/autopublisher.py +++ b/autopublisher/autopublisher.py @@ -37,6 +37,7 @@ from .utils import increment_published_count, initialize_scheduler, schedule_resets from .view import IgnoredNewsChannelsView, MetricsView + logger = getLogger("red.maxcogs.autopublisher") diff --git a/autopublisher/dashboard_integration.py b/autopublisher/dashboard_integration.py index 1c45d2c5..b689d5dc 100644 --- a/autopublisher/dashboard_integration.py +++ b/autopublisher/dashboard_integration.py @@ -23,7 +23,7 @@ """ from datetime import datetime -from typing import Any, Dict +from typing import Any import discord import pytz @@ -51,7 +51,7 @@ async def on_dashboard_cog_add(self, dashboard_cog: commands.Cog) -> None: dashboard_cog.rpc.third_parties_handler.add_third_party(self) @dashboard_page(name="stats", description="View AutoPublisher statistics", is_owner=True) - async def dashboard_stats(self, user: discord.User, **kwargs) -> Dict[str, Any]: + async def dashboard_stats(self, user: discord.User, **kwargs) -> dict[str, Any]: """Dashboard page to display AutoPublisher stats.""" owner_tz = await get_owner_timezone(self.config) data = await self.config.all() diff --git a/autopublisher/utils.py b/autopublisher/utils.py index 958f900a..e4cedbcd 100644 --- a/autopublisher/utils.py +++ b/autopublisher/utils.py @@ -29,6 +29,7 @@ from red_commons.logging import getLogger from redbot.core import Config, commands + logger = getLogger("red.maxcogs.autopublisher.utils") diff --git a/autopublisher/view.py b/autopublisher/view.py index b6af186a..976800de 100644 --- a/autopublisher/view.py +++ b/autopublisher/view.py @@ -29,11 +29,11 @@ import pytz from red_commons.logging import getLogger from redbot.core import commands -from redbot.core.utils.chat_formatting import box, header, humanize_number -from tabulate import tabulate +from redbot.core.utils.chat_formatting import box, humanize_number from .utils import get_next_reset_times, get_owner_timezone + log = getLogger("red.maxcogs.autopublisher.view") @@ -91,7 +91,7 @@ async def start(self, ctx: commands.Context) -> None: async def interaction_check(self, interaction: discord.Interaction) -> bool: """Check if the user is allowed to interact.""" - if interaction.user.id not in [self.ctx.author.id] + list(self.ctx.bot.owner_ids): + if interaction.user.id not in [self.ctx.author.id, *list(self.ctx.bot.owner_ids)]: await interaction.response.send_message( "You are not allowed to use this interaction.", ephemeral=True ) @@ -184,13 +184,12 @@ def rate(val: int, days: int) -> str: return f"{val / days:.1f}" if days > 0 else "N/A" import calendar - from datetime import datetime from datetime import timezone as dt_timezone now = datetime.now(owner_tz) days_in_month = calendar.monthrange(now.year, now.month)[1] day_of_year = now.timetuple().tm_yday - week_day = now.weekday() + 1 + now.weekday() + 1 weekly_avg = rate(weekly, 7) monthly_avg = rate(monthly, days_in_month) @@ -207,7 +206,7 @@ def total_bar(val: int) -> str: return FILL * filled + EMPTY * (BAR_WIDTH - filled) pct_weekly_of_monthly = f"{(weekly / monthly * 100):.1f}%" if monthly else "N/A" - pct_yearly_of_total = f"{(yearly / total * 100):.1f}%" if total else "N/A" + pct_yearly_of_total = f"{(yearly / total * 100):.1f}%" if total else "N/A" last_pub = "Never" if last_count_time: @@ -265,7 +264,7 @@ def __init__(self, cog: commands.Cog) -> None: self.cog = cog self.ctx: commands.Context | None = None self.message: discord.Message | None = None - self.owner_tz: "pytz.timezone | None" = None + self.owner_tz: pytz.timezone | None = None self.refresh_button = discord.ui.Button( label="Refresh", style=discord.ButtonStyle.green, emoji="🔄" @@ -317,7 +316,7 @@ async def _update_view(self) -> None: self.add_item(self.container) async def interaction_check(self, interaction: discord.Interaction) -> bool: - if interaction.user.id not in [self.ctx.author.id] + list(self.ctx.bot.owner_ids): + if interaction.user.id not in [self.ctx.author.id, *list(self.ctx.bot.owner_ids)]: await interaction.response.send_message( "You are not allowed to use this interaction.", ephemeral=True ) diff --git a/counting/__init__.py b/counting/__init__.py index 54280e0f..fcea511e 100644 --- a/counting/__init__.py +++ b/counting/__init__.py @@ -2,6 +2,7 @@ from .counting import Counting + __red_end_user_data_statement__ = "This cog does not persistently store data about users." diff --git a/counting/commands/admin.py b/counting/commands/admin.py index 2f82b376..eb64c557 100644 --- a/counting/commands/admin.py +++ b/counting/commands/admin.py @@ -38,6 +38,7 @@ from ..toggle_view import ToggleSetupView, _build_embed + logger = getLogger("red.maxcogs.counting") diff --git a/counting/commands/user.py b/counting/commands/user.py index ecc53b33..f2d32a77 100644 --- a/counting/commands/user.py +++ b/counting/commands/user.py @@ -112,7 +112,7 @@ async def leaderboard(self, ctx: commands.Context) -> None: numalign="left", ) embed = discord.Embed( - title=f"🏆 Counting Global Leaderboard", + title="🏆 Counting Global Leaderboard", description=box(table, lang="prolog"), color=await ctx.embed_color(), ) diff --git a/counting/event_handlers.py b/counting/event_handlers.py index dc7622d8..7547e79e 100644 --- a/counting/event_handlers.py +++ b/counting/event_handlers.py @@ -22,7 +22,6 @@ SOFTWARE. """ -import asyncio from datetime import datetime, timezone from typing import Any @@ -39,6 +38,7 @@ send_message, ) + logger = getLogger("red.maxcogs.counting.event_handlers") diff --git a/counting/toggle_view.py b/counting/toggle_view.py index 1bbc6c1b..7f24291a 100644 --- a/counting/toggle_view.py +++ b/counting/toggle_view.py @@ -28,6 +28,7 @@ from red_commons.logging import getLogger from redbot.core import commands + log = getLogger("red.maxcogs.counting.toggle_view") TOGGLES: list[tuple[str, str]] = [ ("toggle", "Counting"), @@ -61,7 +62,6 @@ def _build_embed(settings: dict[str, Any], color: discord.Color) -> discord.Embe class ToggleSetupView(discord.ui.View): - def __init__( self, ctx: commands.Context, @@ -89,7 +89,7 @@ def _add_buttons(self) -> None: def _make_callback(self, config_key: str): async def callback(interaction: discord.Interaction) -> None: - if interaction.user.id not in [self.ctx.author.id] + list(self.ctx.bot.owner_ids): + if interaction.user.id not in [self.ctx.author.id, *list(self.ctx.bot.owner_ids)]: return await interaction.response.send_message( "You are not allowed to use this.", ephemeral=True ) diff --git a/counting/utils.py b/counting/utils.py index 88f00bd8..4c3b28dd 100644 --- a/counting/utils.py +++ b/counting/utils.py @@ -30,6 +30,7 @@ from red_commons.logging import getLogger from redbot.core import Config + logger = getLogger("red.maxcogs.counting.utils") diff --git a/currency/__init__.py b/currency/__init__.py index 3480f2d5..ee3fadb5 100644 --- a/currency/__init__.py +++ b/currency/__init__.py @@ -2,6 +2,7 @@ from .currency import Currency + __red_end_user_data_statement__ = "This cog does not persistently store data about users." diff --git a/currency/currency.py b/currency/currency.py index e34c6192..65ec1db9 100644 --- a/currency/currency.py +++ b/currency/currency.py @@ -31,6 +31,7 @@ from redbot.core import app_commands, commands from redbot.core.utils.views import SetApiView + logger = getLogger("red.maxcogs.currency") _CURRENCY_ALIASES: dict[str, str] = { @@ -183,7 +184,7 @@ async def convert_currency( except ValueError as e: logger.error(f"Conversion error: {e}", exc_info=True) await interaction.followup.send( - f"Failed to fetch exchange rate data. Please try again later.", + "Failed to fetch exchange rate data. Please try again later.", ephemeral=True, ) except discord.HTTPException as e: diff --git a/earthquake/__init__.py b/earthquake/__init__.py index f181165a..d7be040e 100644 --- a/earthquake/__init__.py +++ b/earthquake/__init__.py @@ -3,6 +3,7 @@ from .earthquake import Earthquake + __red_end_user_data_statement__ = get_end_user_data_statement(__file__) diff --git a/earthquake/earthquake.py b/earthquake/earthquake.py index d4fdaa39..059bd5ee 100644 --- a/earthquake/earthquake.py +++ b/earthquake/earthquake.py @@ -24,8 +24,7 @@ import asyncio import datetime -from operator import itemgetter -from typing import Dict, Final, List, Optional +from typing import Final, Optional import aiohttp import discord @@ -35,6 +34,7 @@ from redbot.core import Config, commands from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential + logger = getLogger("red.maxcogs.earthquake") @@ -73,7 +73,7 @@ async def red_delete_data_for_user(self, *, requester: str, user_id: int) -> Non """No user data to delete.""" pass - async def get_guild_settings(self) -> Dict[int, Dict]: + async def get_guild_settings(self) -> dict[int, dict]: """Cache guild settings to reduce config calls.""" return {guild.id: await self.config.guild(guild).all() for guild in self.bot.guilds} @@ -85,7 +85,7 @@ async def get_guild_settings(self) -> Dict[int, Dict]: f"Retrying USGS fetch (attempt {retry_state.attempt_number})" ), ) - async def fetch_earthquakes(self, min_magnitude: float = 1.0) -> List[Dict]: + async def fetch_earthquakes(self, min_magnitude: float = 1.0) -> list[dict]: # Validate min_magnitude try: min_magnitude = float(min_magnitude) @@ -172,7 +172,7 @@ async def earthquake_check(self): magnitude = earthquake["properties"].get("mag") if magnitude is None or magnitude < min_magnitude: continue - send_time = datetime.datetime.now(datetime.timezone.utc) + datetime.datetime.now(datetime.timezone.utc) await self.post_earthquake(guild, channel, earthquake) except asyncio.CancelledError: @@ -274,23 +274,20 @@ async def post_earthquake( ) sent = False - if use_webhook: - if channel.permissions_for(guild.me).manage_webhooks: - wh = await self.get_or_create_webhook(channel) - if wh: - try: - await wh.send( - content=content, - embed=embed, - username=self.bot.user.name, - avatar_url=( - str(self.bot.user.avatar) if self.bot.user.avatar else None - ), - allowed_mentions=discord.AllowedMentions(roles=True), - ) - sent = True - except (discord.Forbidden, discord.HTTPException) as e: - logger.error(f"Failed to send via webhook in {guild.name}: {e}") + if use_webhook and channel.permissions_for(guild.me).manage_webhooks: + wh = await self.get_or_create_webhook(channel) + if wh: + try: + await wh.send( + content=content, + embed=embed, + username=self.bot.user.name, + avatar_url=(str(self.bot.user.avatar) if self.bot.user.avatar else None), + allowed_mentions=discord.AllowedMentions(roles=True), + ) + sent = True + except (discord.Forbidden, discord.HTTPException) as e: + logger.error(f"Failed to send via webhook in {guild.name}: {e}") if not sent: if ( @@ -411,7 +408,7 @@ async def set_magnitude( ) @earthquakeset.command(name="safety") - async def set_safety_message(self, ctx: commands.Context, *, message: str = None): + async def set_safety_message(self, ctx: commands.Context, *, message: Optional[str] = None): """Set or clear a custom safety message for alerts.""" if message: if message and len(message) > 1024: diff --git a/easterhunt/__init__.py b/easterhunt/__init__.py index 3b9c7c03..8d3237ea 100644 --- a/easterhunt/__init__.py +++ b/easterhunt/__init__.py @@ -3,6 +3,7 @@ from .easterhunt import EasterHunt + __red_end_user_data_statement__ = get_end_user_data_statement(__file__) diff --git a/easterhunt/commands/user.py b/easterhunt/commands/user.py index 56671613..98eb6a16 100644 --- a/easterhunt/commands/user.py +++ b/easterhunt/commands/user.py @@ -23,6 +23,7 @@ """ import asyncio +import contextlib import random import time from datetime import datetime, timedelta @@ -43,6 +44,7 @@ ) from ..view import EasterWork + log = getLogger("red.maxcogs.easterhunt") @@ -105,9 +107,12 @@ async def hunt(self, ctx: commands.Context): event_task = asyncio.create_task(self.send_hunt_events(ctx.channel, user)) await asyncio.sleep(60) current_streak = await update_hunt_streak(self.db, user.id, current_time) - adjusted_chances, pity_counters, can_roll_legendary, can_roll_mythical = ( - await calculate_hunt_probabilities(self.db, user.id, current_streak) - ) + ( + adjusted_chances, + pity_counters, + can_roll_legendary, + can_roll_mythical, + ) = await calculate_hunt_probabilities(self.db, user.id, current_streak) outcomes = [ "nothing", "common", @@ -561,7 +566,7 @@ async def resetme(self, ctx: commands.Context): description="Are you sure you want to reset your Easter hunt data? This will clear all your eggs, shards, gems, pity counters, and streaks. This action cannot be undone!", color=discord.Color.red(), ) - msg = await ctx.send( + _msg = await ctx.send( embed=embed, view=view, reference=ctx.message.to_reference(fail_if_not_exists=False), @@ -671,12 +676,10 @@ async def start_job(self, interaction, job_type, user): ) except discord.HTTPException as e: log.error(f"Error starting job for {user}: {e}") - try: + with contextlib.suppress(discord.HTTPException): await interaction.channel.send( f"{user.mention}, something went wrong starting your shift! It has been cancelled." ) - except discord.HTTPException: - pass await self.db.set_user_field(user.id, "active_work", False) await self.db.set_user_field(user.id, "last_work", 0) await self.db.set_user_field(user.id, "active_job_type", None) @@ -759,12 +762,10 @@ async def run_job(self, interaction, job_type, user, work_ends): pass except Exception as e: log.error(f"Unexpected error in run_job for {user}: {e}") - try: + with contextlib.suppress(discord.HTTPException): await interaction.channel.send( f"{user.mention}, something went wrong during your shift! It has been cancelled." ) - except discord.HTTPException: - pass finally: await self.db.set_user_field(user.id, "active_work", False) await self.db.set_user_field(user.id, "last_work", 0) diff --git a/easterhunt/db.py b/easterhunt/db.py index 48ee5169..1bae975a 100644 --- a/easterhunt/db.py +++ b/easterhunt/db.py @@ -22,9 +22,10 @@ SOFTWARE. """ +import contextlib import json import random -from typing import Dict, List, Optional, Tuple +from typing import Optional import aiosqlite import discord @@ -81,12 +82,10 @@ async def create_tables(self): async with self.conn.cursor() as cursor: for query in queries: await cursor.execute(query) - try: + with contextlib.suppress(Exception): await cursor.execute( "ALTER TABLE users ADD COLUMN active_job_type TEXT DEFAULT NULL" ) - except Exception: - pass await self.conn.commit() async def ensure_user(self, user_id: int): @@ -112,7 +111,7 @@ async def set_user_field(self, user_id: int, field: str, value): ) await self.conn.commit() - async def get_eggs(self, user_id: int) -> Dict[str, int]: + async def get_eggs(self, user_id: int) -> dict[str, int]: await self.ensure_user(user_id) async with self.conn.cursor() as cursor: await cursor.execute( @@ -148,21 +147,21 @@ async def set_egg_count(self, user_id: int, egg_type: str, value: int): ) await self.conn.commit() - async def get_pity_counters(self, user_id: int) -> Dict[str, int]: + async def get_pity_counters(self, user_id: int) -> dict[str, int]: json_str = await self.get_user_field(user_id, "pity_counter_json") return orjson.loads(json_str) - async def set_pity_counters(self, user_id: int, data: Dict[str, int]): + async def set_pity_counters(self, user_id: int, data: dict[str, int]): await self.set_user_field(user_id, "pity_counter_json", json.dumps(data)) - async def get_achievements(self, user_id: int) -> Dict[str, bool]: + async def get_achievements(self, user_id: int) -> dict[str, bool]: json_str = await self.get_user_field(user_id, "achievements_json") return orjson.loads(json_str) - async def set_achievements(self, user_id: int, data: Dict[str, bool]): + async def set_achievements(self, user_id: int, data: dict[str, bool]): await self.set_user_field(user_id, "achievements_json", json.dumps(data)) - async def get_egg_images(self) -> Dict[str, str]: + async def get_egg_images(self) -> dict[str, str]: async with self.conn.cursor() as cursor: await cursor.execute("SELECT egg_type, image_url FROM egg_images") rows = await cursor.fetchall() @@ -184,7 +183,7 @@ async def delete_user_data(self, user_id: int): await cursor.execute("DELETE FROM users WHERE user_id = ?", (user_id,)) await self.conn.commit() - async def get_stale_active_users(self) -> List[Tuple[int, float]]: + async def get_stale_active_users(self) -> list[tuple[int, float]]: async with self.conn.cursor() as cursor: await cursor.execute("SELECT user_id, last_work FROM users WHERE active_work = 1") return await cursor.fetchall() @@ -201,21 +200,23 @@ async def reset_all(self): await cursor.execute("DELETE FROM egg_images") await self.conn.commit() - async def get_leaderboard_data(self) -> List[Tuple[int, int]]: + async def get_leaderboard_data(self) -> list[tuple[int, int]]: async with self.conn.cursor() as cursor: - await cursor.execute(""" + await cursor.execute( + """ SELECT user_id, SUM(count) as total FROM user_eggs WHERE egg_type IN ('common', 'silver', 'gold') GROUP BY user_id HAVING total > 0 ORDER BY total DESC - """) + """ + ) return await cursor.fetchall() async def find_target_player( self, user_id: int, guild - ) -> Tuple[Optional[discord.Member], Optional[str]]: + ) -> tuple[Optional[discord.Member], Optional[str]]: """Find a random guild member with eggs to steal from, excluding the requesting user.""" potential_targets = [] async with self.conn.cursor() as cursor: diff --git a/easterhunt/easterhunt.py b/easterhunt/easterhunt.py index 9ee5d47b..0bc2d0c4 100644 --- a/easterhunt/easterhunt.py +++ b/easterhunt/easterhunt.py @@ -23,6 +23,7 @@ """ import asyncio +import contextlib import time from typing import Final @@ -34,6 +35,7 @@ from .commands.user import UserCommands from .db import Database + log = getLogger("red.maxcogs.easterhunt") @@ -105,12 +107,10 @@ async def resume_job(self, user, remaining_time, job_type): if remaining_time > 0: await asyncio.sleep(remaining_time) result_message = await self._execute_job_outcome(user.id, job_type, guild=None) - try: + with contextlib.suppress(discord.HTTPException): await user.send( f"🐰 Your shift as a **{job_type.replace('_', ' ').title()}** finished while the bot was restarting!\n{result_message}" ) - except discord.HTTPException: - pass except asyncio.CancelledError: pass finally: diff --git a/easterhunt/utils.py b/easterhunt/utils.py index 3c8c29aa..7726b4b7 100644 --- a/easterhunt/utils.py +++ b/easterhunt/utils.py @@ -23,7 +23,6 @@ """ import random -from typing import Dict, Tuple import discord @@ -54,7 +53,7 @@ async def update_hunt_streak(db, user_id: int, current_time: float) -> int: async def calculate_hunt_probabilities( db, user_id: int, current_streak: int -) -> Tuple[Dict[str, int], Dict[str, int], bool, bool]: +) -> tuple[dict[str, int], dict[str, int], bool, bool]: """Calculate adjusted probabilities with pity and streak bonuses.""" pity_counters = await db.get_pity_counters(user_id) eggs = await db.get_eggs(user_id) @@ -112,7 +111,7 @@ async def process_hunt_outcome( db, user_id: int, result: str, - pity_counters: Dict[str, int], + pity_counters: dict[str, int], can_roll_legendary: bool, can_roll_mythical: bool, ) -> discord.Embed: diff --git a/easterhunt/view.py b/easterhunt/view.py index 9e06673b..4d38670a 100644 --- a/easterhunt/view.py +++ b/easterhunt/view.py @@ -26,6 +26,7 @@ from red_commons.logging import getLogger from redbot.core.utils.chat_formatting import header + log = getLogger("red.maxcogs.easterhunt.view") diff --git a/enforce/__init__.py b/enforce/__init__.py index 47de8762..c77a1a05 100644 --- a/enforce/__init__.py +++ b/enforce/__init__.py @@ -2,6 +2,7 @@ from .enforce import Enforce + __red_end_user_data_statement__ = "This cog does not persistently store data about users." diff --git a/enforce/enforce.py b/enforce/enforce.py index fa67752a..8e7dc923 100644 --- a/enforce/enforce.py +++ b/enforce/enforce.py @@ -36,6 +36,7 @@ from .views import AcceptView + log = getLogger("red.maxcogs.tosenforcer") @@ -303,7 +304,7 @@ async def reset_all(self, ctx): async def accepted_count(self, ctx: commands.Context) -> None: """Show how many users have accepted the ToS.""" all_users = await self.config.all_users() - total = len(all_users) + len(all_users) member_count = len(self.bot.users) accepted = sum(1 for u in all_users.values() if u.get("accepted_tos", False)) await ctx.send( diff --git a/enforce/views.py b/enforce/views.py index 1830c7ec..a1287f39 100644 --- a/enforce/views.py +++ b/enforce/views.py @@ -26,6 +26,7 @@ from red_commons.logging import getLogger from redbot.core import Config + log = getLogger("red.maxcogs.enforce.views") diff --git a/github/__init__.py b/github/__init__.py index d916653a..f4151aa8 100644 --- a/github/__init__.py +++ b/github/__init__.py @@ -3,6 +3,7 @@ from .github import GitHub + with open(Path(__file__).parent / "info.json") as fp: __red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"] diff --git a/github/github.py b/github/github.py index e5814c35..8ced8643 100644 --- a/github/github.py +++ b/github/github.py @@ -28,7 +28,7 @@ import re import typing from datetime import datetime, timezone -from typing import Final, List, Optional +from typing import Final, Optional from urllib.parse import urlparse import aiohttp @@ -42,6 +42,7 @@ from .converters import ExplicitNone + log = logging.getLogger("red.maxcogs.github") # Constants @@ -69,7 +70,7 @@ class GitHub(commands.Cog): """ __version__: Final[str] = "1.0.1" - __author__: Final[List[str]] = ["MAX", "Obi-Wan3"] + __author__: Final[list[str]] = ["MAX", "Obi-Wan3"] __docs__: Final[str] = "https://cogs.maxapp.tv/" def __init__(self, bot): @@ -523,7 +524,7 @@ async def _list_all(self, ctx: commands.Context): if not feeds_string: return await ctx.send("No GitHub RSS feeds have been set up in this server yet.") - embeds: typing.List[discord.Embed] = [] + embeds: list[discord.Embed] = [] for page in pagify(feeds_string, delims=["\n\n"]): embeds.append(discord.Embed(description=page, color=await ctx.embed_color())) @@ -536,13 +537,11 @@ async def _view(self, ctx: commands.Context): """View the server settings for GitHub.""" settings = await self.config.guild(ctx.guild).all() - if channel := settings["channel"]: - if not (channel := ctx.guild.get_channel(channel)): - channel = None + channel_id = settings["channel"] + channel = ctx.guild.get_channel(channel_id) if channel_id else None - if role := settings["role"]: - if not (role := ctx.guild.get_role(role)): - role = None + role_id = settings["role"] + role = ctx.guild.get_role(role_id) if role_id else None return await ctx.send( embed=discord.Embed( @@ -585,7 +584,7 @@ async def _get( ctx: commands.Context, entries: typing.Optional[int], url: str, - branch: str = None, + branch: Optional[str] = None, ): """Test out fetching a GitHub repository url.""" @@ -644,7 +643,6 @@ async def _add(self, ctx: commands.Context, name: str, url: str, branch: Optiona # Set user config async with self.config.member(ctx.author).feeds() as feeds: - # Checks if name in feeds: return await ctx.send("There is already a feed with that name!") @@ -744,7 +742,7 @@ async def _list(self, ctx: commands.Context): f"No feeds found. Try adding one with `{ctx.clean_prefix}github add`!" ) - embeds: typing.List[discord.Embed] = [] + embeds: list[discord.Embed] = [] for page in pagify(feeds_string): embeds.append(discord.Embed(description=page, color=await ctx.embed_color())) @@ -755,7 +753,6 @@ async def _list(self, ctx: commands.Context): async def _do_rss_check(self, guild_to_check: Optional[int] = None) -> None: # Loop through each guild for guild_id, guild_config in (await self.config.all_guilds()).items(): - # Check for single guild if guild_to_check and guild_id != guild_to_check: continue @@ -778,7 +775,6 @@ async def _do_rss_check(self, guild_to_check: Optional[int] = None) -> None: async for member_id, member_data in AsyncIter( (await self.config.all_members(guild)).items(), steps=100 ): - # Loop through each feed for name, feed in member_data["feeds"].items(): try: @@ -799,16 +795,14 @@ async def _do_rss_check(self, guild_to_check: Optional[int] = None) -> None: timestamp=guild_config["timestamp"], short=guild_config["short"], ): - # Get channel (guild vs feed override) ch = channel - if feed["channel"]: - if not ( - (ch := guild.get_channel(feed["channel"])) - and ch.permissions_for(guild.me).send_messages - and ch.permissions_for(guild.me).embed_links - ): - ch = None + if feed["channel"] and not ( + (ch := guild.get_channel(feed["channel"])) + and ch.permissions_for(guild.me).send_messages + and ch.permissions_for(guild.me).embed_links + ): + ch = None # Send feed embed if ch: diff --git a/heist/handlers.py b/heist/handlers.py index 056e8a50..71499712 100644 --- a/heist/handlers.py +++ b/heist/handlers.py @@ -24,15 +24,15 @@ import asyncio import datetime -import logging import random +from typing import Optional import discord from red_commons.logging import getLogger from redbot.core import bank, errors -from redbot.core.utils.views import ConfirmView -from .utils import HEISTS, ITEMS, fmt +from .utils import ITEMS, fmt + log = getLogger("red.cogs.heist.handlers") @@ -69,7 +69,7 @@ async def resolve_heist( user: discord.User, heist_type: str, channel: discord.TextChannel, - fallback_channel_id: int = None, + fallback_channel_id: Optional[int] = None, ): member = None try: @@ -180,7 +180,7 @@ async def resolve_heist( msg_parts.append("Failed — but your shield prevented any loss.") if used_tool: - msg_parts.append(f"(Used {fmt(used_tool)} → +{tool_boost*100:.0f}% success chance)") + msg_parts.append(f"(Used {fmt(used_tool)} → +{tool_boost * 100:.0f}% success chance)") heat += 1 await user_config.heat.set(heat) @@ -268,7 +268,7 @@ async def resolve_heist( if fb: try: await fb.send(f"{member.mention} {msg}") - except: + except Exception: log.warning("Fallback %s also failed", fallback_channel_id) except Exception as e: diff --git a/heist/heist.py b/heist/heist.py index 54ebc00c..153745e5 100644 --- a/heist/heist.py +++ b/heist/heist.py @@ -25,7 +25,7 @@ import asyncio import datetime import random -from typing import Final +from typing import Final, Optional import discord from red_commons.logging import getLogger @@ -36,6 +36,7 @@ from .utils import HEISTS, ITEMS, RECIPES from .views import HeistConfigView, HeistView, ItemPriceConfigView, ShopView + log = getLogger("red.cogs.heist") @@ -133,7 +134,9 @@ async def _consume_item(self, member: discord.Member, item_name: str): del inventory[item_name] await self.config.user(member).inventory.set(inventory) - async def _has_active_heist(self, user: discord.Member, channel_id: int = None) -> bool: + async def _has_active_heist( + self, user: discord.Member, channel_id: Optional[int] = None + ) -> bool: active = await self.config.user(user).active_heist() if not active: return False @@ -432,11 +435,11 @@ async def buy_item(self, ctx: commands.Context): cost = await self.get_item_cost(name) effect = "" if data["type"] == "shield": - effect = f"Reduces loss by {data['reduction']*100:.1f}% (single use)" + effect = f"Reduces loss by {data['reduction'] * 100:.1f}% (single use)" elif data["type"] == "tool": - effect = f"Boosts success by {data['boost']*100:.0f}% for {data['for_heist'].replace('_', ' ').title()} (single use)" + effect = f"Boosts success by {data['boost'] * 100:.0f}% for {data['for_heist'].replace('_', ' ').title()} (single use)" elif data["type"] == "consumable": - effect = f"Reduces risk by {data['risk_reduction']*100:.0f}% (single use)" + effect = f"Reduces risk by {data['risk_reduction'] * 100:.0f}% (single use)" embed.add_field( name=f"{emoji} {name.replace('_', ' ').title()}", value=f"**Cost**: {cost:,} {currency_name}\n**Effect**: {effect}", @@ -473,7 +476,7 @@ async def do_heist(self, ctx: commands.Context): # Batch-read all heist settings in one config call instead of N calls _raw_settings = await self.config.heist_settings() heist_settings = {} - for name in HEISTS.keys(): + for name in HEISTS: defaults = HEISTS[name] custom = _raw_settings.get(name, {}) heist_settings[name] = { @@ -523,7 +526,7 @@ async def do_heist(self, ctx: commands.Context): name=f"{data['emoji']} {name.replace('_', ' ').title()}", value=( f"**Reward**: {min_reward:,}-{max_reward:,} {currency_name}\n" - f"**Risk**: {data['risk']*100:.0f}%\n" + f"**Risk**: {data['risk'] * 100:.0f}%\n" f"**Cooldown**: {cooldown_display}\n" f"**Success**: {data['min_success']}-{data['max_success']}%\n" f"**Duration**: {int(data['duration'].total_seconds() // 60)} min" @@ -570,15 +573,15 @@ async def check_inventory(self, ctx: commands.Context): desc = "" is_equipped = False if data["type"] == "tool": - desc = f"Boosts {data['for_heist'].replace('_', ' ').title()} success by {data['boost']*100:.0f}% (single use)" + desc = f"Boosts {data['for_heist'].replace('_', ' ').title()} success by {data['boost'] * 100:.0f}% (single use)" if equipped["tool"] == item: is_equipped = True elif data["type"] == "shield": - desc = f"Reduces loss by {data['reduction']*100:.1f}% (single use)" + desc = f"Reduces loss by {data['reduction'] * 100:.1f}% (single use)" if equipped["shield"] == item: is_equipped = True elif data["type"] == "consumable": - desc = f"Reduces risk by {data['risk_reduction']*100:.0f}% (single use)" + desc = f"Reduces risk by {data['risk_reduction'] * 100:.0f}% (single use)" if equipped["consumable"] == item: is_equipped = True elif data["type"] == "loot": @@ -676,7 +679,7 @@ async def check_shield(self, ctx: commands.Context): if count > 0: emoji, data = ITEMS[equipped_shield] return await ctx.send( - f"Active {emoji} {equipped_shield.replace('_', ' ').title()} shield: Reduces loss by {data['reduction']*100:.1f}% (single use). You have {count}." + f"Active {emoji} {equipped_shield.replace('_', ' ').title()} shield: Reduces loss by {data['reduction'] * 100:.1f}% (single use). You have {count}." ) await ctx.send("No active shield.") @@ -792,7 +795,7 @@ async def heistset_price(self, ctx: commands.Context): view.message = message @heistset.command(name="reset") - async def heistset_reset(self, ctx: commands.Context, heist_type: str = None): + async def heistset_reset(self, ctx: commands.Context, heist_type: Optional[str] = None): """Reset heist settings to default values. If no heist_type is provided, resets all heists. @@ -837,7 +840,7 @@ async def heistset_reset(self, ctx: commands.Context, heist_type: str = None): await ctx.send("Reset all heist settings to defaults.") @heistset.command(name="resetprice") - async def heistset_resetprice(self, ctx: commands.Context, item_name: str = None): + async def heistset_resetprice(self, ctx: commands.Context, item_name: Optional[str] = None): """Reset item prices to default values. If no item_name is provided, resets all item prices. @@ -864,7 +867,7 @@ async def heistset_resetprice(self, ctx: commands.Context, item_name: str = None @heistset.command(name="show") @commands.bot_has_permissions(embed_links=True) - async def heistset_show(self, ctx: commands.Context, heist_type: str = None): + async def heistset_show(self, ctx: commands.Context, heist_type: Optional[str] = None): """Show current settings for a heist or all heists. Parameters: @@ -920,11 +923,11 @@ async def heistset_show(self, ctx: commands.Context, heist_type: str = None): ) field_value = ( f"Reward: {reward_text}\n" - f"Risk: {data['risk']*100:.0f}%{' ⭐' if is_custom['risk'] else ''}\n" + f"Risk: {data['risk'] * 100:.0f}%{' ⭐' if is_custom['risk'] else ''}\n" f"Success: {data['min_success']}-{data['max_success']}%{' ⭐' if is_custom['min_success'] or is_custom['max_success'] else ''}\n" f"Cooldown: {data['cooldown'].total_seconds() / 3600:.1f}h{' ⭐' if is_custom['cooldown'] else ''}\n" f"Duration: {int(data['duration'].total_seconds() // 60)} min{' ⭐' if is_custom['duration'] else ''}\n" - f"Police Chance: {data['police_chance']*100:.0f}%{' ⭐' if is_custom['police_chance'] else ''}\n" + f"Police Chance: {data['police_chance'] * 100:.0f}%{' ⭐' if is_custom['police_chance'] else ''}\n" f"Jail Time: {data['jail_time'].total_seconds() / 3600:.1f}h{' ⭐' if is_custom['jail_time'] else ''}\n" f"Loss: {data['min_loss']:,}-{data['max_loss']:,} credits" ) @@ -937,7 +940,7 @@ async def heistset_show(self, ctx: commands.Context, heist_type: str = None): @heistset.command(name="showprices") @commands.bot_has_permissions(embed_links=True) - async def heistset_showprices(self, ctx: commands.Context, item_name: str = None): + async def heistset_showprices(self, ctx: commands.Context, item_name: Optional[str] = None): """Show current prices for an item or all shop items. Parameters: diff --git a/heist/utils.py b/heist/utils.py index 79a3c0f9..f80a6073 100644 --- a/heist/utils.py +++ b/heist/utils.py @@ -23,7 +23,6 @@ """ import datetime -from typing import Dict def fmt(s: str) -> str: @@ -31,7 +30,7 @@ def fmt(s: str) -> str: return s.replace("_", " ").title() -ITEMS: Dict[str, tuple] = { +ITEMS: dict[str, tuple] = { "wooden_shield": ( "🛡️", {"type": "shield", "cost": 3000, "reduction": 0.03, "duration_hours": 24}, @@ -185,7 +184,7 @@ def fmt(s: str) -> str: ), } -RECIPES: Dict[str, dict] = { +RECIPES: dict[str, dict] = { "reinforced_wooden_shield": { "materials": {"scrap_metal": 5}, "result": "reinforced_wooden_shield", @@ -273,7 +272,7 @@ def fmt(s: str) -> str: }, } -HEISTS: Dict[str, dict] = { +HEISTS: dict[str, dict] = { "pocket_steal": { "emoji": "🕶️", "risk": 0.01, diff --git a/heist/views.py b/heist/views.py index 59cc8c49..6e5a752c 100644 --- a/heist/views.py +++ b/heist/views.py @@ -23,6 +23,7 @@ """ import asyncio +import contextlib import datetime import discord @@ -33,6 +34,7 @@ from .handlers import schedule_resolve from .utils import HEISTS, ITEMS + log = getLogger("red.cogs.heist.views") @@ -45,7 +47,7 @@ def __init__(self, cog, ctx: commands.Context): async def interaction_check(self, interaction: discord.Interaction) -> bool: """Check if the user is allowed to interact.""" - if interaction.user.id not in [self.ctx.author.id] + list(self.ctx.bot.owner_ids): + if interaction.user.id not in [self.ctx.author.id, *list(self.ctx.bot.owner_ids)]: await interaction.response.send_message( "You are not allowed to use this interaction.", ephemeral=True ) @@ -73,12 +75,12 @@ def __init__(self, cog): description=( f"Cost: {data['cost']:,} | " + ( - f"Reduces loss by {data['reduction']*100:.1f}% (single use)" + f"Reduces loss by {data['reduction'] * 100:.1f}% (single use)" if data["type"] == "shield" else ( - f"Boosts {data['for_heist'].replace('_', ' ').title()} success by {data['boost']*100:.0f}% (single use)" + f"Boosts {data['for_heist'].replace('_', ' ').title()} success by {data['boost'] * 100:.0f}% (single use)" if data["type"] == "tool" - else f"Reduces risk by {data['risk_reduction']*100:.0f}% (single use)" + else f"Reduces risk by {data['risk_reduction'] * 100:.0f}% (single use)" ) ) ), @@ -94,7 +96,7 @@ def __init__(self, cog): async def callback(self, interaction: discord.Interaction): item_type = self.values[0] - emoji, data = ITEMS[item_type] + emoji, _data = ITEMS[item_type] cost = await self.cog.get_item_cost(item_type) balance = await bank.get_balance(interaction.user) currency_name = await bank.get_currency_name(interaction.guild) @@ -146,7 +148,7 @@ def __init__(self, cog, ctx: commands.Context, heist_settings: dict): async def interaction_check(self, interaction: discord.Interaction) -> bool: """Check if the user is allowed to interact.""" - if interaction.user.id not in [self.ctx.author.id] + list(self.cog.bot.owner_ids): + if interaction.user.id not in [self.ctx.author.id, *list(self.cog.bot.owner_ids)]: await interaction.response.send_message( "You are not allowed to use this interaction.", ephemeral=True ) @@ -276,7 +278,7 @@ def __init__(self, cog, ctx: commands.Context): async def interaction_check(self, interaction: discord.Interaction) -> bool: """Check if the user is allowed to interact.""" - if interaction.user.id not in [self.ctx.author.id] + list(self.cog.bot.owner_ids): + if interaction.user.id not in [self.ctx.author.id, *list(self.cog.bot.owner_ids)]: await interaction.response.send_message( "You are not authorized to use this.", ephemeral=True ) @@ -287,10 +289,8 @@ async def on_timeout(self): for item in self.children: item.disabled = True if self.message: - try: + with contextlib.suppress(discord.HTTPException): await self.message.edit(view=self) - except discord.HTTPException: - pass class HeistConfigButton(discord.ui.Button): @@ -431,7 +431,7 @@ def __init__(self, cog, ctx: commands.Context): self.add_item(ItemPriceConfigButton(cog)) async def interaction_check(self, interaction: discord.Interaction) -> bool: - if interaction.user.id not in [self.ctx.author.id] + list(self.cog.bot.owner_ids): + if interaction.user.id not in [self.ctx.author.id, *list(self.cog.bot.owner_ids)]: await interaction.response.send_message( "You are not authorized to use this.", ephemeral=True ) @@ -442,10 +442,8 @@ async def on_timeout(self): for item in self.children: item.disabled = True if self.message: - try: + with contextlib.suppress(discord.HTTPException): await self.message.edit(view=self) - except discord.HTTPException: - pass class ItemPriceConfigButton(discord.ui.Button): diff --git a/history/__init__.py b/history/__init__.py index 21ccd405..85a6fbf5 100644 --- a/history/__init__.py +++ b/history/__init__.py @@ -2,6 +2,7 @@ from .history import History + __red_end_user_data_statement__ = get_end_user_data_statement(__file__) diff --git a/history/history.py b/history/history.py index 639956ec..3ee651b6 100644 --- a/history/history.py +++ b/history/history.py @@ -35,6 +35,7 @@ from .utils import fetch_events, format_year + log = getLogger("red.maxcogs.history") WIKIPEDIA_LOGO = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/80/Wikipedia-logo-v2.svg/1200px-Wikipedia-logo-v2.svg.png" @@ -146,7 +147,7 @@ async def history( events: list[dict[str, Any]] = await fetch_events(self.session, month_str, day_str) except ValueError as e: log.error( - f"Failed to fetch events for {month_str}/{day_str}: {str(e)}", + f"Failed to fetch events for {month_str}/{day_str}: {e!s}", exc_info=True, ) return await ctx.send( diff --git a/history/utils.py b/history/utils.py index f4b9024d..c375ae2f 100644 --- a/history/utils.py +++ b/history/utils.py @@ -28,6 +28,7 @@ import orjson from red_commons.logging import getLogger + log = getLogger("red.maxcogs.history.utils") DEFAULT_ERA_NOTATION = "BC" _CIRCA_PREFIXES: tuple[str, ...] = ("circa", "c.", "ca.", "approximately") @@ -73,11 +74,13 @@ def format_year(year: int | str | None) -> str: return ( f"c. {formatted_year}" if is_circa - else formatted_year if year_int != 0 else "Unknown Year" + else formatted_year + if year_int != 0 + else "Unknown Year" ) except (ValueError, TypeError) as e: - log.error(f"Failed to format year '{year}': {str(e)}") + log.error(f"Failed to format year '{year}': {e!s}") return "Unknown Year" @@ -96,7 +99,7 @@ async def fetch_events( return data.get("events", []) except orjson.JSONDecodeError as e: log.error(f"Failed to decode API response for {month}/{day}: {e}") - raise ValueError("Error processing history data from Wikimedia.") + raise ValueError("Error processing history data from Wikimedia.") from e except aiohttp.ClientError as e: log.error(f"Network error fetching events for {month}/{day}: {e}", exc_info=True) raise ValueError(f"Network error while fetching history data: {e}") from e diff --git a/honeycombs/__init__.py b/honeycombs/__init__.py index 72fff48e..cd7397ef 100644 --- a/honeycombs/__init__.py +++ b/honeycombs/__init__.py @@ -3,6 +3,7 @@ from .honeycombs import HoneyCombs + __red_end_user_data_statement__ = get_end_user_data_statement(__file__) diff --git a/honeycombs/bank_utils.py b/honeycombs/bank_utils.py index 44166b10..42931ece 100644 --- a/honeycombs/bank_utils.py +++ b/honeycombs/bank_utils.py @@ -26,6 +26,7 @@ from redbot.core import bank from redbot.core.errors import BankError + # Tried to make stuff shorter in honeycombs.py but somehow made it longer, so this file got kinda pointless # But i already had writen stuff so there's no point removing right away... i'll do someday when im less lazy. @@ -40,7 +41,7 @@ async def safe_withdraw(user: discord.Member, amount: int, currency_name: str) - currency_name: The name of the currency for error messaging. Returns: - Tuple[bool, str]: (success, message) + tuple[bool, str]: (success, message) - success: True if withdrawal succeeded, False otherwise. - message: A message describing the outcome (success or error). """ @@ -61,7 +62,7 @@ async def safe_withdraw(user: discord.Member, amount: int, currency_name: str) - except BankError as e: return ( False, - f"Failed to withdraw {amount} {currency_name} from {user.mention}: {str(e)}", + f"Failed to withdraw {amount} {currency_name} from {user.mention}: {e!s}", ) @@ -75,7 +76,7 @@ async def safe_deposit(user: discord.Member, amount: int, currency_name: str) -> currency_name: The name of the currency for error messaging. Returns: - Tuple[bool, str]: (success, message) + tuple[bool, str]: (success, message) - success: True if deposit succeeded, False otherwise. - message: A message describing the outcome (success or error). """ @@ -89,5 +90,5 @@ async def safe_deposit(user: discord.Member, amount: int, currency_name: str) -> except BankError as e: return ( False, - f"Failed to deposit {amount} {currency_name} to {user.mention}: {str(e)}", + f"Failed to deposit {amount} {currency_name} to {user.mention}: {e!s}", ) diff --git a/honeycombs/honeycombs.py b/honeycombs/honeycombs.py index b14a374f..ac9a6d9e 100644 --- a/honeycombs/honeycombs.py +++ b/honeycombs/honeycombs.py @@ -39,6 +39,7 @@ from .bank_utils import safe_deposit, safe_withdraw from .view import HoneycombView + log = logging.getLogger("red.maxcogs.honeycombs") MAGIC_BYTES: dict[bytes, str] = { @@ -376,7 +377,7 @@ async def checklist(self, ctx: commands.Context): if not game_state.players: return await ctx.send("No ongoing game found.") - player_list = [f"Player {number}" for number in game_state.players.keys()] + player_list = [f"Player {number}" for number in game_state.players] pages = ["\n".join(player_list[i : i + 10]) for i in range(0, len(player_list), 10)] await SimpleMenu( pages, diff --git a/honeycombs/view.py b/honeycombs/view.py index bbe0a054..02894f68 100644 --- a/honeycombs/view.py +++ b/honeycombs/view.py @@ -29,6 +29,7 @@ import discord from redbot.core import bank + log = logging.getLogger("red.maxcogs.honeycombs.view") diff --git a/lockdown/__init__.py b/lockdown/__init__.py index e11f9219..2b61a691 100644 --- a/lockdown/__init__.py +++ b/lockdown/__init__.py @@ -2,6 +2,7 @@ from .lockdown import Lockdown + __red_end_user_data_statement__ = "This cog does not persistently store data about users." diff --git a/lockdown/lockdown.py b/lockdown/lockdown.py index eedacf69..54ba71e3 100644 --- a/lockdown/lockdown.py +++ b/lockdown/lockdown.py @@ -23,7 +23,7 @@ """ import re -from typing import Any, Final, Optional, Tuple, Union +from typing import Any, Final, Optional import discord from red_commons.logging import getLogger @@ -31,6 +31,7 @@ from .view import UnlockView + logger = getLogger("red.maxcogs.lockdown") @@ -153,7 +154,7 @@ async def _parse_reason_and_role( reason: Optional[str], role: Optional[discord.Role], guild: discord.Guild, - ) -> Tuple[Optional[str], Optional[discord.Role]]: + ) -> tuple[Optional[str], Optional[discord.Role]]: if reason and role is None: mentions = re.findall(r"<@&(\d+)>", reason) if mentions: diff --git a/lockdown/view.py b/lockdown/view.py index f5e1c173..d337715d 100644 --- a/lockdown/view.py +++ b/lockdown/view.py @@ -26,6 +26,7 @@ from red_commons.logging import getLogger from redbot.core import commands + logger = getLogger("red.maxcogs.lockdown.view") diff --git a/messageguard/container.py b/messageguard/container.py index 9df53576..acd12dc0 100644 --- a/messageguard/container.py +++ b/messageguard/container.py @@ -25,6 +25,7 @@ import re from typing import Final + # ForwardDeleter FD_WARN_MESSAGE: Final[str] = "You are not allowed to forward message(s)." # NoSpoiler diff --git a/messageguard/messageguard.py b/messageguard/messageguard.py index ed7316c0..fa24300d 100644 --- a/messageguard/messageguard.py +++ b/messageguard/messageguard.py @@ -23,7 +23,6 @@ """ import asyncio -import re from asyncio import Lock from collections import defaultdict from typing import Any, Final @@ -55,6 +54,7 @@ send_spoiler_warning, ) + log = getLogger("red.maxcogs.messageguard") @@ -303,35 +303,37 @@ async def on_raw_message_edit(self, payload: discord.RawMessageUpdateEvent) -> N if cfg.get("ns_enabled") and "content" in payload.data: author_id = int(payload.data.get("author", {}).get("id", 0)) author = guild.get_member(author_id) or self.bot.get_user(author_id) - if author and not author.bot and not await self.bot.is_automod_immune(author): - if SPOILER_REGEX.search(payload.data.get("content", "")): - if can_moderate(channel, guild.me): - try: - message = discord.Message( - state=channel._state, - channel=channel, - data=payload.data, - ) - if cfg.get("ns_spoiler_warn"): - await send_spoiler_warning( - message, - cfg.get("ns_spoiler_warn_message", NS_DEFAULT_WARNING), - cfg.get("ns_timeout", 10), - cfg.get("ns_use_embed", False), - await self.bot.get_embed_color(channel), - ) - await message.delete() - if cfg.get("ns_log_enabled") and cfg.get("ns_log_channel"): - await send_log( - message, - "NoSpoiler", - await self.bot.get_embed_color(channel), - cfg["ns_log_channel"], - ) - except (discord.Forbidden, discord.NotFound, discord.HTTPException) as e: - log.error( - "[NoSpoiler] Edit handler failed for %s: %s", payload.message_id, e - ) + if ( + author + and not author.bot + and not await self.bot.is_automod_immune(author) + and SPOILER_REGEX.search(payload.data.get("content", "")) + and can_moderate(channel, guild.me) + ): + try: + message = discord.Message( + state=channel._state, + channel=channel, + data=payload.data, + ) + if cfg.get("ns_spoiler_warn"): + await send_spoiler_warning( + message, + cfg.get("ns_spoiler_warn_message", NS_DEFAULT_WARNING), + cfg.get("ns_timeout", 10), + cfg.get("ns_use_embed", False), + await self.bot.get_embed_color(channel), + ) + await message.delete() + if cfg.get("ns_log_enabled") and cfg.get("ns_log_channel"): + await send_log( + message, + "NoSpoiler", + await self.bot.get_embed_color(channel), + cfg["ns_log_channel"], + ) + except (discord.Forbidden, discord.NotFound, discord.HTTPException) as e: + log.error("[NoSpoiler] Edit handler failed for %s: %s", payload.message_id, e) rp_channels = cfg.get("rp_channel_ids", []) if rp_channels and payload.channel_id in rp_channels: diff --git a/messageguard/utils.py b/messageguard/utils.py index 2bde634b..dedc6629 100644 --- a/messageguard/utils.py +++ b/messageguard/utils.py @@ -27,6 +27,7 @@ import discord from red_commons.logging import getLogger + log = getLogger("red.maxcogs.messageguard.utils") @@ -168,7 +169,7 @@ async def send_log( current_field: list[str] = [] field_index = 1 for url in attachment_urls: - test = "\n".join(current_field + [url]) + test = "\n".join([*current_field, url]) if len(test) > 1024: embed.add_field( name=f"Attachments (Part {field_index})", diff --git a/nba/__init__.py b/nba/__init__.py index 6147c3bc..7af8b4bb 100644 --- a/nba/__init__.py +++ b/nba/__init__.py @@ -1,5 +1,6 @@ from .nba import NBA + __red_end_user_data_statement__ = "This cog does not persistently store data about users." diff --git a/nba/commands/nba_commands.py b/nba/commands/nba_commands.py index c2dfdc8c..600236d5 100644 --- a/nba/commands/nba_commands.py +++ b/nba/commands/nba_commands.py @@ -23,7 +23,7 @@ """ import asyncio -from typing import List, Optional, Union +from typing import Optional, Union import discord import orjson @@ -49,6 +49,7 @@ ) from ..view import GameMenu + log = getLogger("red.maxcogs.nba") @@ -389,7 +390,7 @@ async def schedule(self, ctx: commands.Context, *, team: Optional[str] = None): @schedule.autocomplete("team") async def schedule_autocomplete( self, interaction: discord.Interaction, current: str - ) -> List[app_commands.Choice]: + ) -> list[app_commands.Choice]: choices = [t for t in TEAM_NAMES if current.lower() in t.lower()] return [app_commands.Choice(name=t, value=t) for t in choices[:25]] @@ -439,7 +440,7 @@ async def scoreboard(self, ctx: commands.Context, team: Optional[str] = None): @scoreboard.autocomplete("team") async def scoreboard_autocomplete( self, interaction: discord.Interaction, current: str - ) -> List[app_commands.Choice]: + ) -> list[app_commands.Choice]: choices = [t for t in TEAM_NAMES if current.lower() in t.lower()] return [app_commands.Choice(name=t, value=t) for t in choices[:25]] diff --git a/nba/converter.py b/nba/converter.py index 669c50d5..3b7f6c68 100644 --- a/nba/converter.py +++ b/nba/converter.py @@ -28,6 +28,7 @@ import pytz from red_commons.logging import getLogger + log = getLogger("red.maxcogs.nba.converter") TODAY_SCOREBOARD = "https://cdn.nba.com/static/json/liveData/scoreboard/todaysScoreboard_00.json" diff --git a/nba/formatters.py b/nba/formatters.py index a22e8020..9cf4031b 100644 --- a/nba/formatters.py +++ b/nba/formatters.py @@ -25,7 +25,7 @@ import math from datetime import datetime, timezone from itertools import islice -from typing import List, Optional +from typing import Optional import discord from redbot.core import commands @@ -36,9 +36,9 @@ async def build_schedule_embeds( ctx: commands.Context, - games: List[dict], + games: list[dict], team: Optional[str], -) -> List[discord.Embed]: +) -> list[discord.Embed]: """Build paginated embeds for upcoming NBA schedule.""" # NBA season typically runs from October to June, with the offseason in July, August, and September. now = datetime.now() @@ -71,9 +71,7 @@ async def build_schedule_embeds( ) for game in islice(games, i, i + 6): arena_info = game.get("arena", "Unknown") - city_info = ( - f"{game.get('arena_city', 'Unknown')}, " f"{game.get('arenastate', 'Unknown')}" - ) + city_info = f"{game.get('arena_city', 'Unknown')}, {game.get('arenastate', 'Unknown')}" embed.add_field( name=f"{game['home_team']} vs {game['away_team']}", value=( @@ -85,7 +83,7 @@ async def build_schedule_embeds( inline=False, ) embed.set_footer( - text=(f"Page {i // 6 + 1}/{math.ceil(len(games) / 6)}" " | 🏀Provided by NBA.com") + text=(f"Page {i // 6 + 1}/{math.ceil(len(games) / 6)} | 🏀Provided by NBA.com") ) pages.append(embed) return pages @@ -93,9 +91,9 @@ async def build_schedule_embeds( async def build_scoreboard_embeds( ctx: commands.Context, - games: List[dict], + games: list[dict], team: Optional[str], -) -> List[discord.Embed]: +) -> list[discord.Embed]: """Build detailed paginated embeds for scoreboard.""" if not games: start, end = get_time_bounds() @@ -195,7 +193,7 @@ async def build_scoreboard_embeds( or team.lower() in (g["homeTeam"]["teamName"] + g["awayTeam"]["teamName"]).lower() ] ) - embed.set_footer(text=(f"🏀Provided by NBA.com" f" | Page {len(pages) + 1}/{total_pages}")) + embed.set_footer(text=(f"🏀Provided by NBA.com | Page {len(pages) + 1}/{total_pages}")) pages.append(embed) if not pages and team: @@ -205,8 +203,8 @@ async def build_scoreboard_embeds( async def build_news_embeds( ctx: commands.Context, - news: List[dict], -) -> List[discord.Embed]: + news: list[dict], +) -> list[discord.Embed]: """Build paginated embeds for news.""" if not news: await ctx.send("No news found from ESPN.") @@ -229,7 +227,7 @@ async def build_news_embeds( color=await ctx.embed_color(), ) embed.set_footer( - text=(f"🏀Provided by ESPN" f" | Page {i // 5 + 1}/{math.ceil(len(news) / 5)}") + text=(f"🏀Provided by ESPN | Page {i // 5 + 1}/{math.ceil(len(news) / 5)}") ) pages.append(embed) return pages @@ -265,7 +263,7 @@ def build_pregame_embed( async def build_playoff_embeds( ctx: commands.Context, data: dict, -) -> List[discord.Embed]: +) -> list[discord.Embed]: """Build paginated embeds for the NBA playoff picture from nba_api PlayoffPicture.""" now = datetime.now() is_not_playoffs = ( @@ -322,7 +320,7 @@ async def build_playoff_embeds( async def build_standings_embeds( ctx: commands.Context, data: dict, -) -> List[discord.Embed]: +) -> list[discord.Embed]: """Build West-then-East standings embeds from ESPN standings API.""" children = data.get("children", []) if not children: diff --git a/nba/nba.py b/nba/nba.py index ff56f491..d7834eed 100644 --- a/nba/nba.py +++ b/nba/nba.py @@ -26,7 +26,7 @@ import sqlite3 from datetime import datetime, timezone from time import time -from typing import Any, Dict, Final, List, Optional +from typing import Any, Final, Optional import aiohttp import discord @@ -50,6 +50,7 @@ from .formatters import build_pregame_embed, build_score_update_embed from .view import PreGameView + log = getLogger("red.maxcogs.nba") # TODO to myself. @@ -73,7 +74,7 @@ class NBA(NBACommands, commands.Cog): def __init__(self, bot): self.bot = bot self.config = Config.get_conf(self, identifier=1234567891011) - default_guild: Dict[str, Any] = { + default_guild: dict[str, Any] = { "team_channels": {}, # Legacy keys kept only for migration - do not use in new code. "channel": None, @@ -331,9 +332,7 @@ async def periodic_check(self): # If both teams are configured, only notify via the home team's channel # to avoid double posting when two tracked teams face each other. - if home_entry and away_entry: - entries_to_notify = [home_entry] - elif home_entry: + if (home_entry and away_entry) or home_entry: entries_to_notify = [home_entry] elif away_entry: entries_to_notify = [away_entry] @@ -473,9 +472,7 @@ async def _check_pregame_notifications(self) -> None: elif api_name == away_team: away_entry = entry - if home_entry and away_entry: - entries_to_notify = [home_entry] - elif home_entry: + if (home_entry and away_entry) or home_entry: entries_to_notify = [home_entry] elif away_entry: entries_to_notify = [away_entry] @@ -546,7 +543,7 @@ async def get_cached_data( setattr(self, f"{cache_key}_time", time()) return data - async def fetch_scoreboard(self, ctx: commands.Context) -> Optional[List[dict]]: + async def fetch_scoreboard(self, ctx: commands.Context) -> Optional[list[dict]]: """Fetch and parse NBA scoreboard data.""" data = await self.get_cached_data(TODAY_SCOREBOARD, ctx, "scoreboard") if not data: @@ -557,7 +554,7 @@ async def fetch_scoreboard(self, ctx: commands.Context) -> Optional[List[dict]]: log.error("Failed to decode scoreboard: %s", e) return None - async def fetch_news(self, ctx: commands.Context) -> Optional[List[dict]]: + async def fetch_news(self, ctx: commands.Context) -> Optional[list[dict]]: """Fetch and parse NBA news feed.""" data = await self.fetch_data(ESPN_NBA_NEWS, ctx) if not data: diff --git a/nba/view.py b/nba/view.py index 8823e954..1e2fbb35 100644 --- a/nba/view.py +++ b/nba/view.py @@ -30,6 +30,7 @@ from .converter import PLAYBYPLAY + log = getLogger("red.maxcogs.nba.view") @@ -65,14 +66,13 @@ def __init__(self, game_id): async def view_play_by_play(self, interaction: discord.Interaction, button: discord.ui.Button): url = f"{PLAYBYPLAY}/liveData/playbyplay/playbyplay_{self.game_id}.json" try: - async with aiohttp.ClientSession() as session: - async with session.get(url) as response: - if response.status != 200: - return await interaction.response.send_message( - f"Failed to fetch play-by-play data (status {response.status}).", - ephemeral=True, - ) - play_by_play_data = await response.json() + async with aiohttp.ClientSession() as session, session.get(url) as response: + if response.status != 200: + return await interaction.response.send_message( + f"Failed to fetch play-by-play data (status {response.status}).", + ephemeral=True, + ) + play_by_play_data = await response.json() except aiohttp.ClientError as e: log.error("Network error fetching play-by-play for game %s: %s", self.game_id, e) return await interaction.response.send_message( diff --git a/nekosbest/__init__.py b/nekosbest/__init__.py index 27c776b3..2cb28707 100644 --- a/nekosbest/__init__.py +++ b/nekosbest/__init__.py @@ -3,6 +3,7 @@ from .nekosbest import NekosBest + __red_end_user_data_statement__ = get_end_user_data_statement(__file__) diff --git a/nekosbest/core.py b/nekosbest/core.py index b6d5ba57..1a308f22 100644 --- a/nekosbest/core.py +++ b/nekosbest/core.py @@ -22,11 +22,12 @@ SOFTWARE. """ -from typing import Dict, Final +from typing import Final + NEKOS: Final[str] = "https://nekos.best/api/v2/" ICON: Final[str] = "https://nekos.best/logo_short.png" -ACTIONS: Dict[str, str] = { +ACTIONS: dict[str, str] = { "baka": "baka", "cry": "cries at", "cuddle": "cuddles", diff --git a/nekosbest/nekosbest.py b/nekosbest/nekosbest.py index 8180ad54..1c2262b9 100644 --- a/nekosbest/nekosbest.py +++ b/nekosbest/nekosbest.py @@ -23,12 +23,12 @@ """ import logging -from typing import Any, Dict, Final, Literal, Optional +from typing import Any, Final, Literal, Optional import aiohttp import discord import orjson -from redbot.core import Config, app_commands, commands +from redbot.core import Config, commands from redbot.core.bot import Red from redbot.core.utils.chat_formatting import humanize_number from redbot.core.utils.views import ConfirmView @@ -36,6 +36,7 @@ from .core import ACTIONS, ICON, NEKOS from .view import ImageButtonView + log = logging.getLogger("red.maxcogs.nekosbest") RequestType = Literal["discord_deleted_user", "owner", "user", "user_strict"] @@ -68,21 +69,21 @@ async def red_delete_data_for_user(self, *, requester: RequestType, user_id: int async def red_get_data_for_user(self, *, user_id: int): return - async def _api_call(self, ctx: commands.Context, endpoint: str) -> Optional[Dict[str, Any]]: + async def _api_call(self, ctx: commands.Context, endpoint: str) -> Optional[dict[str, Any]]: async with self.session.get(NEKOS + endpoint) as response: if response.status != 200: log.error( - "Something went wrong while trying to contact API. " "Status code: %s", + "Something went wrong while trying to contact API. Status code: %s", response.status, ) return await ctx.send( - "Something went wrong while trying to contact API. " "Please try again later.", + "Something went wrong while trying to contact API. Please try again later.", ) data = await response.read() return orjson.loads(data) async def imgembedgen( - self, ctx: commands.Context, response: Dict[str, Any], category: str + self, ctx: commands.Context, response: dict[str, Any], category: str ) -> None: data = response["results"][0] artist = data["artist_name"] @@ -143,7 +144,7 @@ async def embedgen(self, ctx: commands.Context, member: discord.Member, action: ), ) emb.set_image(url=url["results"][0]["url"]) - emb.set_footer(text=f"Powered by nekos.best", icon_url=ICON) + emb.set_footer(text="Powered by nekos.best", icon_url=ICON) await ctx.send(embed=emb) # -------- image Commands -------> diff --git a/plaguegame/__init__.py b/plaguegame/__init__.py index 1366287f..28cdffd4 100644 --- a/plaguegame/__init__.py +++ b/plaguegame/__init__.py @@ -25,6 +25,7 @@ from .plague import Plague + __red_end_user_data_statement__ = "This cog stores data on users based off their interactions within the game. Examples of such data are their 'health state' (healthy or infected) or their 'game role' (user, doctor, plaguebearer)." diff --git a/plaguegame/converters.py b/plaguegame/converters.py index 9cfb0990..337770c6 100644 --- a/plaguegame/converters.py +++ b/plaguegame/converters.py @@ -34,8 +34,8 @@ def hundred_int(arg: str): try: ret = int(arg) - except ValueError: - raise BadArgument(f"{inline(arg)} is not an integer.") + except ValueError as err: + raise BadArgument(f"{inline(arg)} is not an integer.") from err if ret < 1 or ret > 100: raise BadArgument(f"{inline(arg)} must be an integer between 1 and 100.") return ret @@ -50,7 +50,7 @@ def __init__(self, response: bool = True): async def convert(self, ctx: commands.Context, argument: str) -> discord.Role: try: member = await super().convert(ctx, argument) - except BadArgument: + except BadArgument as err: guild = ctx.guild result = [ (m[2], m[1]) @@ -62,7 +62,9 @@ async def convert(self, ctx: commands.Context, argument: str) -> discord.Role: ) ] if not result: - raise BadArgument(f'Member "{argument}" not found.' if self.response else None) + raise BadArgument( + f'Member "{argument}" not found.' if self.response else None + ) from err sorted_result = sorted(result, key=lambda r: r[1], reverse=True) member = sorted_result[0][0] @@ -89,9 +91,9 @@ async def convert(self, ctx: commands.Context, argument: str) -> discord.Member: f"**{member.name}** is already infected with {game_data['plagueName']}." ) elif data["gameRole"] == "Doctor": - raise BadArgument(f"You cannot infect a Doctor!") + raise BadArgument("You cannot infect a Doctor!") elif data["gameRole"] == "God": - raise BadArgument(f"Don't mess with God.") + raise BadArgument("Don't mess with God.") return member @@ -104,7 +106,7 @@ async def convert(self, ctx: commands.Context, argument: str) -> discord.Member: if data["gameState"] == "healthy": raise BadArgument(f"**{member.name}** is already healthy.") elif data["gameRole"] == "Plaguebearer": - raise BadArgument(f"You cannot cure a Plaguebearer!") + raise BadArgument("You cannot cure a Plaguebearer!") elif data["gameRole"] == "God": - raise BadArgument(f"Don't mess with God.") + raise BadArgument("Don't mess with God.") return member diff --git a/plaguegame/plague.py b/plaguegame/plague.py index 849eaa90..413ef5d6 100644 --- a/plaguegame/plague.py +++ b/plaguegame/plague.py @@ -24,8 +24,10 @@ """ import asyncio +import contextlib import random from collections import Counter +from typing import Optional import discord from redbot.core import Config, app_commands, bank, commands @@ -37,6 +39,7 @@ from .converters import Curable, FuzzyHuman, Infectable, hundred_int + hn = humanize_number @@ -122,11 +125,6 @@ def __init__(self, bot): async def cog_unload(self): self.bot.tree.remove_command(self.ctx_menu.name, type=self.ctx_menu.type) - def format_help_for_context(self, ctx: commands.Context) -> str: - """Thanks Sinbad!""" - pre = super().format_help_for_context(ctx) - return f"{pre}\n\nAuthor: {self.__author__}\nCog Version: {self.__version__}\nDocs: {self.__docs__}" - async def red_delete_data_for_user(self, *, requester: str, user_id: int): await self.config.user_from_id(user_id).clear() @@ -136,10 +134,7 @@ async def generate_plague_profile(self, member): userState = data["gameState"] notifications = data["notifications"] - if notifications: - notifications = "Enabled" - else: - notifications = "Disabled" + notifications = "Enabled" if notifications else "Disabled" if userRole == GameRole.DOCTOR: thumbnail = "https://max.shx.gg/6GJ6zTXmI.png" @@ -212,7 +207,7 @@ async def cure(self, ctx, *, member: Curable): async def plaguenotify(self, ctx): """Enable/Disable Plague Game notifications.""" notifications = await self.config.user(ctx.author).notifications() - if notifications != False: + if notifications: await self.config.user(ctx.author).notifications.set(False) message = "You will no longer be sent Plague Game notifications." else: @@ -286,7 +281,7 @@ async def plagueset(self, ctx): """Settings for the Plague game.""" @plagueset.command() - async def name(self, ctx, *, name: str = None): + async def name(self, ctx, *, name: Optional[str] = None): """Set's the plague's name. Leave blank to show the current name.""" plagueName = await self.config.plagueName() if not name: @@ -597,10 +592,8 @@ async def notify_user(self, ctx, user: discord.User, notificationType: str): embed = discord.Embed(title=title, description=description) embed.set_footer(text=f"Use `{prefixes[-1]}plaguenotify` to disable these notifications.") - try: + with contextlib.suppress(discord.Forbidden): await user.send(embed=embed) - except discord.Forbidden: - pass @commands.Cog.listener() async def on_command(self, ctx: commands.Context): diff --git a/pokemon/api.py b/pokemon/api.py index 7147eac3..5798165c 100644 --- a/pokemon/api.py +++ b/pokemon/api.py @@ -25,6 +25,7 @@ import aiohttp from red_commons.logging import getLogger + log = getLogger("red.maxcogs.whosthatpokemon.api") API_URL = "https://pokeapi.co/api/v2" _DEFAULT_TIMEOUT = aiohttp.ClientTimeout(total=15) diff --git a/pokemon/commands/pokeinfo.py b/pokemon/commands/pokeinfo.py index a120a301..d0f50119 100644 --- a/pokemon/commands/pokeinfo.py +++ b/pokemon/commands/pokeinfo.py @@ -29,6 +29,7 @@ from ..formatters import create_pokemon_embed from ..views import PokemonView + log = getLogger("red.maxcogs.whosthatpokemon.commands.pokeinfo") diff --git a/pokemon/commands/tcgcard.py b/pokemon/commands/tcgcard.py index 9088a0e4..a6575bfe 100644 --- a/pokemon/commands/tcgcard.py +++ b/pokemon/commands/tcgcard.py @@ -31,6 +31,7 @@ from redbot.core.utils.chat_formatting import box from redbot.core.utils.views import SimpleMenu + log = getLogger("red.maxcogs.whosthatpokemon.commands.tcgcard") _TCG_FIELDS = ( diff --git a/pokemon/commands/whosthatpokemon.py b/pokemon/commands/whosthatpokemon.py index 3b6e4a36..5f42eb24 100644 --- a/pokemon/commands/whosthatpokemon.py +++ b/pokemon/commands/whosthatpokemon.py @@ -36,6 +36,7 @@ from ..image import generate_image from ..views import HintView, WhosThatPokemonView + log = getLogger("red.maxcogs.whosthatpokemon.commands.whosthatpokemon") diff --git a/pokemon/converters.py b/pokemon/converters.py index 452c7d2e..540168dc 100644 --- a/pokemon/converters.py +++ b/pokemon/converters.py @@ -26,6 +26,7 @@ from redbot.core import commands + _GEN_RANGES: dict[str, tuple[int, int]] = { "gen1": (1, 151), "gen2": (152, 251), diff --git a/pokemon/formatters.py b/pokemon/formatters.py index f59780ed..538c651a 100644 --- a/pokemon/formatters.py +++ b/pokemon/formatters.py @@ -22,14 +22,13 @@ SOFTWARE. """ -from typing import Dict - import aiohttp import discord from red_commons.logging import getLogger from .api import API_URL, fetch_data + log = getLogger("red.maxcogs.whosthatpokemon.formatters") MAX_DESCRIPTION_LENGTH = 4000 @@ -59,8 +58,8 @@ def _format_stats(stats: list[dict]) -> str: def _format_height_weight(height: int, weight: int) -> tuple[str, str]: """Format height and weight in metric and imperial units.""" return ( - f"{height/10:.1f}m ({height*3.28084/10:.2f}ft)", - f"{weight/10:.1f}kg ({weight*2.20462/10:.2f}lbs)", + f"{height / 10:.1f}m ({height * 3.28084 / 10:.2f}ft)", + f"{weight / 10:.1f}kg ({weight * 2.20462 / 10:.2f}lbs)", ) @@ -94,7 +93,7 @@ def _truncate_description(text: str) -> str: async def create_pokemon_embed( - session: aiohttp.ClientSession, pokemon_data: Dict, section: str = "base" + session: aiohttp.ClientSession, pokemon_data: dict, section: str = "base" ) -> discord.Embed: """ Create a Discord embed for Pokémon data based on the specified section. diff --git a/pokemon/image.py b/pokemon/image.py index 4baa820a..e5eca752 100644 --- a/pokemon/image.py +++ b/pokemon/image.py @@ -32,6 +32,7 @@ from red_commons.logging import getLogger from redbot.core.data_manager import bundled_data_path + log = getLogger("red.maxcogs.whosthatpokemon.image") _DEFAULT_TIMEOUT = aiohttp.ClientTimeout(total=15) diff --git a/pokemon/pokemon.py b/pokemon/pokemon.py index ff405080..b82a33f4 100644 --- a/pokemon/pokemon.py +++ b/pokemon/pokemon.py @@ -34,6 +34,7 @@ from .commands.tcgcard import TcgcardCommands from .commands.whosthatpokemon import WhosThatPokemonCommands + log = getLogger("red.maxcogs.whosthatpokemon") diff --git a/pokemon/utils.py b/pokemon/utils.py index 36352289..e9cf3474 100644 --- a/pokemon/utils.py +++ b/pokemon/utils.py @@ -23,9 +23,8 @@ """ import asyncio -import logging from io import BytesIO -from typing import Dict, Optional +from typing import Optional import aiohttp import discord @@ -33,6 +32,7 @@ from red_commons.logging import getLogger from redbot.core.data_manager import bundled_data_path + log = getLogger("red.maxcogs.whosthatpokemon.utils") API_URL = "https://pokeapi.co/api/v2" @@ -125,8 +125,8 @@ def _format_stats(stats: list[dict]) -> str: def _format_height_weight(height: int, weight: int) -> tuple[str, str]: """Format height and weight in metric and imperial units.""" return ( - f"{height/10:.1f}m ({height*3.28084/10:.2f}ft)", - f"{weight/10:.1f}kg ({weight*2.20462/10:.2f}lbs)", + f"{height / 10:.1f}m ({height * 3.28084 / 10:.2f}ft)", + f"{weight / 10:.1f}kg ({weight * 2.20462 / 10:.2f}lbs)", ) @@ -160,7 +160,7 @@ def _truncate_description(text: str) -> str: async def create_pokemon_embed( - session: aiohttp.ClientSession, pokemon_data: Dict, section: str = "base" + session: aiohttp.ClientSession, pokemon_data: dict, section: str = "base" ) -> discord.Embed: """ Create a Discord embed for Pokémon data based on the specified section. diff --git a/pokemon/views.py b/pokemon/views.py index 35e741fd..f8567359 100644 --- a/pokemon/views.py +++ b/pokemon/views.py @@ -22,8 +22,9 @@ SOFTWARE. """ +import contextlib import random -from typing import Any, List +from typing import Any import aiohttp import discord @@ -32,6 +33,7 @@ from .formatters import create_pokemon_embed + log = getLogger("red.maxcogs.whosthatpokemon.views") @@ -50,7 +52,7 @@ async def on_submit(self, interaction: discord.Interaction) -> None: class WhosThatPokemonView(discord.ui.View): - def __init__(self, eligible_names: List[Any]) -> None: + def __init__(self, eligible_names: list[Any]) -> None: self.eligible_names = eligible_names self.winner = None self.message = None @@ -61,10 +63,8 @@ async def on_timeout(self) -> None: item: discord.ui.Item item.disabled = True if self.message: - try: + with contextlib.suppress(discord.HTTPException): await self.message.edit(view=self) - except discord.HTTPException: - pass @discord.ui.button(label="Guess The Pokémon", style=discord.ButtonStyle.blurple) async def guess_the_pokemon(self, interaction: discord.Interaction, button: discord.ui.Button): @@ -79,19 +79,15 @@ async def guess_the_pokemon(self, interaction: discord.Interaction, button: disc button.style = discord.ButtonStyle.success if self.message: await self.message.edit(view=self) - try: + with contextlib.suppress(discord.HTTPException): await interaction.followup.send( f"{interaction.user.mention} Guessed the Pokémon correctly!", ) - except discord.HTTPException: - pass else: - try: + with contextlib.suppress(discord.HTTPException): await interaction.followup.send( f"{interaction.user.mention}, Wrong Pokémon name!", ) - except discord.HTTPException: - pass class HintView(discord.ui.View): @@ -127,9 +123,9 @@ async def hint_button(self, interaction: discord.Interaction, button: discord.ui characteristics = [] if height := pokemon_data.get("height"): - characteristics.append(f"Height: {height/10:.1f}m") + characteristics.append(f"Height: {height / 10:.1f}m") if weight := pokemon_data.get("weight"): - characteristics.append(f"Weight: {weight/10:.1f}kg") + characteristics.append(f"Weight: {weight / 10:.1f}kg") if characteristics: hints.append(random.choice(characteristics)) @@ -202,10 +198,8 @@ async def on_timeout(self) -> None: for item in self.children: item.disabled = True if self.message: - try: + with contextlib.suppress(discord.NotFound): await self.message.edit(view=self) - except discord.NotFound: - pass async def update_embed(self, interaction: discord.Interaction) -> None: try: @@ -225,8 +219,6 @@ async def close_button(self, interaction: discord.Interaction, button: discord.u for item in self.children: item.disabled = True if self.message: - try: + with contextlib.suppress(discord.NotFound): await self.message.edit(view=self) - except discord.NotFound: - pass self.stop() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..c64f6a02 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[tool.ruff] +line-length = 99 +target-version = "py39" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "SIM", # flake8-simplify + "RUF", # ruff-specific rules +] +ignore = [ + "E501", # line too long (handled by formatter) + "RUF006", # asyncio dangling task (too noisy for cogs) + "RUF001", # ambiguous Unicode characters + "RUF002", # ambiguous Unicode characters + "RUF003" # ambiguous Unicode characters +] +fixable = ["ALL"] +extend-unsafe-fixes = ["ALL"] + +[tool.ruff.lint.isort] +known-first-party = ["maxcogs"] +force-single-line = false +lines-after-imports = 2 + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" diff --git a/redupdate/__init__.py b/redupdate/__init__.py index baab2278..81642c77 100644 --- a/redupdate/__init__.py +++ b/redupdate/__init__.py @@ -3,6 +3,7 @@ from .redupdate import RedUpdate + __red_end_user_data_statement__ = "This cog does not persistently store data about users." diff --git a/redupdate/redupdate.py b/redupdate/redupdate.py index 1445e2ed..c65c4a33 100644 --- a/redupdate/redupdate.py +++ b/redupdate/redupdate.py @@ -32,6 +32,7 @@ from .view import ForkURLView, RestartButton + log = getLogger("red.maxcogs.redupdate") _STABLE_PACKAGE: Final[str] = "Red-DiscordBot" _DEV_PACKAGE: Final[str] = ( diff --git a/redupdate/view.py b/redupdate/view.py index f7550e9c..7e09a0ae 100644 --- a/redupdate/view.py +++ b/redupdate/view.py @@ -22,11 +22,12 @@ SOFTWARE. """ +import contextlib import re import discord from red_commons.logging import getLogger -from redbot.core import Config + log = getLogger("red.maxcogs.redupdate.view") GITHUB = re.compile(r"^(git\+ssh://git@github\.com|git\+https://github\.com)") @@ -57,13 +58,11 @@ async def on_timeout(self) -> None: for item in self.children: item: discord.ui.Item item.disabled = True - try: + with contextlib.suppress(discord.HTTPException): await self.message.edit(view=self) - except discord.HTTPException: - pass async def interaction_check(self, interaction: discord.Interaction) -> bool: - if not interaction.user.id == self.ctx.author.id: + if interaction.user.id != self.ctx.author.id: await interaction.response.send_message( "You are not the author of this command.", ephemeral=True ) @@ -83,7 +82,7 @@ async def on_submit(self, interaction: discord.Interaction, button: discord.ui.B url = modal.forkurl.value if not url or not GITHUB.match(url): await interaction.followup.send( - f"This is not a valid url for your fork.\nCheck `redset whatlink` for more information.", + "This is not a valid url for your fork.\nCheck `redset whatlink` for more information.", ephemeral=True, ) return @@ -136,7 +135,7 @@ async def on_timeout(self) -> None: pass async def interaction_check(self, interaction: discord.Interaction): - if not interaction.user.id == self.ctx.author.id: + if interaction.user.id != self.ctx.author.id: await interaction.response.send_message( "You are not the author of this command.", ephemeral=True ) @@ -155,5 +154,5 @@ async def restart_button(self, interaction: discord.Interaction, button: discord try: await self.bot.shutdown(restart=True) except Exception as e: - await interaction.channel.send(f"Error restarting bot: {str(e)}") + await interaction.channel.send(f"Error restarting bot: {e!s}") log.error("Error restarting bot: %s", e, exc_info=True) diff --git a/slashhelpmenu/__init__.py b/slashhelpmenu/__init__.py index 74b200b8..594f0900 100644 --- a/slashhelpmenu/__init__.py +++ b/slashhelpmenu/__init__.py @@ -3,6 +3,7 @@ from .slashhelpmenu import SlashHelpMenu + __red_end_user_data_statement__ = get_end_user_data_statement(__file__) diff --git a/slashhelpmenu/slashhelpmenu.py b/slashhelpmenu/slashhelpmenu.py index d12e6655..4a6eefca 100644 --- a/slashhelpmenu/slashhelpmenu.py +++ b/slashhelpmenu/slashhelpmenu.py @@ -33,6 +33,7 @@ from .view import HelpView + log = logging.getLogger("red.maxcogs.slashhelpmenu") _TITLE = "Available Slash Commands" @@ -249,7 +250,7 @@ async def help_command(self, interaction: discord.Interaction) -> None: if len(joined) > 1024: current_chunk: list[str] = [] for mention in mentions: - candidate = " ".join(current_chunk + [mention]) + candidate = " ".join([*current_chunk, mention]) if len(candidate) > 1024: field_parts.append(" ".join(current_chunk)) current_chunk = [mention] diff --git a/slashhelpmenu/view.py b/slashhelpmenu/view.py index 2135046b..2c8cb9a0 100644 --- a/slashhelpmenu/view.py +++ b/slashhelpmenu/view.py @@ -22,6 +22,8 @@ SOFTWARE. """ +import contextlib + import discord @@ -55,10 +57,8 @@ async def prev_button( ) -> None: self.current_page -= 1 self._update_buttons() - try: + with contextlib.suppress(discord.NotFound, discord.InteractionResponded): await interaction.response.edit_message(embed=self.pages[self.current_page], view=self) - except (discord.NotFound, discord.InteractionResponded): - pass @discord.ui.button(label="Next", style=discord.ButtonStyle.blurple) async def next_button( @@ -66,10 +66,8 @@ async def next_button( ) -> None: self.current_page += 1 self._update_buttons() - try: + with contextlib.suppress(discord.NotFound, discord.InteractionResponded): await interaction.response.edit_message(embed=self.pages[self.current_page], view=self) - except (discord.NotFound, discord.InteractionResponded): - pass @discord.ui.button(label="Close", style=discord.ButtonStyle.red) async def close_button( @@ -78,10 +76,8 @@ async def close_button( for item in self.children: item.disabled = True # type: ignore[union-attr] self.stop() - try: + with contextlib.suppress(discord.NotFound, discord.InteractionResponded): await interaction.response.edit_message(view=self) - except (discord.NotFound, discord.InteractionResponded): - pass async def on_timeout(self) -> None: for item in self.children: diff --git a/technews/__init__.py b/technews/__init__.py index 56452cd9..45819695 100644 --- a/technews/__init__.py +++ b/technews/__init__.py @@ -2,6 +2,7 @@ from .technews import TechNews + __red_end_user_data_statement__ = get_end_user_data_statement(__file__) diff --git a/technews/technews.py b/technews/technews.py index fafe9359..9149ccd3 100644 --- a/technews/technews.py +++ b/technews/technews.py @@ -37,6 +37,7 @@ from .utils import ChannelOrThread, _can_post from .views import NewsLayout + log = getLogger("red.maxcogs.technews") diff --git a/technews/utils.py b/technews/utils.py index 636d63f1..a65ff8fe 100644 --- a/technews/utils.py +++ b/technews/utils.py @@ -26,6 +26,7 @@ import discord + # Type alias for supported destinations ChannelOrThread = Union[discord.TextChannel, discord.Thread] diff --git a/themoviedb/__init__.py b/themoviedb/__init__.py index 6c199f13..a231891b 100644 --- a/themoviedb/__init__.py +++ b/themoviedb/__init__.py @@ -2,6 +2,7 @@ from .themoviedb import TheMovieDB + __red_end_user_data_statement__ = "This cog does not persistently store data about users." diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py index f744d7b0..d5254427 100644 --- a/themoviedb/themoviedb.py +++ b/themoviedb/themoviedb.py @@ -38,6 +38,7 @@ from .tmdb_utils import PREDEFINED_CHANNELS, fetch_tmdb, person_embed, search_and_display + logger = getLogger("red.maxcogs.themoviedb") diff --git a/themoviedb/tmdb_utils.py b/themoviedb/tmdb_utils.py index e1662641..94f63530 100644 --- a/themoviedb/tmdb_utils.py +++ b/themoviedb/tmdb_utils.py @@ -23,7 +23,6 @@ """ import asyncio -import re import urllib.parse from datetime import datetime from typing import Any @@ -32,9 +31,10 @@ import discord import orjson from red_commons.logging import getLogger -from redbot.core.utils.chat_formatting import box, header, humanize_list, humanize_number +from redbot.core.utils.chat_formatting import header, humanize_list, humanize_number from redbot.core.utils.views import SimpleMenu + log = getLogger("red.maxcogs.themoviedb.tmdb_utils") BASE_MEDIA = "https://api.themoviedb.org/3/search" BASE_URL = "https://api.themoviedb.org/3" diff --git a/vanish/__init__.py b/vanish/__init__.py index 32990090..43265719 100644 --- a/vanish/__init__.py +++ b/vanish/__init__.py @@ -3,6 +3,7 @@ from .vanish import Vanish + __red_end_user_data_statement__ = get_end_user_data_statement(__file__) diff --git a/vanish/vanish.py b/vanish/vanish.py index 1409bc94..2b62f457 100644 --- a/vanish/vanish.py +++ b/vanish/vanish.py @@ -25,11 +25,11 @@ import datetime from typing import Final -import discord from red_commons.logging import getLogger from redbot.core import Config, commands from redbot.core.commands.converter import parse_timedelta + log = getLogger("red.maxcogs.vanish")