diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml
index 786c8e3..fff51c0 100644
--- a/.github/workflows/python-app.yml
+++ b/.github/workflows/python-app.yml
@@ -13,29 +13,59 @@ permissions:
contents: read
jobs:
- build:
-
+ ruff:
runs-on: ubuntu-latest
-
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.12
- uses: actions/setup-python@v3
+ uses: actions/setup-python@v6
with:
python-version: "3.12"
+ cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
- pip install flake8 pytest
+ pip install ruff
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- - name: Lint with flake8
+ - name: Lint with ruff
run: |
# stop the build if there are Python syntax errors or undefined names
- flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
- # can also be more harsh :D
- # flake8 . --count --ignore=W503,W504 --show-source --statistics
+ ruff check . --select=E9,F63,F7,F8 --statistics
# exit-zero treats all errors as warnings.
- flake8 . --count --exit-zero --max-complexity=10 --statistics
- - name: Test with pytest
+ ruff check . --exit-zero
+
+ mypy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python 3.12
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.12"
+ cache: 'pip'
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install mypy
+ if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+ - name: Lint with mypy
+ run: |
+ mypy main.py
+
+ pytest:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python 3.12
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.12"
+ cache: 'pip'
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install pytest
+ if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+ - name: Lint with pytest
run: |
pytest
diff --git a/.gitignore b/.gitignore
index 49b54a8..4e02593 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ __pycache__
Folder.DotSettings.user
rina_docs
.coverage
+.mypy_cache
diff --git a/extensions/addons/cogs/otheraddons.py b/extensions/addons/cogs/otheraddons.py
index 56208de..3e4d7dc 100644
--- a/extensions/addons/cogs/otheraddons.py
+++ b/extensions/addons/cogs/otheraddons.py
@@ -1,6 +1,5 @@
-import json # to read API json responses
-import requests # to read api calls
+import aiohttp
import discord
import discord.app_commands as app_commands
import discord.ext.commands as commands
@@ -297,11 +296,8 @@ async def convert_unit(
"appid": itx.client.api_tokens['Open Exchange Rates'],
"show_alternative": "true",
}
- response_api = requests.get(
- "https://openexchangerates.org/api/latest.json",
- params=params
- ).text
- data = json.loads(response_api)
+ async with aiohttp.ClientSession() as client, client.get("https://openexchangerates.org/api/latest.json", params=params) as response:
+ data = await response.json()
if data.get("error", 0):
await itx.response.send_message(
"I'm sorry, something went wrong while trying to get "
diff --git a/extensions/addons/cogs/searchaddons.py b/extensions/addons/cogs/searchaddons.py
index a621140..e823df9 100644
--- a/extensions/addons/cogs/searchaddons.py
+++ b/extensions/addons/cogs/searchaddons.py
@@ -1,6 +1,7 @@
import json # to read API json responses
-import requests # to read api calls
+import aiohttp # to read api calls
+import aiohttp.client_exceptions
import discord
import discord.app_commands as app_commands
@@ -375,11 +376,8 @@ async def equaldex(self, itx: discord.Interaction[Bot], country_id: str):
"apiKey": equaldex_key,
# "formatted": "true",
}
- response = requests.get(
- "https://www.equaldex.com/api/region",
- params=querystring
- )
- response_api = response.text
+ async with aiohttp.ClientSession() as client, client.get("https://www.equaldex.com/api/region", params=querystring) as response:
+ response_api = await response.text()
# returns ->
{"regions":{...}} <- so you need to
# remove the and
parts. It also has some
#
\r\n strings in there for some reason...? so uh
@@ -505,11 +503,9 @@ async def math(self, itx: discord.Interaction[Bot], query: str):
"output": "json",
}
try:
- api_response: WolframResult = requests.get(
- "https://api.wolframalpha.com/v2/query",
- params=params
- ).json()
- except requests.exceptions.JSONDecodeError:
+ async with aiohttp.ClientSession() as client, client.get("https://api.wolframalpha.com/v2/query", params=params) as response:
+ api_response: WolframResult = await response.json()
+ except (aiohttp.client_exceptions.ContentTypeError, json.JSONDecodeError):
await itx.followup.send(
"Your input gave a malformed result! Perhaps it took "
"too long to calculate...",
diff --git a/extensions/compliments/cogs/compliments.py b/extensions/compliments/cogs/compliments.py
index 6365f18..f5eb865 100644
--- a/extensions/compliments/cogs/compliments.py
+++ b/extensions/compliments/cogs/compliments.py
@@ -336,7 +336,7 @@ async def on_message(self, message: discord.Message):
if called_cute is True:
try:
await message.add_reaction("<:this:960916817801535528>")
- except (discord.HTTPException or discord.NotFound):
+ except (discord.HTTPException, discord.NotFound):
await log_to_guild(
self.client,
message.guild,
diff --git a/extensions/help/helppages.py b/extensions/help/helppages.py
index 2aee417..e37bed6 100644
--- a/extensions/help/helppages.py
+++ b/extensions/help/helppages.py
@@ -846,7 +846,7 @@
}
-FIRST_PAGE: int = list(help_pages)[0]
+FIRST_PAGE: int = next(iter(help_pages))
aliases: dict[int, list[str]] = {
diff --git a/extensions/reminders/objects/reminderobject.py b/extensions/reminders/objects/reminderobject.py
index 5247260..5be10b0 100644
--- a/extensions/reminders/objects/reminderobject.py
+++ b/extensions/reminders/objects/reminderobject.py
@@ -37,8 +37,7 @@ async def relaunch_ongoing_reminders(
store new scheduler events.
"""
collection = client.async_rina_db["reminders"]
- query = {}
- db_data = collection.find(query)
+ db_data = collection.find({})
async for entry in db_data:
entry: DatabaseData
try:
@@ -128,7 +127,7 @@ def __init__(
run_date=self.remindertime
)
- async def send_reminder(self):
+ async def send_reminder(self) -> None:
user = await self.client.fetch_user(self.userID)
creationtime = int(self.creationtime.timestamp())
try:
@@ -143,6 +142,12 @@ async def send_reminder(self):
collection = self.client.rina_db["reminders"]
query = {"userID": self.userID}
db_data: DatabaseData | None = collection.find_one(query)
+ # If the user isn't in the database, despite a reminder object existing, something has gone *bad*.
+ #
+ # But probably not bad enough to crash over, but...
+ # TODO: should log this
+ if db_data is None:
+ return
reminders = db_data["reminders"]
index_subtraction = 0
for reminder_index in range(len(reminders)):
@@ -412,7 +417,7 @@ async def _create_reminder(
)
await view.wait()
- if view.value == 1:
+ if view.return_interaction is not None:
msg = (f"{itx.user.mention} shared a reminder on "
f"for \"{reminder}\"")
copy_view = CopyReminder(
@@ -421,11 +426,17 @@ async def _create_reminder(
timeout=300
)
try:
- await itx.channel.send(
- content=msg,
- view=copy_view,
- allowed_mentions=discord.AllowedMentions.none()
- )
+ if isinstance(itx.channel, discord.abc.Messageable):
+ await itx.channel.send(
+ content=msg,
+ view=copy_view,
+ allowed_mentions=discord.AllowedMentions.none()
+ )
+ else:
+ await view.return_interaction.response.send_message(
+ msg, view=copy_view,
+ allowed_mentions=discord.AllowedMentions.none()
+ )
except discord.errors.Forbidden:
await view.return_interaction.response.send_message(
msg, view=copy_view,
diff --git a/extensions/reminders/utils.py b/extensions/reminders/utils.py
index 0e2ddd9..750a1d5 100644
--- a/extensions/reminders/utils.py
+++ b/extensions/reminders/utils.py
@@ -1,5 +1,7 @@
import discord
+from typing import cast
+
from resources.customs import Bot
from extensions.reminders.objects import (
@@ -14,7 +16,8 @@ def get_user_reminders(
# Check if user has an entry in database yet.
collection = client.rina_db["reminders"]
query = {"userID": user.id}
- db_data: DatabaseData = collection.find_one(query)
+ # We're getting this straight from the DB, so it should be fine
+ db_data = cast(DatabaseData, collection.find_one(query))
if db_data is None:
collection.insert_one(query)
db_data = collection.find_one(query)
diff --git a/extensions/reminders/views/sharereminder.py b/extensions/reminders/views/sharereminder.py
index bcf15ef..34ec43f 100644
--- a/extensions/reminders/views/sharereminder.py
+++ b/extensions/reminders/views/sharereminder.py
@@ -5,9 +5,16 @@
class ShareReminder(discord.ui.View):
- def __init__(self, timeout=300):
+ timeout: int|float
+ return_interaction: discord.Interaction[Bot] | None
+
+ # This can be entirely synthesized from the value of return_interaction, given that the API guarantees that the given interaction is not None
+ @property
+ def value(self) -> int:
+ return int(self.return_interaction is not None)
+
+ def __init__(self, timeout:float=300):
super().__init__()
- self.value = 0
self.timeout = timeout
self.return_interaction: discord.Interaction[Bot] | None = None
self.add_item(create_simple_button(
@@ -17,6 +24,5 @@ def __init__(self, timeout=300):
)
async def callback(self, interaction: discord.Interaction[Bot]):
- self.value = 1
self.return_interaction = interaction
self.stop()
diff --git a/extensions/settings/objects/server_attributes.py b/extensions/settings/objects/server_attributes.py
index 5e416d7..234a4c4 100644
--- a/extensions/settings/objects/server_attributes.py
+++ b/extensions/settings/objects/server_attributes.py
@@ -20,6 +20,7 @@
| discord.Role | list[discord.Role]
| discord.User
| discord.VoiceChannel
+ | discord.ForumChannel
)
diff --git a/extensions/settings/objects/server_settings.py b/extensions/settings/objects/server_settings.py
index cf36019..92a4f6d 100644
--- a/extensions/settings/objects/server_settings.py
+++ b/extensions/settings/objects/server_settings.py
@@ -10,7 +10,7 @@
import discord
from resources.utils.debug import debug, DebugColor
-from .server_attributes import ServerAttributes, GuildAttributeType
+from .server_attributes import ServerAttributes, GuildAttributeType, MessageableGuildChannel
from .server_attribute_ids import ServerAttributeIds
from .enabled_modules import EnabledModules
@@ -123,7 +123,7 @@ def is_attribute_type(val: type):
)
return f
- funcs: set[Callable[[int], GuildAttributeType | None]] = set()
+ funcs: set[Callable[[int], GuildAttributeType| None]] = set()
if is_attribute_type(discord.Guild):
funcs.add(client.get_guild)
@@ -133,9 +133,10 @@ def is_attribute_type(val: type):
# parse if the type matches exactly.
funcs.add(guild.get_channel_or_thread)
if is_attribute_type(discord.abc.Messageable):
- funcs.add(client.get_channel)
+ # There is no attribute that gives a PrivateChannel
+ funcs.add(typing.cast(Callable[[int], MessageableGuildChannel|None], client.get_channel))
if is_attribute_type(discord.TextChannel):
- funcs.add(client.get_channel)
+ funcs.add(typing.cast(Callable[[int], discord.TextChannel|None], client.get_channel))
if is_attribute_type(discord.User):
funcs.add(client.get_user)
if is_attribute_type(discord.Role):
@@ -144,9 +145,9 @@ def is_attribute_type(val: type):
# I think it's safe to assume the stored value was an object of
# the correct type in the first place. As in, it's a
# CategoryChannel id, not a VoiceChannel id.
- funcs.add(client.get_channel)
+ funcs.add(typing.cast(Callable[[int], discord.CategoryChannel|None], client.get_channel))
if is_attribute_type(discord.channel.VoiceChannel):
- funcs.add(client.get_channel)
+ funcs.add(typing.cast(Callable[[int], discord.VoiceChannel|None], client.get_channel))
if is_attribute_type(discord.Emoji):
funcs.add(guild.get_emoji)
if is_attribute_type(int):
@@ -252,6 +253,8 @@ async def update_query(
class ServerSettings:
DATABASE_KEY = "server_settings"
+ attributes: ServerAttributes
+
@staticmethod
def get_original(
attribute: T | list[T]
diff --git a/extensions/staffaddons/cogs/staffaddons.py b/extensions/staffaddons/cogs/staffaddons.py
index 9f30a5f..06b0b14 100644
--- a/extensions/staffaddons/cogs/staffaddons.py
+++ b/extensions/staffaddons/cogs/staffaddons.py
@@ -1,8 +1,7 @@
from datetime import datetime, timedelta
# ^ for /delete_week_selfies (within 7 days), and /version startup
# time parsing to discord unix
-import requests
-# to fetch from GitHub and see Rina is running the latest version
+import aiohttp # to fetch from GitHub and see Rina is running the latest version
import discord
import discord.app_commands as app_commands
@@ -171,9 +170,8 @@ async def delete_week_selfies(self, itx: discord.Interaction[Bot]):
async def get_bot_version(self, itx: discord.Interaction[Bot]):
# get most recently pushed bot version
# noinspection LongLine
- latest_rina = requests.get(
- "https://raw.githubusercontent.com/TransPlace-Devs/uncute-rina/main/main.py" # noqa
- ).text
+ async with aiohttp.ClientSession() as client, client.get("https://raw.githubusercontent.com/TransPlace-Devs/uncute-rina/main/main.py") as response:
+ latest_rina = await response.text()
latest_version = (latest_rina
.split("BOT_VERSION = \"", 1)[1]
.split("\"", 1)[0])
@@ -187,11 +185,10 @@ async def get_bot_version(self, itx: discord.Interaction[Bot]):
f"(started at )",
ephemeral=False)
return
- else:
- await itx.response.send_message(
- f"Bot is currently running on v{itx.client.version} (latest)\n"
- f"(started at )",
- ephemeral=False)
+ await itx.response.send_message(
+ f"Bot is currently running on v{itx.client.version} (latest)\n"
+ f"(started at )",
+ ephemeral=False)
@app_commands.check(is_staff_check)
@app_commands.command(name="update", description="Update slash-commands")
diff --git a/extensions/tags/local_tag_list.py b/extensions/tags/local_tag_list.py
index e73bf6f..00b96a9 100644
--- a/extensions/tags/local_tag_list.py
+++ b/extensions/tags/local_tag_list.py
@@ -140,7 +140,8 @@ async def fetch_tags(
if data is None:
return {}
- local_tag_list[guild.id] = {name: DatabaseTagObject(**tag_data)
+ # This should be good, since it comes directly from the DB
+ local_tag_list[guild.id] = {name: DatabaseTagObject(**tag_data) # type: ignore[typeddict-item]
for name, tag_data in data.items()}
return local_tag_list[guild.id]
@@ -163,7 +164,8 @@ async def fetch_all_tags(
for guild_id, tag_objects in data.items():
tags = {}
for tag_name, tag_data in tag_objects.items():
- tag_object = DatabaseTagObject(**tag_data)
+ # This should be good, since it comes directly from the DB
+ tag_object = DatabaseTagObject(**tag_data) # type: ignore[typeddict-item]
tags[tag_name] = tag_object
local_tag_list[guild_id] = tags
diff --git a/extensions/watchlist/local_watchlist.py b/extensions/watchlist/local_watchlist.py
index d6c350e..2b194c8 100644
--- a/extensions/watchlist/local_watchlist.py
+++ b/extensions/watchlist/local_watchlist.py
@@ -213,7 +213,7 @@ async def refetch_watchlist_threads(
# store reference to dict into a shorter variable
- failed_fetches = []
+ failed_fetches: list[discord.Thread] = []
# iterate non-archived threads
for thread in watch_channel.threads:
await _import_thread_to_local_list(
diff --git a/main.py b/main.py
old mode 100644
new mode 100755
index 190088b..2701e92
--- a/main.py
+++ b/main.py
@@ -114,7 +114,8 @@ def get_token_data() -> tuple[
missing_tokens.append(key)
continue
tokens[key] = api_keys[key]
- tokens = ApiTokenDict(**tokens)
+ # We manually check totality later (if missing_tokens ...)
+ tokens = ApiTokenDict(**tokens) # type: ignore[typeddict-item]
except FileNotFoundError:
raise
except json.decoder.JSONDecodeError as ex:
diff --git a/requirements.txt b/requirements.txt
index 3af5c74..7e4187d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,6 +5,5 @@ pymongo>=4.14.0
pandas>=2.3.1
apscheduler>=3.11.0
matplotlib>=3.10.5
-requests>=2.32.3
pytest>=8.4.1
aiohttp>=3.12.15
diff --git a/resources/checks/permissions.py b/resources/checks/permissions.py
index 3465b5d..4d89c3f 100644
--- a/resources/checks/permissions.py
+++ b/resources/checks/permissions.py
@@ -26,8 +26,13 @@ def is_staff(
# No roles, no server, so no staff
return False
+ if itx.guild is None:
+ # No server, so no staff
+ return False
+
+ # The passed keys will only correspond to roles, so this cast is fine
staff_roles: list[discord.Role] = itx.client.get_guild_attribute(
- itx.guild, AttributeKeys.staff_roles, default=[])
+ itx.guild, AttributeKeys.staff_roles, default=[]) # type: ignore[assignment]
roles_set: set[discord.Role] = set(staff_roles)
return (
len(roles_set.intersection(member.roles)) > 0
@@ -52,8 +57,9 @@ def is_admin(
# No roles, no server, so no staff
return False
+ # The passed keys will only correspond to roles, so this cast is fine
admin_roles: list[discord.Role] = itx.client.get_guild_attribute(
- itx.guild, AttributeKeys.admin_roles, default=[])
+ itx.guild, AttributeKeys.admin_roles, default=[]) # type: ignore[assignment]
roles_set: set[discord.Role] = set(admin_roles)
return (
len(roles_set.intersection(member.roles)) > 0
diff --git a/resources/customs/bot.py b/resources/customs/bot.py
index a8d6b30..01e875d 100644
--- a/resources/customs/bot.py
+++ b/resources/customs/bot.py
@@ -8,7 +8,7 @@
# ^ for scheduling Reminders
from datetime import datetime
# ^ for startup and crash logging, and Reminders
-from typing import TYPE_CHECKING, TypeVar
+from typing import TYPE_CHECKING, TypeVar, cast
import motor.core as motorcore # for typing
from pymongo.database import Database as PyMongoDatabase
# ^ for MongoDB database typing
@@ -28,7 +28,7 @@
T = TypeVar('T')
G = TypeVar('G')
-
+type GetGuildAttributeReturn[T] = GuildAttributeType | T | None | list["GetGuildAttributeReturn"[T]]
class Bot(commands.Bot):
# bot uptime start, used in /version in cmd_staffaddons
@@ -125,12 +125,14 @@ def get_command_mention_with_args(
return command_mention
+ # Type checking with the class is effectively impossible, as the key dictates the type,
+ # and any union we would emit would then need to be explicitly ignored in a type check
def get_guild_attribute(
self,
guild: discord.Guild | int,
*args: str,
- default: T = None
- ) -> GuildAttributeType | T | list[GuildAttributeType | T]:
+ default: T | None = None
+ ) -> GetGuildAttributeReturn[T]:
"""
Get ServerSettings attributes for the given guild.
@@ -159,14 +161,14 @@ def get_guild_attribute(
# doesn't have any settings (perhaps the bot was recently
# added).
if len(args) > 1:
- return [default for _ in args]
+ return [cast(GetGuildAttributeReturn[T], default) for _ in args]
return default
attributes = self.server_settings[guild_id].attributes
- output: list[GuildAttributeType | T] = []
+ output: list[GetGuildAttributeReturn[T]] = []
- parent_server = attributes[AttributeKeys.parent_server] # type: ignore
+ parent_server = attributes[AttributeKeys.parent_server] # type: ignore[literal-required]
for arg in args:
if arg not in attributes:
@@ -243,4 +245,4 @@ def is_me(self, user_id: discord.Member | discord.User | int) -> bool:
user_id = user_id.id
# Could also use hasattr(user_id, "id") for a more generic approach...
# But this should work fine enough.
- return self.user.id == user_id
+ return self.user is not None and self.user.id == user_id
diff --git a/resources/utils/database.py b/resources/utils/database.py
index 390da21..8278d5a 100644
--- a/resources/utils/database.py
+++ b/resources/utils/database.py
@@ -12,5 +12,5 @@ def transform_bson(self, value):
return int(value)
-codec_options = CodecOptions(
+codec_options: CodecOptions = CodecOptions(
type_registry=TypeRegistry([Int64ToIntDecoder()]))
diff --git a/resources/utils/timeparser.py b/resources/utils/timeparser.py
index 65c3bf7..ca49ab9 100644
--- a/resources/utils/timeparser.py
+++ b/resources/utils/timeparser.py
@@ -152,7 +152,7 @@ def parse_date(
TimeParser.parse_time_string(time_string) # can raise ValueError
)
- timedict = {
+ timedict: dict[str, int|float] = {
"y": start_date.year,
"M": start_date.month,
"d": start_date.day,
@@ -235,21 +235,21 @@ def is_whole(time: float) -> bool:
raise ValueError("I don't think I can remind you in that long!")
# round everything down to the nearest whole number
- timedict = {i: int(timedict[i]) for i in timedict}
+ rounded_timedict = {i: int(j) for i, j in timedict.items()}
distance = datetime(
- timedict["y"],
- timedict["M"],
+ rounded_timedict["y"],
+ rounded_timedict["M"],
1, # add days using timedelta(days=d)
- timedict["h"],
- timedict["m"],
- timedict["s"],
- timedict["f"],
+ rounded_timedict["h"],
+ rounded_timedict["m"],
+ rounded_timedict["s"],
+ rounded_timedict["f"],
tzinfo=start_date.tzinfo
)
# You cant have >31 days in a month, but if overflow is given,
# then let this timedelta calculate the new months/years
- distance += timedelta(days=timedict["d"] - 1)
+ distance += timedelta(days=rounded_timedict["d"] - 1)
# ^ -1 cuz datetime.day has to start at 1 (first day of the month)
return distance
diff --git a/resources/utils/utils.py b/resources/utils/utils.py
index 6fd90b4..850816a 100644
--- a/resources/utils/utils.py
+++ b/resources/utils/utils.py
@@ -1,6 +1,6 @@
from __future__ import annotations
# ^ for logging, to show log time; and for parsetime
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING,cast
import discord
@@ -32,11 +32,11 @@ def get_mod_ticket_channel(
"""
if isinstance(guild_id, discord.Interaction):
guild_id = guild_id.guild.id
- ticket_channel: MessageableGuildChannel | None = \
+ ticket_channel = cast(MessageableGuildChannel | None, \
client.get_guild_attribute(
guild_id,
AttributeKeys.ticket_create_channel
- )
+ ))
return ticket_channel
@@ -69,8 +69,12 @@ async def log_to_guild(
:raise MissingAttributesCheckFailure: If no logging channel is
defined.
"""
+ # If we don't have a logging channel, then this ain't gonna work
+ if guild is None:
+ return False
+ # The given key should restrict us to messagable channels
log_channel: discord.abc.Messageable | None = client.get_guild_attribute(
- guild, AttributeKeys.log_channel)
+ guild, AttributeKeys.log_channel) # type: ignore[assignment]
if log_channel is None:
if ignore_dms and is_in_dms(guild):
return False
diff --git a/resources/views/generics.py b/resources/views/generics.py
index 793b72d..d7a5364 100644
--- a/resources/views/generics.py
+++ b/resources/views/generics.py
@@ -29,11 +29,15 @@ def create_simple_button(
:return: A button with the given properties.
"""
+ button: discord.ui.Button
if label_is_emoji:
button = discord.ui.Button(emoji=label, style=style, disabled=disabled)
else:
button = discord.ui.Button(label=label, style=style, disabled=disabled)
- button.callback = callback
+ # evil kludge that makes mypy angery
+ #
+ # We need to assign the method, and we're not getting a callback for something that isn't a bot
+ button.callback = callback # type: ignore[method-assign, assignment]
return button
diff --git a/ruff.toml b/ruff.toml
new file mode 100644
index 0000000..79e1fb7
--- /dev/null
+++ b/ruff.toml
@@ -0,0 +1,69 @@
+exclude = [".venv", "unit_tests"]
+
+[lint]
+select = [
+ "ANN", # Type annotations
+ "ASYNC", # Async bugs
+ "S", # Security bugs
+ "A", # Built-in shadowing bugs
+ "C4", # Generator expression lints
+ "EXE", # Checks that executable files are set up properly
+ "FIX", # Grabs TODOs
+ "ICN", # Catches weird imports
+ "LOG", # Catches logging issues
+ "SLF", # Catches reading private members
+ # Catches redundant code
+ #"SIM101", "SIM102", "SIM103", "SIM110", "SIM113", "SIM114", "SIM116", "SIM117", "SIM118", "SIM2", "SIM401", "SIM9",
+ "TID", # More weird imports
+ "PTH", # Pathlib
+ "NPY", # NumPy
+ "PD", # Pandas
+ "PERF", # Performance lints
+ "E", "W", # Standard warnings and errors
+ "F", # Generic non-style mistakes
+ "PLE", # Pylint errors
+ "PLW", # Pylint warnings
+ # Ruff-specific
+ "RUF",
+ # "RUF006", # asyncio tasks can be GC'd and killed
+ # "RUF013", # Explicit Optional
+ # "RUF015", # Turning something into a list to get the first element is slow
+ # "RUF016", # Invalid index type
+ # "RUF017", # Bizarre list merge
+ # "RUF018", "RUF030", "RUF040", # Assert issues
+ # "RUF019", # Bizarre dict element access
+ # "RUF021", # and/or bracketing
+ # "RUF024", # Bad dict construction
+ # "RUF026", # Defaultdict misuse
+ # "RUF027", # Something looks like an f-string, but is missing the `f`
+ # "RUF028", # Invalid supression comments
+ # "RUF043", # pytest issues
+]
+
+ignore = [
+ # Things we should probably do
+ "ANN", # Type annotations would take wayyy too long to do properly
+ "S101", # We should not use asserts, as some environments disable them
+ "C4", # We should probably move to list comprehensions, but that's a lot of work
+ "RUF013", # Optional types should be properly marked
+ "PGH004", "RUF100", # Blanket disabling lints isn't great
+
+ # Style stuff that's bad, but won't cause issues
+ "PTH", # pathlib still technically works
+ "RUF022", "RUF023", "I001", # Sorted inputs are nice, but not *needed*
+ "PERF", # ._.
+ "RUF059", # We are explicitly allowing unused variables
+
+ # Ignoring these more permanently
+ "S311", # We don't need a CSPRNG anywhere, right?
+ "SIM300", "SIM108", "SIM105", # Allow yoda conditions, choosing not to use ternary operator, and explicit `catch: pass`
+ "E501", # Allow long lines
+ "PLW2901", # Allow reassigning in loops
+ "PLW0127", # Allow self-assingment to indicate type
+ "PLW0603", # We seem to be choosing to use globals
+ "FIX002", # Too much TODO
+ "SIM101", # We can use tuples in isInstance, but we don't need to
+ "RUF005", # [*a, *b] is apparently faster than a + b, but it's not *that* important
+ "RUF010", # Explicit f-string conversion is fine and readable
+ "RUF001", "RUF003", # Weird tests for characters that look similar
+]
diff --git a/setup.cfg b/setup.cfg
index a071bb4..ace5efb 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,17 +1,18 @@
[mypy]
-disable_error_code = import-untyped
+disable_error_code=import-untyped,no-redef
+implicit_optional=True
+allow_redefinition_new=True
+local_partial_types=True
+allow_redefinition=True
[flake8]
-max-line-length = 79
-exclude =
- venv
- rina_docs
+max-line-length=79
+exclude=venv,rina_docs
[tool:pytest]
-pythonpath = .
-filterwarnings =
- ignore::DeprecationWarning:discord.player
+pythonpath=.
+filterwarnings=ignore::DeprecationWarning:discord.player
; Only necessary in python 3.12. discord.py imports `audioop-lts` in python 3.13 resolving this DeprecationWarning.
[coverage:run]
-omit = */unit_tests/*,*/__init__.py,*/module.py
+omit=*/unit_tests/*,*/__init__.py,*/module.py
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..bac24a4
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,6 @@
+#!/usr/bin/env python
+
+import setuptools
+
+if __name__ == "__main__":
+ setuptools.setup()
diff --git a/tmp.py b/tmp.py
new file mode 100644
index 0000000..6208769
--- /dev/null
+++ b/tmp.py
@@ -0,0 +1,14 @@
+from typing import TypeAliasType
+import typing
+from extensions.settings.objects.server_settings import get_attribute_type
+
+attribute_type, _ = get_attribute_type("attribute_key")
+
+def is_attribute_type(val: type):
+ f = any(
+ val in typing.get_args(i.__value__)
+ if type(i) is TypeAliasType
+ else val is i
+ for i in attribute_type
+ )
+ return f