Add flake8 docstring plugin, formatting, create base class for embeds & models

pull/119/head
stijndcl 2022-07-13 22:54:16 +02:00
parent 8d6dbe1c94
commit b9c5c6ab10
24 changed files with 241 additions and 16 deletions

25
.flake8
View File

@ -1,4 +1,5 @@
[flake8] [flake8]
# Don't lint non-Python files
exclude = exclude =
.git, .git,
.github, .github,
@ -9,10 +10,30 @@ exclude =
htmlcov, htmlcov,
tests, tests,
venv venv
ignore=E203 # Disable rules that we don't care about (or conflict with others)
extend-ignore =
# Missing docstring in public module
D100, D104,
# Missing docstring in magic method
D105,
# Missing docstring in __init__
D107,
# First line of docstrings should end with a period
D400,
# First line of docstrings should be in imperative mood
D401,
# Whitespace before ":"
E203,
# Don't require docstrings when overriding a method,
# the base method should have a docstring but the rest not
ignore-decorator=overrides
max-line-length = 120 max-line-length = 120
# Disable some rules for entire files
per-file-ignores = per-file-ignores =
# Missing __all__, main isn't supposed to be imported # Missing __all__, main isn't supposed to be imported
main.py: DALL000, main.py: DALL000,
# Missing __all__, Cogs aren't modules # Missing __all__, Cogs aren't modules
./didier/cogs/*: DALL000 ./didier/cogs/*: DALL000,
# All of the colours methods are just oneliners to create a colour,
# there's no point adding docstrings (function names are enough)
./didier/utils/discord/colours.py: D103

View File

@ -28,6 +28,7 @@ async def create_new_announcement(
async def remove_old_announcements(session: AsyncSession): async def remove_old_announcements(session: AsyncSession):
"""Delete all announcements that are > 8 days old """Delete all announcements that are > 8 days old
The RSS feed only goes back 7 days, so all of these old announcements never have to The RSS feed only goes back 7 days, so all of these old announcements never have to
be checked again when checking if an announcement is fresh or not. be checked again when checking if an announcement is fresh or not.
""" """

View File

@ -12,7 +12,8 @@ __all__ = [
async def get_or_add(session: AsyncSession, user_id: int) -> User: async def get_or_add(session: AsyncSession, user_id: int) -> User:
"""Get a user's profile """Get a user's profile
If it doesn't exist yet, create it (along with all linked datastructures).
If it doesn't exist yet, create it (along with all linked datastructures)
""" """
statement = select(User).where(User.user_id == user_id) statement = select(User).where(User.user_id == user_id)
user: Optional[User] = (await session.execute(statement)).scalar_one_or_none() user: Optional[User] = (await session.execute(statement)).scalar_one_or_none()

View File

@ -2,13 +2,15 @@ from typing import List, Mapping, Optional
import discord import discord
from discord.ext import commands from discord.ext import commands
from overrides import overrides
from didier import Didier from didier import Didier
class CustomHelpCommand(commands.MinimalHelpCommand): class CustomHelpCommand(commands.MinimalHelpCommand):
"""Customised Help command to override the default implementation """Customised Help command to override the default implementation
The default is ugly as hell
The default is ugly as hell so we do some fiddling with it
""" """
def _help_embed_base(self, title: str) -> discord.Embed: def _help_embed_base(self, title: str) -> discord.Embed:
@ -30,6 +32,7 @@ class CustomHelpCommand(commands.MinimalHelpCommand):
return list(sorted(filtered_cogs, key=lambda cog: cog.qualified_name)) return list(sorted(filtered_cogs, key=lambda cog: cog.qualified_name))
@overrides
async def send_bot_help(self, mapping: Mapping[Optional[commands.Cog], List[commands.Command]], /): async def send_bot_help(self, mapping: Mapping[Optional[commands.Cog], List[commands.Command]], /):
embed = self._help_embed_base("Categorieën") embed = self._help_embed_base("Categorieën")
filtered_cogs = await self._filter_cogs(list(mapping.keys())) filtered_cogs = await self._filter_cogs(list(mapping.keys()))

View File

@ -0,0 +1,23 @@
from discord.ext import commands
from didier import Didier
from didier.data.apis import urban_dictionary
class Other(commands.Cog):
"""Cog for commands that don't really belong anywhere else"""
client: Didier
def __init__(self, client: Didier):
self.client = client
@commands.command(name="Define", aliases=["Ud", "Urban"], usage="[Woord]")
async def define(self, ctx: commands.Context, *, query: str):
"""Look up the definition of a word on the Urban Dictionary"""
definitions = urban_dictionary.lookup(self.client.http_session, query)
async def setup(client: Didier):
"""Load the cog"""
await client.add_cog(Other(client))

View File

@ -25,8 +25,9 @@ class Owner(commands.Cog):
self.client = client self.client = client
async def cog_check(self, ctx: commands.Context) -> bool: async def cog_check(self, ctx: commands.Context) -> bool:
"""Global check for every command in this cog, so we don't have to add """Global check for every command in this cog
is_owner() to every single command separately
This means that we don't have to add is_owner() to every single command separately
""" """
# pylint: disable=W0236 # Pylint thinks this can't be async, but it can # pylint: disable=W0236 # Pylint thinks this can't be async, but it can
return await self.client.is_owner(ctx.author) return await self.client.is_owner(ctx.author)

View File

View File

@ -0,0 +1,14 @@
from aiohttp import ClientSession
from didier.data.embeds.urban_dictionary import Definition
__all__ = ["lookup"]
async def lookup(http_session: ClientSession, query: str) -> list[Definition]:
"""Fetch the Urban Dictionary definitions for a given word"""
url = "https://api.urbandictionary.com/v0/define"
async with http_session.get(url, params={"term": query}) as response:
response_json = await response.json()
return list(map(Definition.parse_obj, response_json["list"]))

View File

@ -0,0 +1,22 @@
from abc import ABC, abstractmethod
import discord
from pydantic import BaseModel
__all__ = [
"EmbedBaseModel",
"EmbedPydantic",
]
class EmbedBaseModel(ABC):
"""Abstract base class for a model that can be turned into a Discord embed"""
@abstractmethod
def to_embed(self) -> discord.Embed:
"""Turn this model into a Discord embed"""
raise NotImplementedError
class EmbedPydantic(ABC, EmbedBaseModel, BaseModel):
"""Pydantic version of EmbedModel"""

View File

@ -14,6 +14,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
import settings import settings
from database.crud import ufora_announcements as crud from database.crud import ufora_announcements as crud
from database.models import UforaCourse from database.models import UforaCourse
from didier.data.embeds.base import EmbedBaseModel
from didier.utils.types.datetime import int_to_weekday from didier.utils.types.datetime import int_to_weekday
from didier.utils.types.string import leading from didier.utils.types.string import leading
@ -25,7 +26,7 @@ __all__ = [
@dataclass @dataclass
class UforaNotification: class UforaNotification(EmbedBaseModel):
"""A single notification from Ufora""" """A single notification from Ufora"""
content: dict content: dict

View File

@ -0,0 +1,24 @@
from datetime import datetime
import discord
from overrides import overrides
from didier.data.embeds.base import EmbedPydantic
__all__ = ["Definition"]
class Definition(EmbedPydantic):
"""A definition from the Urban Dictionary"""
word: str
definition: str
permalink: str
author: str
thumbs_up: int
thumbs_down: int
written_on: datetime
@overrides
def to_embed(self) -> discord.Embed:
embed = discord.Embed()

View File

@ -2,6 +2,7 @@ import traceback
import typing import typing
import discord import discord
from overrides import overrides
from database.crud.custom_commands import create_command, edit_command from database.crud.custom_commands import create_command, edit_command
from didier import Didier from didier import Didier
@ -24,12 +25,14 @@ class CreateCustomCommand(discord.ui.Modal, title="Create Custom Command"):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.client = client self.client = client
@overrides
async def on_submit(self, interaction: discord.Interaction): async def on_submit(self, interaction: discord.Interaction):
async with self.client.db_session as session: async with self.client.db_session as session:
command = await create_command(session, str(self.name.value), str(self.response.value)) command = await create_command(session, str(self.name.value), str(self.response.value))
await interaction.response.send_message(f"Successfully created ``{command.name}``.", ephemeral=True) await interaction.response.send_message(f"Successfully created ``{command.name}``.", ephemeral=True)
@overrides
async def on_error(self, interaction: discord.Interaction, error: Exception): # type: ignore async def on_error(self, interaction: discord.Interaction, error: Exception): # type: ignore
await interaction.response.send_message("Something went wrong.", ephemeral=True) await interaction.response.send_message("Something went wrong.", ephemeral=True)
traceback.print_tb(error.__traceback__) traceback.print_tb(error.__traceback__)
@ -37,7 +40,8 @@ class CreateCustomCommand(discord.ui.Modal, title="Create Custom Command"):
class EditCustomCommand(discord.ui.Modal, title="Edit Custom Command"): class EditCustomCommand(discord.ui.Modal, title="Edit Custom Command"):
"""Modal to edit an existing custom command """Modal to edit an existing custom command
Fills in the current values as defaults
Fills in the current values as defaults for QOL
""" """
name: discord.ui.TextInput name: discord.ui.TextInput
@ -59,6 +63,7 @@ class EditCustomCommand(discord.ui.Modal, title="Edit Custom Command"):
) )
) )
@overrides
async def on_submit(self, interaction: discord.Interaction): async def on_submit(self, interaction: discord.Interaction):
name_field = typing.cast(discord.ui.TextInput, self.children[0]) name_field = typing.cast(discord.ui.TextInput, self.children[0])
response_field = typing.cast(discord.ui.TextInput, self.children[1]) response_field = typing.cast(discord.ui.TextInput, self.children[1])
@ -68,6 +73,7 @@ class EditCustomCommand(discord.ui.Modal, title="Edit Custom Command"):
await interaction.response.send_message(f"Successfully edited ``{self.original_name}``.", ephemeral=True) await interaction.response.send_message(f"Successfully edited ``{self.original_name}``.", ephemeral=True)
@overrides
async def on_error(self, interaction: discord.Interaction, error: Exception): # type: ignore async def on_error(self, interaction: discord.Interaction, error: Exception): # type: ignore
await interaction.response.send_message("Something went wrong.", ephemeral=True) await interaction.response.send_message("Something went wrong.", ephemeral=True)
traceback.print_tb(error.__traceback__) traceback.print_tb(error.__traceback__)

View File

@ -42,7 +42,10 @@ class Didier(commands.Bot):
return DBSession() return DBSession()
async def setup_hook(self) -> None: async def setup_hook(self) -> None:
"""Hook called once the bot is initialised""" """Do some initial setup
This hook is called once the bot is initialised
"""
# Load extensions # Load extensions
await self._load_initial_extensions() await self._load_initial_extensions()
await self._load_directory_extensions("didier/cogs") await self._load_directory_extensions("didier/cogs")
@ -113,7 +116,9 @@ class Didier(commands.Bot):
async def _try_invoke_custom_command(self, message: discord.Message) -> bool: async def _try_invoke_custom_command(self, message: discord.Message) -> bool:
"""Check if the message tries to invoke a custom command """Check if the message tries to invoke a custom command
If it does, send the reply associated with it If it does, send the reply associated with it
Returns a boolean indicating if a message invoked a command or not
""" """
# Doesn't start with the custom command prefix # Doesn't start with the custom command prefix
if not message.content.startswith(settings.DISCORD_CUSTOM_COMMAND_PREFIX): if not message.content.startswith(settings.DISCORD_CUSTOM_COMMAND_PREFIX):

View File

View File

@ -0,0 +1,12 @@
__all__ = ["MissingEnvironmentVariable"]
class MissingEnvironmentVariable(RuntimeError):
"""Exception raised when an environment variable is missing
These are not necessarily checked on startup, because they may be unused
during a given test run, and random unrelated crashes would be annoying
"""
def __init__(self, variable: str):
super().__init__(f"Missing environment variable: {variable}")

View File

@ -0,0 +1,7 @@
import discord
__all__ = ["urban_dictionary_green"]
def urban_dictionary_green() -> discord.Colour:
return discord.Colour.from_rgb(220, 255, 0)

View File

@ -6,6 +6,7 @@ __all__ = ["abbreviated_number"]
def abbreviated_number(argument: str) -> Union[str, int]: def abbreviated_number(argument: str) -> Union[str, int]:
"""Custom converter to allow numbers to be abbreviated """Custom converter to allow numbers to be abbreviated
Examples: Examples:
515k 515k
4m 4m

View File

@ -10,6 +10,7 @@ __all__ = ["get_prefix"]
def get_prefix(client: commands.Bot, message: Message) -> str: def get_prefix(client: commands.Bot, message: Message) -> str:
"""Match a prefix against a message """Match a prefix against a message
This is done dynamically to allow variable amounts of whitespace, This is done dynamically to allow variable amounts of whitespace,
and through regexes to allow case-insensitivity among other things. and through regexes to allow case-insensitivity among other things.
""" """

View File

@ -6,6 +6,7 @@ __all__ = ["leading", "pluralize"]
def leading(character: str, string: str, target_length: Optional[int] = 2) -> str: def leading(character: str, string: str, target_length: Optional[int] = 2) -> str:
"""Add a leading [character] to [string] to make it length [target_length] """Add a leading [character] to [string] to make it length [target_length]
Pass None to target length to always do it (once), no matter the length Pass None to target length to always do it (once), no matter the length
""" """
# Cast to string just in case # Cast to string just in case

View File

@ -22,6 +22,7 @@ profile = "black"
[tool.mypy] [tool.mypy]
plugins = [ plugins = [
"pydantic.mypy",
"sqlalchemy.ext.mypy.plugin" "sqlalchemy.ext.mypy.plugin"
] ]
[[tool.mypy.overrides]] [[tool.mypy.overrides]]

View File

@ -11,8 +11,8 @@ types-pytz==2021.3.8
# Flake8 + plugins # Flake8 + plugins
flake8==4.0.1 flake8==4.0.1
flake8-bandit==3.0.0 flake8-bandit==3.0.0
flake8-black==0.3.3
flake8-bugbear==22.7.1 flake8-bugbear==22.7.1
flake8-docstrings==1.6.0
flake8-dunder-all==0.2.1 flake8-dunder-all==0.2.1
flake8-eradicate==1.2.1 flake8-eradicate==1.2.1
flake8-isort==4.1.1 flake8-isort==4.1.1

View File

@ -6,5 +6,8 @@ git+https://github.com/Rapptz/discord.py
environs==9.5.0 environs==9.5.0
feedparser==6.0.10 feedparser==6.0.10
markdownify==0.11.2 markdownify==0.11.2
overrides==6.1.0
pydantic==1.9.1
python-dateutil==2.8.2
pytz==2022.1 pytz==2022.1
sqlalchemy[asyncio]==1.4.37 sqlalchemy[asyncio]==1.4.37

View File

@ -6,6 +6,26 @@ from environs import Env
env = Env() env = Env()
env.read_env() env.read_env()
__all__ = [
"SANDBOX",
"LOGFILE",
"DB_NAME",
"DB_USERNAME",
"DB_PASSWORD",
"DB_HOST",
"DB_PORT",
"DISCORD_TOKEN",
"DISCORD_READY_MESSAGE",
"DISCORD_STATUS_MESSAGE",
"DISCORD_TEST_GUILDS",
"DISCORD_BOOS_REACT",
"DISCORD_CUSTOM_COMMAND_PREFIX",
"UFORA_ANNOUNCEMENTS_CHANNEL",
"UFORA_RSS_TOKEN",
"URBAN_DICTIONARY_TOKEN",
]
"""General config""" """General config"""
SANDBOX: bool = env.bool("SANDBOX", True) SANDBOX: bool = env.bool("SANDBOX", True)
LOGFILE: str = env.str("LOGFILE", "didier.log") LOGFILE: str = env.str("LOGFILE", "didier.log")
@ -18,13 +38,14 @@ DB_HOST: str = env.str("DB_HOST", "localhost")
DB_PORT: int = env.int("DB_PORT", "5432") DB_PORT: int = env.int("DB_PORT", "5432")
"""Discord""" """Discord"""
DISCORD_TOKEN: str = env.str("DISC_TOKEN") DISCORD_TOKEN: str = env.str("DISCORD_TOKEN")
DISCORD_READY_MESSAGE: str = env.str("DISC_READY_MESSAGE", "I'M READY I'M READY I'M READY") DISCORD_READY_MESSAGE: str = env.str("DISCORD_READY_MESSAGE", "I'M READY I'M READY I'M READY")
DISCORD_STATUS_MESSAGE: str = env.str("DISC_STATUS_MESSAGE", "with your Didier Dinks.") DISCORD_STATUS_MESSAGE: str = env.str("DISCORD_STATUS_MESSAGE", "with your Didier Dinks.")
DISCORD_TEST_GUILDS: list[int] = env.list("DISC_TEST_GUILDS", [], subcast=int) DISCORD_TEST_GUILDS: list[int] = env.list("DISCORD_TEST_GUILDS", [], subcast=int)
DISCORD_BOOS_REACT: str = env.str("DISC_BOOS_REACT", "<:boos:629603785840263179>") DISCORD_BOOS_REACT: str = env.str("DISCORD_BOOS_REACT", "<:boos:629603785840263179>")
DISCORD_CUSTOM_COMMAND_PREFIX: str = env.str("DISC_CUSTOM_COMMAND_PREFIX", "?") DISCORD_CUSTOM_COMMAND_PREFIX: str = env.str("DISCORD_CUSTOM_COMMAND_PREFIX", "?")
UFORA_ANNOUNCEMENTS_CHANNEL: Optional[int] = env.int("UFORA_ANNOUNCEMENTS_CHANNEL", None) UFORA_ANNOUNCEMENTS_CHANNEL: Optional[int] = env.int("UFORA_ANNOUNCEMENTS_CHANNEL", None)
"""API Keys""" """API Keys"""
UFORA_RSS_TOKEN: Optional[str] = env.str("UFORA_RSS_TOKEN", None) UFORA_RSS_TOKEN: Optional[str] = env.str("UFORA_RSS_TOKEN", None)
URBAN_DICTIONARY_TOKEN: Optional[str] = env.str("URBAN_DICTIONARY_TOKEN", None)

View File

@ -0,0 +1,56 @@
{
"list": [
{
"definition": "When you fall [asleep] [tweeting] about [nonsensical] things",
"permalink": "http://cofveve.urbanup.com/11642742",
"thumbs_up": 170,
"sound_urls": [],
"author": "Bonafidé",
"word": "cofveve",
"defid": 11642742,
"current_vote": "",
"written_on": "2017-06-03T00:32:10.987Z",
"example": "[Despite] [the negative] [press] cofveve",
"thumbs_down": 30
},
{
"definition": "when you want to type [conference] and your hands are too small to reach [the keys] (someone else's [brill] def)",
"permalink": "http://cofveve.urbanup.com/11662158",
"thumbs_up": 69,
"sound_urls": [],
"author": "blacklist2017",
"word": "cofveve",
"defid": 11662158,
"current_vote": "",
"written_on": "2017-06-07T00:44:36.793Z",
"example": "\"[i want] to [shut down] this [press] cofveve...",
"thumbs_down": 16
},
{
"definition": "[Bullshit]",
"permalink": "http://cofveve.urbanup.com/12386593",
"thumbs_up": 5,
"sound_urls": [],
"author": "FreedomTodd",
"word": "Cofveve",
"defid": 12386593,
"current_vote": "",
"written_on": "2018-01-06T17:52:59.822Z",
"example": "[Im] [full of] [cofveve]",
"thumbs_down": 6
},
{
"definition": "When you dip [an apple] in a chocolate [Starbucks] [frappe]",
"permalink": "http://razzle-cofveve.urbanup.com/12006856",
"thumbs_up": 0,
"sound_urls": [],
"author": "harry potter theme song si ",
"word": "razzle cofveve",
"defid": 12006856,
"current_vote": "",
"written_on": "2017-09-30T03:00:54.233Z",
"example": "[My friend] loves [apples] and [Starbucks], so the razzle cofveve was perfect!",
"thumbs_down": 0
}
]
}