From 7346cf246976ec0873aa143214e5d1b3232d77b5 Mon Sep 17 00:00:00 2001 From: penguinboi Date: Mon, 11 May 2026 18:38:09 -0400 Subject: [PATCH 1/2] bumped python version --- .github/workflows/grace.yml | 10 +++++----- README.md | 2 +- pyproject.toml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/grace.yml b/.github/workflows/grace.yml index 7d443b41..4bc4c0ed 100644 --- a/.github/workflows/grace.yml +++ b/.github/workflows/grace.yml @@ -18,18 +18,18 @@ jobs: steps: - uses: actions/checkout@v2 - - - name: Set up Python 3.10 + + - name: Set up Python 3.12 uses: actions/setup-python@v3 with: - python-version: "3.11" + python-version: "3.12" - name: Install dependencies run: | python -m pip install --upgrade pip pip install flake8 pytest pip install .[dev] - + - name: Lint with ruff run: | ruff check --preview @@ -49,7 +49,7 @@ jobs: adapter = sqlite database = grace_test.db EOF - + - name: Test with pytest run: | pytest -v diff --git a/README.md b/README.md index 6b1ddad1..4ef5fb57 100755 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Installing Grace is fairly simple. You can do it in three short step. 2. [Start the bot](#2-start-the-bot) ### 0. Python and Dependencies -Install [Python](https://www.python.org/downloads/). Python 3.10 or higher is required. +Install [Python](https://www.python.org/downloads/). Python 3.12 or higher is required. > [!NOTE] > We highly recommend that you set up a virtual environment to work on Grace. diff --git a/pyproject.toml b/pyproject.toml index 34db2639..3bb6d504 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ { name = "Code Society Lab" } ] license = { text = "GNU General Public License v3.0" } -requires-python = ">=3.11" +requires-python = ">=3.12" readme = "README.md" dependencies = [ From ee7a61340ca86565b306ca67f7254004ebf156e3 Mon Sep 17 00:00:00 2001 From: penguinboi Date: Mon, 11 May 2026 18:56:37 -0400 Subject: [PATCH 2/2] fix ruff --- bot/__init__.py | 0 bot/extensions/__init__.py | 0 bot/extensions/bookmark_cog.py | 7 +++-- bot/extensions/color_cog.py | 13 +++------ bot/extensions/command_error_handler.py | 11 +++++--- bot/extensions/extension_cog.py | 3 +-- bot/extensions/fun_cog.py | 5 ++-- bot/extensions/grace_cog.py | 2 +- bot/extensions/language_cog.py | 16 +++++------ bot/extensions/mermaid_cog.py | 3 +-- bot/extensions/moderation_cog.py | 17 ++++++------ bot/extensions/pun_cog.py | 8 +++--- bot/extensions/reddit_cog.py | 5 ++-- bot/extensions/reminder_cog.py | 14 +++++----- bot/extensions/thank_cog.py | 9 +++---- bot/extensions/threads_cog.py | 27 ++++++++++--------- bot/extensions/time_cog.py | 5 +++- bot/extensions/weather_cog.py | 5 ++-- bot/extensions/welcome_cog.py | 10 ++++--- bot/extensions/wikipedia_cog.py | 20 ++++++-------- bot/grace.py | 14 +++++----- bot/helpers/__init__.py | 4 +-- bot/helpers/github_helper.py | 8 +++--- bot/helpers/log_helper.py | 7 ++--- bot/models/channel.py | 3 +-- bot/models/extension.py | 3 ++- bot/models/extensions/fun/answer.py | 3 +-- bot/models/extensions/language/pun.py | 6 ++--- bot/models/extensions/language/pun_word.py | 1 - bot/models/extensions/language/trigger.py | 5 ++-- bot/models/extensions/thank.py | 3 +-- bot/models/extensions/thread.py | 7 ++--- bot/services/github_service.py | 3 +-- bot/services/mermaid_service.py | 8 +++--- ...dd_latest_thread_and_daily_reminder_to_.py | 3 +-- lib/bidirectional_iterator.py | 10 +++---- lib/config_required.py | 13 +++++---- lib/fields.py | 3 +-- lib/paged_embeds.py | 9 ++++--- lib/timed_view.py | 4 +-- pyproject.toml | 2 +- tests/extensions/__init__.py | 3 ++- tests/extensions/test_reminder_cog.py | 13 ++++----- tests/extensions/test_threads_cog.py | 5 ++-- tests/models/__init__.py | 9 ++++--- 45 files changed, 163 insertions(+), 166 deletions(-) mode change 100755 => 100644 bot/__init__.py mode change 100755 => 100644 bot/extensions/__init__.py mode change 100755 => 100644 bot/extensions/grace_cog.py mode change 100755 => 100644 bot/extensions/language_cog.py mode change 100755 => 100644 bot/extensions/welcome_cog.py mode change 100755 => 100644 bot/grace.py mode change 100755 => 100644 bot/helpers/__init__.py diff --git a/bot/__init__.py b/bot/__init__.py old mode 100755 new mode 100644 diff --git a/bot/extensions/__init__.py b/bot/extensions/__init__.py old mode 100755 new mode 100644 diff --git a/bot/extensions/bookmark_cog.py b/bot/extensions/bookmark_cog.py index 5dc6fe5a..6f9cdc31 100644 --- a/bot/extensions/bookmark_cog.py +++ b/bot/extensions/bookmark_cog.py @@ -1,4 +1,3 @@ -from typing import List from discord import Embed, File, Interaction, Message from discord.app_commands import ContextMenu @@ -17,7 +16,7 @@ def __init__(self, bot: Grace) -> None: self.bot.tree.add_command(save_message_ctx_menu) - async def get_message_files(self, message: Message) -> List[File]: + async def get_message_files(self, message: Message) -> list[File]: """Fetch files from the message attachments :param message: Message to fetch files from @@ -26,7 +25,7 @@ async def get_message_files(self, message: Message) -> List[File]: :return: List of files :rtype: List[File] """ - return list(map(lambda attachment: attachment.to_file(), message.attachments)) + return [attachment.to_file() for attachment in message.attachments] async def save_message(self, interaction: Interaction, message: Message) -> None: """Saves the message @@ -37,7 +36,7 @@ async def save_message(self, interaction: Interaction, message: Message) -> None :type message: Message """ sent_at: int = int(message.created_at.timestamp()) - files: List[File] = await self.get_message_files(message) + files: list[File] = await self.get_message_files(message) save_embed: Embed = Embed(title="Bookmark Info", color=self.bot.default_color) diff --git a/bot/extensions/color_cog.py b/bot/extensions/color_cog.py index 04dcf432..9065ef09 100644 --- a/bot/extensions/color_cog.py +++ b/bot/extensions/color_cog.py @@ -1,5 +1,4 @@ import os -from typing import Tuple, Union from discord import Color, Embed, File from discord.ext.commands import ( @@ -14,7 +13,7 @@ from bot.helpers.error_helper import send_command_error -def get_embed_color(color: Union[Tuple[int, int, int], str]) -> Color: +def get_embed_color(color: tuple[int, int, int] | str) -> Color: """Convert a color to an Embed Color object. :param color: A tuple of 3 integers in the range 0-255 representing an RGB @@ -60,7 +59,7 @@ async def show_group(self, ctx: Context) -> None: await ctx.send_help(ctx.command) async def display_color( - self, ctx: Context, color: Union[Tuple[int, int, int], str] + self, ctx: Context, color: tuple[int, int, int] | str ) -> None: """Display a color in an embed message. @@ -114,9 +113,7 @@ async def rgb_command_error(self, ctx: Context, error: Exception) -> None: :param error: The error that was raised during command execution. :type error: Exception """ - if isinstance(error, HybridCommandError) or isinstance( - error, CommandInvokeError - ): + if isinstance(error, (HybridCommandError, CommandInvokeError)): await send_command_error( ctx, "Expected rgb color", ctx.command, "244 195 8" ) @@ -149,9 +146,7 @@ async def hex_command_error(self, ctx: Context, error: Exception) -> None: :param error: The error that was raised during command execution. :type error: Exception """ - if isinstance(error, HybridCommandError) or isinstance( - error, CommandInvokeError - ): + if isinstance(error, (HybridCommandError, CommandInvokeError)): await send_command_error( ctx, "Expected hexadecimal color", ctx.command, "#F4C308" ) diff --git a/bot/extensions/command_error_handler.py b/bot/extensions/command_error_handler.py index 5eed9339..a55562c3 100644 --- a/bot/extensions/command_error_handler.py +++ b/bot/extensions/command_error_handler.py @@ -1,6 +1,9 @@ +import logging +from collections.abc import Coroutine from datetime import timedelta -from logging import warning -from typing import Any, Coroutine, Optional +from typing import Any + +logger = logging.getLogger(__name__) from discord import Interaction from discord.ext.commands import ( @@ -33,7 +36,7 @@ async def get_command_error(self, ctx: Context, error: Exception) -> None: :param error: The error that was raised during command execution. :type error: Exception """ - warning(f"Error: {error}. Issued by {ctx.author}") + logger.warning(f"Error: {error}. Issued by {ctx.author}") if isinstance(error, CommandNotFound): await send_command_help(ctx) @@ -57,7 +60,7 @@ async def get_command_error(self, ctx: Context, error: Exception) -> None: @Cog.listener("on_app_command_error") async def get_app_command_error( - self, interaction: Optional[Interaction], _: Exception + self, interaction: Interaction | None, _: Exception ) -> None: """Event listener for command errors that occurred during an interaction. It sends an error message to the user. diff --git a/bot/extensions/extension_cog.py b/bot/extensions/extension_cog.py index ecead7db..443603ee 100644 --- a/bot/extensions/extension_cog.py +++ b/bot/extensions/extension_cog.py @@ -1,4 +1,3 @@ -from typing import List from discord import Embed from discord.app_commands import Choice, autocomplete @@ -25,7 +24,7 @@ def extension_autocomplete(state: bool): :return: An autocomplete function. """ - async def inner_autocomplete(_, current: str) -> List[Choice]: + async def inner_autocomplete(_, current: str) -> list[Choice]: """Autocomplete function for extensions. :param current: The current word being autocompleted. diff --git a/bot/extensions/fun_cog.py b/bot/extensions/fun_cog.py index 46e966c2..6e0eac40 100644 --- a/bot/extensions/fun_cog.py +++ b/bot/extensions/fun_cog.py @@ -1,3 +1,4 @@ +import asyncio from json import loads from random import choice as random_choice @@ -80,8 +81,8 @@ async def quote_command(self, ctx: Context) -> None: :param ctx: The context in which the command was called. :type ctx: Context """ - response = get( - "https://api.forismatic.com/api/1.0/?method=getQuote&format=json&lang=en" + response = await asyncio.to_thread( + get, "https://api.forismatic.com/api/1.0/?method=getQuote&format=json&lang=en" ) if response.ok: diff --git a/bot/extensions/grace_cog.py b/bot/extensions/grace_cog.py old mode 100755 new mode 100644 index 90e9afa4..d79eae9d --- a/bot/extensions/grace_cog.py +++ b/bot/extensions/grace_cog.py @@ -1,6 +1,6 @@ from discord import Embed, Interaction from discord.app_commands import Choice, autocomplete -from discord.ext.commands import Cog, Context, hybrid_command, has_permissions +from discord.ext.commands import Cog, Context, has_permissions, hybrid_command from discord.ui import Button from emoji import emojize diff --git a/bot/extensions/language_cog.py b/bot/extensions/language_cog.py old mode 100755 new mode 100644 index 47807ccb..b6f4fc0d --- a/bot/extensions/language_cog.py +++ b/bot/extensions/language_cog.py @@ -1,4 +1,6 @@ -from logging import warning +import logging + +logger = logging.getLogger(__name__) from discord import Embed, Message from discord.ext.commands import Cog, Context, has_permissions, hybrid_group @@ -55,7 +57,7 @@ async def name_react(self, message: Message) -> None: """ grace_trigger = Trigger.find_by(name="Grace") if grace_trigger is None: - warning('Missing trigger entry for "Grace"') + logger.warning('Missing trigger entry for "Grace"') return if self.bot.user.mentioned_in(message) and not message.content.startswith( @@ -82,11 +84,11 @@ async def penguin_react(self, message: Message) -> None: """ linus_trigger = Trigger.find_by(name="Linus") if linus_trigger is None: - warning('Missing trigger entry for "Linus"') + logger.warning('Missing trigger entry for "Linus"') return message_tokens = self.tokenizer.tokenize(message.content) - tokenlist = list(map(lambda s: s.lower(), message_tokens)) + tokenlist = [s.lower() for s in message_tokens] linustarget = [i for i, x in enumerate(tokenlist) if x in linus_trigger.words] # Get the indices of all linuses in the message @@ -97,9 +99,7 @@ async def penguin_react(self, message: Message) -> None: if ( tokenlist[linusindex + 1] == "tech" and tokenlist[linusindex + 2] == "tips" - ): - fail = True - elif ( + ) or ( tokenlist[linusindex + 1] == "and" and tokenlist[linusindex + 2] == "lucy" ): @@ -143,7 +143,7 @@ async def triggers_group(self, ctx) -> None: if ctx.invoked_subcommand is None: trigger = Trigger.find_by(name="Linus") if trigger is None: - warning('Missing trigger entry for "Linus"') + logger.warning('Missing trigger entry for "Linus"') return embed = Embed( diff --git a/bot/extensions/mermaid_cog.py b/bot/extensions/mermaid_cog.py index b2be7669..68c516e8 100644 --- a/bot/extensions/mermaid_cog.py +++ b/bot/extensions/mermaid_cog.py @@ -1,5 +1,4 @@ import re -from typing import Optional from discord import Embed, Message from discord.ext.commands import Cog, Context, command @@ -83,7 +82,7 @@ def extract_code_block( help="Generate a diagram from mermaid script", usage="՝՝՝\nMermaid script goes here...\n՝՝՝", ) - async def mermaid(self, ctx: Context, *, content: Optional[str]): + async def mermaid(self, ctx: Context, *, content: str | None): """Generates a mermaid diagram Reply with this command to a message that contains a code block with diff --git a/bot/extensions/moderation_cog.py b/bot/extensions/moderation_cog.py index e4dea4c9..84181684 100644 --- a/bot/extensions/moderation_cog.py +++ b/bot/extensions/moderation_cog.py @@ -1,6 +1,7 @@ -from datetime import datetime -from logging import info -from typing import Optional +import logging +from datetime import UTC, datetime + +logger = logging.getLogger(__name__) from discord import Member, Message, Reaction from discord.ext.commands import Cog, Context, has_permissions, hybrid_command @@ -24,7 +25,7 @@ def moderation_channel(self): @hybrid_command(name="purge", help="Deletes n amount of messages.") @has_permissions(manage_messages=True) async def purge( - self, ctx: Context, limit: int, reason: Optional[str] = "No reason given" + self, ctx: Context, limit: int, reason: str | None = "No reason given" ) -> None: """Purge a specified number of messages from the channel. @@ -60,7 +61,7 @@ async def on_reaction_add(self, reaction: Reaction, member: Member) -> None: ) if author.bot or is_already_reacted: - return None + return match demojize(str(reaction.emoji)): case ":SOS_button:": @@ -76,7 +77,7 @@ async def on_reaction_add(self, reaction: Reaction, member: Member) -> None: f"If you need some help, read the <#{guidelines.channel_id}> and open a post in <#{help.channel_id}>!" ) case _: - return None + return # Grace also reacts and log the reaction # because some people remove their reaction afterward @@ -98,11 +99,11 @@ async def on_member_join(self, member) -> None: """ minimum_account_age = app.config.get("moderation", "minimum_account_age") account_age_in_days = ( - datetime.now().replace(tzinfo=None) - member.created_at.replace(tzinfo=None) + datetime.now(tz=UTC) - member.created_at.astimezone(UTC) ).days if account_age_in_days < minimum_account_age: - info(f"{member} kicked due to account age restriction!") + logger.info(f"{member} kicked due to account age restriction!") log = danger("KICK", f"{member} has been kicked.") log.add_field( diff --git a/bot/extensions/pun_cog.py b/bot/extensions/pun_cog.py index d5da8f29..48e5fa44 100644 --- a/bot/extensions/pun_cog.py +++ b/bot/extensions/pun_cog.py @@ -45,7 +45,7 @@ async def pun_react(self, message: Message) -> None: tokenlist = set(map(str.lower, message_tokens)) pun_words = PunWord.distinct().all() - word_set = set(map(lambda pun_word: pun_word.word, pun_words)) + word_set = {pun_word.word for pun_word in pun_words} matches = tokenlist.intersection(word_set) invoked_at = message.created_at.replace(tzinfo=None) @@ -54,7 +54,7 @@ async def pun_react(self, message: Message) -> None: matched_pun_words = filter( lambda pun_word: pun_word.word in matches, pun_words ) - puns = map(lambda pun_word: Pun.find(pun_word.pun_id), matched_pun_words) + puns = (Pun.find(pun_word.pun_id) for pun_word in matched_pun_words) puns = list(filter(lambda pun: pun.can_invoke_at_time(invoked_at), puns)) for pun_word in matched_pun_words: @@ -81,9 +81,7 @@ async def puns_group(self, ctx: Context) -> None: @has_permissions(administrator=True) async def list_puns(self, ctx: Context) -> None: if ctx.invoked_subcommand is None: - pun_texts_with_ids = map( - lambda pun: "{}.\t{}".format(pun.id, pun.text), Pun.all() - ) + pun_texts_with_ids = (f"{pun.id}.\t{pun.text}" for pun in Pun.all()) embed = Embed( color=self.bot.default_color, diff --git a/bot/extensions/reddit_cog.py b/bot/extensions/reddit_cog.py index 087510bb..c232a808 100644 --- a/bot/extensions/reddit_cog.py +++ b/bot/extensions/reddit_cog.py @@ -1,5 +1,4 @@ import re -from typing import List from discord import Embed, Message from discord.ext.commands import Cog @@ -20,7 +19,7 @@ def moderation_channel(self): """Returns the moderation channel""" return self.bot.get_channel_by_name("moderation_logs") - async def notify_moderation(self, message: Message, blacklisted: List[str]): + async def notify_moderation(self, message: Message, blacklisted: list[str]): """Notifies moderators about a blacklisted subreddit mention :param message: Message that contained blacklisted subreddits @@ -35,7 +34,7 @@ async def notify_moderation(self, message: Message, blacklisted: List[str]): ) await log.send(self.moderation_channel) - async def extract_subreddits(self, message: Message) -> List[List]: + async def extract_subreddits(self, message: Message) -> list[list]: """Extracts and filters all mentioned subreddits from a message :param message: Message from which to extract subreddits diff --git a/bot/extensions/reminder_cog.py b/bot/extensions/reminder_cog.py index 2f92dce2..7b9ab52d 100644 --- a/bot/extensions/reminder_cog.py +++ b/bot/extensions/reminder_cog.py @@ -1,10 +1,12 @@ import re -from datetime import datetime, timedelta, tzinfo +from datetime import UTC, datetime, timedelta, tzinfo +from io import BytesIO +from re import Match + from discord import Embed, File -from discord.ext.commands import Cog, hybrid_command, Context +from discord.ext.commands import Cog, Context, hybrid_command from pytz import timezone -from typing import Match -from io import BytesIO + from bot.grace import Grace @@ -51,7 +53,7 @@ def _build_embed(self, title: str, message: str, author: str) -> Embed: color=self.bot.default_color, title=f"**{title}**", description=message, - timestamp=datetime.now(), + timestamp=datetime.now(tz=UTC), ) embed.set_author(name=author) @@ -108,7 +110,7 @@ async def reminder(self, ctx: Context, timer: str, *, message: str) -> None: "date", run_date=reminder_time, args=[ctx, message], - id=f"reminder_{ctx.author.id}_{datetime.now().timestamp()}", + id=f"reminder_{ctx.author.id}_{datetime.now(tz=UTC).timestamp()}", ) ) diff --git a/bot/extensions/thank_cog.py b/bot/extensions/thank_cog.py index dd4a7dc0..81fb725c 100644 --- a/bot/extensions/thank_cog.py +++ b/bot/extensions/thank_cog.py @@ -1,4 +1,3 @@ -from typing import List, Optional from discord import Embed, Member from discord.ext.commands import BucketType, Cog, Context, cooldown, hybrid_group @@ -76,7 +75,7 @@ async def thank_leaderboard(self, ctx: Context, *, top: int = 10) -> None: ) return - helpers: List[Thank] = Thank.order_by(count="desc").limit(top).all() + helpers: list[Thank] = Thank.order_by(count="desc").limit(top).all() if not helpers: await ctx.reply("No helpers found.", ephemeral=True) @@ -91,16 +90,14 @@ async def thank_leaderboard(self, ctx: Context, *, top: int = 10) -> None: for position, helper in enumerate(helpers): member = await self.bot.fetch_user(helper.member_id) leaderboard_embed.description += ( - "{}. **{}**: **{}** with {} thank(s).\n".format( - position + 1, member.display_name, helper.rank, helper.count - ) + f"{position + 1}. **{member.display_name}**: **{helper.rank}** with {helper.count} thank(s).\n" ) await ctx.reply(embed=leaderboard_embed, ephemeral=True) @thank_group.command(name="rank", description="Shows your current thank rank.") async def thank_rank( - self, ctx: Context, *, member: Optional[Member] = None + self, ctx: Context, *, member: Member | None = None ) -> None: """Show the current rank of the member who issue this command. diff --git a/bot/extensions/threads_cog.py b/bot/extensions/threads_cog.py index 74aec001..f796a462 100644 --- a/bot/extensions/threads_cog.py +++ b/bot/extensions/threads_cog.py @@ -1,17 +1,18 @@ +import logging import traceback -from logging import info -from pytz import timezone -from datetime import datetime +from datetime import UTC, datetime + +logger = logging.getLogger(__name__) from discord import Embed, Interaction, TextStyle from discord.app_commands import Choice, autocomplete - +from discord.ext.commands import Cog, Context, has_permissions, hybrid_group from discord.ui import Modal, TextInput -from discord.ext.commands import Cog, has_permissions, hybrid_group, Context +from pytz import timezone -from bot.models.extensions.thread import Thread from bot.classes.recurrence import Recurrence from bot.extensions.command_error_handler import send_command_help +from bot.models.extensions.thread import Thread from lib.config_required import cog_config_required @@ -33,8 +34,8 @@ class ThreadModal(Modal, title="Thread"): def __init__( self, recurrence: Recurrence, - reminder: bool = None, - thread: Thread = None, + reminder: bool | None = None, + thread: Thread | None = None, ): super().__init__() @@ -143,7 +144,7 @@ def cog_load(self): async def daily_reminder(self): """Send a daily reminder for active threads.""" - info("Posting daily threads's reminder") + logger.info("Posting daily threads's reminder") embed = Embed( color=self.bot.default_color, @@ -175,19 +176,19 @@ def cog_unload(self): self.bot.scheduler.remove_job(job.id) async def daily_post(self): - info("Posting daily threads") + logger.info("Posting daily threads") for thread in Thread.find_by_recurrence(Recurrence.DAILY): await self.post_thread(thread) async def weekly_post(self): - info("Posting weekly threads") + logger.info("Posting weekly threads") for thread in Thread.find_by_recurrence(Recurrence.WEEKLY): await self.post_thread(thread) async def monthly_post(self): - info("Posting monthly threads") + logger.info("Posting monthly threads") for thread in Thread.find_by_recurrence(Recurrence.MONTHLY): await self.post_thread(thread) @@ -201,7 +202,7 @@ async def post_thread(self, thread: Thread): color=self.bot.default_color, title=thread.title, description=thread.content, - timestamp=datetime.now() + timestamp=datetime.now(tz=UTC) ) if channel: diff --git a/bot/extensions/time_cog.py b/bot/extensions/time_cog.py index 17826cb0..9ef1d86e 100644 --- a/bot/extensions/time_cog.py +++ b/bot/extensions/time_cog.py @@ -1,6 +1,9 @@ +import logging import re from datetime import datetime, timedelta +logger = logging.getLogger(__name__) + import pytz from dateutil import parser from discord import Message @@ -148,7 +151,7 @@ async def on_message(self, message: Message) -> None: timestamp = self._build_timestamp(utc, time_str) await message.reply(f"") except Exception: - pass + logger.debug("Could not parse time string", exc_info=True) async def setup(bot): diff --git a/bot/extensions/weather_cog.py b/bot/extensions/weather_cog.py index ba249a12..bf78c29e 100644 --- a/bot/extensions/weather_cog.py +++ b/bot/extensions/weather_cog.py @@ -1,3 +1,4 @@ +import asyncio from datetime import datetime from string import capwords @@ -72,8 +73,8 @@ async def get_weather(self, city: str): :rtype: dict """ # complete_url to retreive weather info - response = get( - f"{self.OPENWEATHER_BASE_URL}/weather?appid={self.api_key}&q={city}" + response = await asyncio.to_thread( + get, f"{self.OPENWEATHER_BASE_URL}/weather?appid={self.api_key}&q={city}" ) # code 200 means the city is found otherwise, city is not found diff --git a/bot/extensions/welcome_cog.py b/bot/extensions/welcome_cog.py old mode 100755 new mode 100644 index 4945dd71..71956629 --- a/bot/extensions/welcome_cog.py +++ b/bot/extensions/welcome_cog.py @@ -1,4 +1,6 @@ -from logging import info +import logging + +logger = logging.getLogger(__name__) from discord import Embed, Member from discord.ext.commands import Cog, hybrid_command @@ -117,7 +119,7 @@ async def on_member_update(self, before, after): :type after: discord.Member """ if not before.bot and (before.pending and not after.pending): - info(f"{after.display_name} accepted the rules!") + logger.info(f"{after.display_name} accepted the rules!") embed = self.__build_embed(after) welcome_channel = self.bot.get_channel_by_name("welcome") @@ -134,7 +136,7 @@ async def on_member_join(self, member): :param member: The member who joined the server. :type member: discord.Member """ - info(f"{member.display_name} joined the server!") + logger.info(f"{member.display_name} joined the server!") @hybrid_command( name="welcome", description="Welcomes the person who issues the command" @@ -145,7 +147,7 @@ async def welcome_command(self, ctx): :param ctx: The context in which the command was invoked. :type ctx: Context """ - info(f"{ctx.author.display_name} asked to get welcomed!") + logger.info(f"{ctx.author.display_name} asked to get welcomed!") embed = self.__build_embed(ctx.author) await ctx.send(embed=embed, ephemeral=True) diff --git a/bot/extensions/wikipedia_cog.py b/bot/extensions/wikipedia_cog.py index bd52cbab..9f150ea0 100644 --- a/bot/extensions/wikipedia_cog.py +++ b/bot/extensions/wikipedia_cog.py @@ -1,5 +1,5 @@ from json import loads -from typing import Any, List +from typing import Any from urllib.parse import quote_plus from urllib.request import Request, urlopen @@ -13,7 +13,7 @@ USER_AGENT = "grace-bot/1.0 (https://github.com/Code-Society-Lab/grace)" -def search_results(search: str) -> List[Any]: +def search_results(search: str) -> list[Any]: """Return search results from Wikipedia for the given search query. :param search: The search query to be used to search Wikipedia. @@ -31,11 +31,11 @@ def search_results(search: str) -> List[Any]: class Buttons(View): - def __init__(self, search: str, result: List[Any]) -> None: + def __init__(self, search: str, result: list[Any]) -> None: super().__init__() self.search: str = search - self.result: List[Any] = result + self.result: list[Any] = result async def wiki_result( self, interaction: Interaction, _: Button, index: int @@ -52,9 +52,7 @@ async def wiki_result( """ if len(self.result[3]) >= index: await interaction.response.send_message( - "{mention} requested:\n {request}".format( - mention=interaction.user.mention, request=self.result[3][index - 1] - ) + f"{interaction.user.mention} requested:\n {self.result[3][index - 1]}" ) self.stop() else: @@ -89,7 +87,7 @@ async def wiki(self, ctx: Context, *, search: str) -> None: :param search: The search query to be used to search Wikipedia. :type search: str """ - result: List[Any] = search_results(search) + result: list[Any] = search_results(search) view: Buttons = Buttons(search, result) if len(result[1]) == 0: @@ -98,10 +96,8 @@ async def wiki(self, ctx: Context, *, search: str) -> None: ) else: result_view = "" - search_count = 1 - for result in result[1]: - result_view += f"{str(search_count)}: {result}\n" - search_count += 1 + for search_count, item in enumerate(result[1], start=1): + result_view += f"{search_count!s}: {item}\n" embed = Embed( color=0x2376FF, diff --git a/bot/grace.py b/bot/grace.py old mode 100755 new mode 100644 index 1e9a61dc..8e83322a --- a/bot/grace.py +++ b/bot/grace.py @@ -1,11 +1,13 @@ -from logging import info, warning +import logging + +logger = logging.getLogger(__name__) from discord import Activity, ActivityType, Colour, Intents +from grace.bot import Bot from pretty_help import PrettyHelp from bot.models.channel import Channel from bot.models.extension import Extension -from grace.bot import Bot class Grace(Bot): @@ -34,17 +36,17 @@ async def load_extensions(self): extension = Extension.where(module_name=module).first() if not extension: - warning(f"{module} is not registered. Registering the extension.") + logger.warning(f"{module} is not registered. Registering the extension.") extension = Extension.create(module_name=module) if not extension.should_be_loaded(): extension.disable() if extension.is_enabled(): - info(f"Loading {module}") + logger.info(f"Loading {module}") await self.load_extension(module) else: - info(f"{module} is disabled, it will not be loaded.") + logger.info(f"{module} is disabled, it will not be loaded.") async def on_ready(self): - info(f"{self.user.name}#{self.user.id} is online and ready to use!") + logger.info(f"{self.user.name}#{self.user.id} is online and ready to use!") diff --git a/bot/helpers/__init__.py b/bot/helpers/__init__.py old mode 100755 new mode 100644 index 200d255b..3b78409c --- a/bot/helpers/__init__.py +++ b/bot/helpers/__init__.py @@ -2,5 +2,5 @@ # and would give 'run command not found' error when running grace # Main purpose is to streamline/simplify importing of the logging functions # that are separated into multiple modules -from bot.helpers.error_helper import * # noqa - ignoring violations -from bot.helpers.log_helper import * # noqa +from bot.helpers.error_helper import * +from bot.helpers.log_helper import * diff --git a/bot/helpers/github_helper.py b/bot/helpers/github_helper.py index 4df835fb..57e47d11 100644 --- a/bot/helpers/github_helper.py +++ b/bot/helpers/github_helper.py @@ -1,5 +1,5 @@ +from collections.abc import Iterable from math import ceil -from typing import Iterable, List from discord import Color, Embed from discord.ui import Button @@ -11,16 +11,16 @@ def available_project_names() -> Iterable[str]: organization: Organization = GithubService().get_code_society_lab() - return map(lambda r: r.name, organization.get_repos()) + return (r.name for r in organization.get_repos()) -def create_contributors_embeds(repository: Repository) -> List[Embed]: +def create_contributors_embeds(repository: Repository) -> list[Embed]: """Get an embed with a list of contributors for the Cursif repository. :return: An embed with a list of contributors. :rtype: Embed """ - embeds: List[Embed] = [] + embeds: list[Embed] = [] contributors = repository.get_contributors() page_count: int = ceil(contributors.totalCount / 25) diff --git a/bot/helpers/log_helper.py b/bot/helpers/log_helper.py index 8857eb96..79b5945d 100644 --- a/bot/helpers/log_helper.py +++ b/bot/helpers/log_helper.py @@ -1,4 +1,5 @@ -from datetime import datetime +from datetime import UTC, datetime +from typing import ClassVar from discord import Color, Embed @@ -22,7 +23,7 @@ def danger(title, description): class LogHelper: __DEFAULT_COLOR = Color.from_rgb(0, 123, 255) - COLORS_BY_LOG_LEVEL = { + COLORS_BY_LOG_LEVEL: ClassVar[dict] = { "danger": Color.from_rgb(220, 53, 69), "warning": Color.from_rgb(255, 193, 7), "info": __DEFAULT_COLOR, @@ -33,7 +34,7 @@ def __init__(self, title, description, log_level="info"): title=title, description=description, color=self.COLORS_BY_LOG_LEVEL.get(log_level, self.__DEFAULT_COLOR), - timestamp=datetime.utcnow(), + timestamp=datetime.now(tz=UTC), ) def add_field(self, name, value): diff --git a/bot/models/channel.py b/bot/models/channel.py index 1cbecb84..5467563f 100644 --- a/bot/models/channel.py +++ b/bot/models/channel.py @@ -1,6 +1,5 @@ -from sqlalchemy import UniqueConstraint - from grace.model import Field, Model +from sqlalchemy import UniqueConstraint class Channel(Model): diff --git a/bot/models/extension.py b/bot/models/extension.py index 8252bd18..916274a1 100644 --- a/bot/models/extension.py +++ b/bot/models/extension.py @@ -1,6 +1,7 @@ +from grace.model import Field, Model + from bot import app from bot.classes.state import State -from grace.model import Field, Model from lib.fields import EnumField diff --git a/bot/models/extensions/fun/answer.py b/bot/models/extensions/fun/answer.py index 79374a3f..d23685c7 100644 --- a/bot/models/extensions/fun/answer.py +++ b/bot/models/extensions/fun/answer.py @@ -1,4 +1,3 @@ -from typing import Optional from grace.model import Field, Model @@ -6,5 +5,5 @@ class Answer(Model): __tablename__ = "answers" - id: Optional[int] = Field(default=None, primary_key=True) + id: int | None = Field(default=None, primary_key=True) answer: str = Field(max_length=255) diff --git a/bot/models/extensions/language/pun.py b/bot/models/extensions/language/pun.py index acd31781..1781b16d 100644 --- a/bot/models/extensions/language/pun.py +++ b/bot/models/extensions/language/pun.py @@ -1,9 +1,9 @@ from datetime import datetime, timedelta -from typing import List + +from grace.model import Field, Model, Relationship from bot.models.bot import BotSettings from bot.models.extensions.language.pun_word import PunWord -from grace.model import Field, Model, Relationship class Pun(Model): @@ -12,7 +12,7 @@ class Pun(Model): id: int | None = Field(default=None, primary_key=True) text: str | None = Field(unique=True) last_invoked: datetime | None = Field(default=None) - pun_words: List["PunWord"] = Relationship( + pun_words: list["PunWord"] = Relationship( back_populates="pun", sa_relationship_kwargs={"lazy": "selectin"} ) diff --git a/bot/models/extensions/language/pun_word.py b/bot/models/extensions/language/pun_word.py index 6f48880a..be65cbc4 100644 --- a/bot/models/extensions/language/pun_word.py +++ b/bot/models/extensions/language/pun_word.py @@ -1,7 +1,6 @@ from typing import TYPE_CHECKING from emoji import emojize - from grace.model import Field, Model, Relationship if TYPE_CHECKING: diff --git a/bot/models/extensions/language/trigger.py b/bot/models/extensions/language/trigger.py index a5a683bc..ce5f8417 100644 --- a/bot/models/extensions/language/trigger.py +++ b/bot/models/extensions/language/trigger.py @@ -1,9 +1,8 @@ -from typing import List from emoji import emojize +from grace.model import Field, Model, Relationship from bot.models.extensions.language.trigger_word import TriggerWord -from grace.model import Field, Model, Relationship class Trigger(Model): @@ -14,7 +13,7 @@ class Trigger(Model): positive_emoji_code: str = Field(max_length=255) negative_emoji_code: str = Field(max_length=255) - trigger_words: List[TriggerWord] = Relationship( + trigger_words: list[TriggerWord] = Relationship( back_populates="trigger", sa_relationship_kwargs={"lazy": "selectin"} ) diff --git a/bot/models/extensions/thank.py b/bot/models/extensions/thank.py index 3d4b9d92..e2e40a75 100644 --- a/bot/models/extensions/thank.py +++ b/bot/models/extensions/thank.py @@ -1,4 +1,3 @@ -from typing import Optional from grace.model import Field, Model @@ -13,7 +12,7 @@ class Thank(Model): count: int | None = Field(default=0) @property - def rank(self) -> Optional[str]: + def rank(self) -> str | None: """Returns the rank of the member based on the number of times they have been thanked. diff --git a/bot/models/extensions/thread.py b/bot/models/extensions/thread.py index 5b73b43d..cdc9c061 100644 --- a/bot/models/extensions/thread.py +++ b/bot/models/extensions/thread.py @@ -1,8 +1,9 @@ +from typing import Self + +from grace.model import Field, Model from sqlalchemy import Text -from typing import List, Self from bot.classes.recurrence import Recurrence -from grace.model import Field, Model from lib.fields import EnumField @@ -19,5 +20,5 @@ class Thread(Model): daily_reminder: bool @classmethod - def find_by_recurrence(cls, recurrence: Recurrence) -> List[Self]: + def find_by_recurrence(cls, recurrence: Recurrence) -> list[Self]: return cls.where(recurrence=recurrence.value).all() diff --git a/bot/services/github_service.py b/bot/services/github_service.py index c9978207..6d9d0c87 100644 --- a/bot/services/github_service.py +++ b/bot/services/github_service.py @@ -1,4 +1,3 @@ -from typing import Optional, Union from github import Github, Organization from github.Repository import Repository @@ -7,7 +6,7 @@ class GithubService(Github): - __token: Optional[Union[str, int, bool]] = app.config.get("github", "api_key") + __token: str | int | bool | None = app.config.get("github", "api_key") def __init__(self): if self.__token: diff --git a/bot/services/mermaid_service.py b/bot/services/mermaid_service.py index 08589be9..48680a53 100644 --- a/bot/services/mermaid_service.py +++ b/bot/services/mermaid_service.py @@ -1,7 +1,9 @@ import base64 import json +import logging import zlib -from logging import critical, info + +logger = logging.getLogger(__name__) import requests @@ -50,10 +52,10 @@ def _is_valid_diagram(url: str) -> bool: """ try: response = requests.get(url, timeout=5) - info(f"url: {url} - code: {response.status_code}") + logger.info(f"url: {url} - code: {response.status_code}") return response.status_code == 200 except requests.RequestException as e: - critical(e) + logger.critical(e) return False diff --git a/db/alembic/versions/b7c695397ab2_add_latest_thread_and_daily_reminder_to_.py b/db/alembic/versions/b7c695397ab2_add_latest_thread_and_daily_reminder_to_.py index 6f5f443d..04a23a68 100644 --- a/db/alembic/versions/b7c695397ab2_add_latest_thread_and_daily_reminder_to_.py +++ b/db/alembic/versions/b7c695397ab2_add_latest_thread_and_daily_reminder_to_.py @@ -6,9 +6,8 @@ """ -from alembic import op import sqlalchemy as sa - +from alembic import op # revision identifiers, used by Alembic. revision = "b7c695397ab2" diff --git a/lib/bidirectional_iterator.py b/lib/bidirectional_iterator.py index 215d6772..646954f2 100644 --- a/lib/bidirectional_iterator.py +++ b/lib/bidirectional_iterator.py @@ -1,9 +1,7 @@ -from typing import Generic, Iterator, List, Optional, TypeVar +from collections.abc import Iterator -T = TypeVar("T") - -class BidirectionalIterator(Generic[T]): +class BidirectionalIterator[T]: """An iterator allows to go forward and backward in a list, modify the list during iteration and obtain the item at the current position in the list. @@ -12,8 +10,8 @@ class BidirectionalIterator(Generic[T]): :type collection: Optional[List[T]] """ - def __init__(self, collection: Optional[List[T]]): - self.__collection: List[T] = collection or [] + def __init__(self, collection: list[T] | None): + self.__collection: list[T] = collection or [] self.__position: int = 0 @property diff --git a/lib/config_required.py b/lib/config_required.py index c794a59a..a36474e4 100644 --- a/lib/config_required.py +++ b/lib/config_required.py @@ -1,4 +1,4 @@ -from typing import Callable, Optional +from collections.abc import Callable from discord.ext import commands from discord.ext.commands import CogMeta, Context, DisabledCommand @@ -13,7 +13,6 @@ class ConfigRequiredError(DisabledCommand): other CommandError exception in `on_command_error` """ - pass class MissingRequiredConfigError(ConfigRequiredError): @@ -22,7 +21,7 @@ class MissingRequiredConfigError(ConfigRequiredError): Inherit from `ConfigRequiredError` """ - def __init__(self, section_key: str, value_key: str, message: Optional[str] = None): + def __init__(self, section_key: str, value_key: str, message: str | None = None): base_error_message = f"Missing config '{value_key}' in section '{section_key}'" super().__init__( f"{base_error_message}\n{message}" if message else base_error_message @@ -30,7 +29,7 @@ def __init__(self, section_key: str, value_key: str, message: Optional[str] = No def cog_config_required( - section_key: str, value_key: str, message: Optional[str] = None + section_key: str, value_key: str, message: str | None = None ) -> Callable: """Validates the presences of a given configuration before each invocation of a `discord.ext.commands.Cog` commands @@ -49,8 +48,8 @@ async def _cog_before_invoke(self, _: Context): if not self.required_config: raise MissingRequiredConfigError(section_key, value_key, message) - setattr(cls, "required_config", app.config.get(section_key, value_key)) - setattr(cls, "cog_before_invoke", _cog_before_invoke) + cls.required_config = app.config.get(section_key, value_key) + cls.cog_before_invoke = _cog_before_invoke return cls @@ -58,7 +57,7 @@ async def _cog_before_invoke(self, _: Context): def command_config_required( - section_key: str, value_key: str, message: Optional[str] = None + section_key: str, value_key: str, message: str | None = None ) -> Callable[[Context], bool]: """Validates the presences of a given configuration before running the `discord.ext.commands.Command` diff --git a/lib/fields.py b/lib/fields.py index 807909f9..17839a68 100644 --- a/lib/fields.py +++ b/lib/fields.py @@ -1,4 +1,3 @@ -from typing import Type from sqlalchemy import Column from sqlalchemy.types import Integer, TypeDecorator @@ -24,7 +23,7 @@ def process_result_value(self, value, dialect): return self.enumtype(value) if value is not None else None -def EnumField(enum_cls: Type, **kwargs): +def EnumField(enum_cls: type, **kwargs): """ Wrapper around sqlmodel.Field for integer-backed enums. Allows passing nullable, default, etc. diff --git a/lib/paged_embeds.py b/lib/paged_embeds.py index 0802b3bb..4f61f790 100644 --- a/lib/paged_embeds.py +++ b/lib/paged_embeds.py @@ -1,4 +1,5 @@ -from typing import Any, Callable, List, Optional +from collections.abc import Callable +from typing import Any from discord import Embed, Interaction, Message from discord.ext.commands import Context @@ -21,12 +22,12 @@ async def callback(self, interaction: Interaction) -> Any: class PagedEmbedView(View): - def __init__(self, embeds: List[Embed]): + def __init__(self, embeds: list[Embed]): super().__init__() - self.__message: Optional[Message] = None + self.__message: Message | None = None self.__embeds: BidirectionalIterator[Embed] = BidirectionalIterator(embeds) - self.__arrow_button: List[EmbedButton] = [ + self.__arrow_button: list[EmbedButton] = [ EmbedButton( self.__embeds.previous, emoji=emojize(":left_arrow:"), disabled=True ), diff --git a/lib/timed_view.py b/lib/timed_view.py index b5ef00f2..0c27644d 100644 --- a/lib/timed_view.py +++ b/lib/timed_view.py @@ -1,7 +1,6 @@ from asyncio import Task, create_task from asyncio import sleep as async_sleep from datetime import timedelta -from typing import Optional from discord.ui import View @@ -23,7 +22,7 @@ def __init__(self, seconds: int = 900): super().__init__(timeout=None) self.seconds: int = seconds - self.__timer_task: Optional[Task[None]] = None + self.__timer_task: Task[None] | None = None @property def seconds(self) -> int: @@ -80,7 +79,6 @@ async def on_timer_update(self): This callback does nothing by default but can be overriden to change its behaviour. """ - pass async def on_timer_elapsed(self): """A callback that is called when the timer elapsed. diff --git a/pyproject.toml b/pyproject.toml index 3bb6d504..28140b1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dev = [ "pytest-asyncio", "flake8", "mypy", - "ruff", + "ruff==0.15.12", "freezegun", ] diff --git a/tests/extensions/__init__.py b/tests/extensions/__init__.py index 83527638..a6f58f06 100644 --- a/tests/extensions/__init__.py +++ b/tests/extensions/__init__.py @@ -1,6 +1,7 @@ -from bot import app from grace.database import up_migration +from bot import app + app.load("test") app.drop_tables() diff --git a/tests/extensions/test_reminder_cog.py b/tests/extensions/test_reminder_cog.py index f9c8bca1..43e54cc2 100644 --- a/tests/extensions/test_reminder_cog.py +++ b/tests/extensions/test_reminder_cog.py @@ -1,12 +1,13 @@ import re -import pytest +from datetime import datetime, timedelta +from unittest.mock import AsyncMock, MagicMock -from datetime import timedelta, datetime -from freezegun import freeze_time -from unittest.mock import MagicMock, AsyncMock +import pytest +from dateutil.tz import tzlocal from discord import Embed +from freezegun import freeze_time + from bot.extensions.reminder_cog import ReminderCog -from dateutil.tz import tzlocal @pytest.fixture @@ -116,7 +117,7 @@ async def test_reminder_valid_input(reminder_cog, timer): assert kwargs["args"][0] == ctx assert kwargs["args"][1] == message assert kwargs["id"].startswith( - f"reminder_{ctx.author.id}_{datetime.now().timestamp()}" + f"reminder_{ctx.author.id}_{datetime.now(tz=tzlocal()).timestamp()}" ) assert kwargs["run_date"] >= datetime.now(tz=tzlocal()) diff --git a/tests/extensions/test_threads_cog.py b/tests/extensions/test_threads_cog.py index 31550c0b..8fddf69e 100644 --- a/tests/extensions/test_threads_cog.py +++ b/tests/extensions/test_threads_cog.py @@ -1,7 +1,8 @@ +from unittest.mock import AsyncMock, MagicMock + import pytest from bot.extensions.threads_cog import ThreadsCog -from unittest.mock import AsyncMock, MagicMock from bot.models.extensions.thread import Thread @@ -131,7 +132,7 @@ async def test_daily_reminder_with_active_threads(threads_cog, mock_bot): await threads_cog.daily_reminder() mock_channel.send.assert_awaited_once() - args, kwargs = mock_channel.send.await_args + _args, kwargs = mock_channel.send.await_args embed = kwargs.get("embed") assert embed is not None diff --git a/tests/models/__init__.py b/tests/models/__init__.py index d66b5c4d..e1b4e4b3 100644 --- a/tests/models/__init__.py +++ b/tests/models/__init__.py @@ -1,8 +1,11 @@ -from logging import info +import logging + +logger = logging.getLogger(__name__) + +from grace.database import up_migration from bot import app from db.seed import get_seed_modules -from grace.database import up_migration app.load("test") @@ -16,5 +19,5 @@ up_migration(app, "head") for seed_module in get_seed_modules(): - info(f"Seeding {seed_module.__name__}") + logger.info(f"Seeding {seed_module.__name__}") seed_module.seed_database()