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