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]
# Don't lint non-Python files
exclude =
.git,
.github,
@ -9,10 +10,30 @@ exclude =
htmlcov,
tests,
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
# Disable some rules for entire files
per-file-ignores =
# Missing __all__, main isn't supposed to be imported
main.py: DALL000,
# 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):
"""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
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:
"""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)
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
from discord.ext import commands
from overrides import overrides
from didier import Didier
class CustomHelpCommand(commands.MinimalHelpCommand):
"""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:
@ -30,6 +32,7 @@ class CustomHelpCommand(commands.MinimalHelpCommand):
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]], /):
embed = self._help_embed_base("Categorieën")
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
async def cog_check(self, ctx: commands.Context) -> bool:
"""Global check for every command in this cog, so we don't have to add
is_owner() to every single command separately
"""Global check for every command in this cog
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
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
from database.crud import ufora_announcements as crud
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.string import leading
@ -25,7 +26,7 @@ __all__ = [
@dataclass
class UforaNotification:
class UforaNotification(EmbedBaseModel):
"""A single notification from Ufora"""
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 discord
from overrides import overrides
from database.crud.custom_commands import create_command, edit_command
from didier import Didier
@ -24,12 +25,14 @@ class CreateCustomCommand(discord.ui.Modal, title="Create Custom Command"):
super().__init__(*args, **kwargs)
self.client = client
@overrides
async def on_submit(self, interaction: discord.Interaction):
async with self.client.db_session as session:
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)
@overrides
async def on_error(self, interaction: discord.Interaction, error: Exception): # type: ignore
await interaction.response.send_message("Something went wrong.", ephemeral=True)
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"):
"""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
@ -59,6 +63,7 @@ class EditCustomCommand(discord.ui.Modal, title="Edit Custom Command"):
)
)
@overrides
async def on_submit(self, interaction: discord.Interaction):
name_field = typing.cast(discord.ui.TextInput, self.children[0])
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)
@overrides
async def on_error(self, interaction: discord.Interaction, error: Exception): # type: ignore
await interaction.response.send_message("Something went wrong.", ephemeral=True)
traceback.print_tb(error.__traceback__)

View File

@ -42,7 +42,10 @@ class Didier(commands.Bot):
return DBSession()
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
await self._load_initial_extensions()
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:
"""Check if the message tries to invoke a custom command
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
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]:
"""Custom converter to allow numbers to be abbreviated
Examples:
515k
4m

View File

@ -10,6 +10,7 @@ __all__ = ["get_prefix"]
def get_prefix(client: commands.Bot, message: Message) -> str:
"""Match a prefix against a message
This is done dynamically to allow variable amounts of whitespace,
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:
"""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
"""
# Cast to string just in case

View File

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

View File

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

View File

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

View File

@ -6,6 +6,26 @@ from environs import Env
env = 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"""
SANDBOX: bool = env.bool("SANDBOX", True)
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")
"""Discord"""
DISCORD_TOKEN: str = env.str("DISC_TOKEN")
DISCORD_READY_MESSAGE: str = env.str("DISC_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_TEST_GUILDS: list[int] = env.list("DISC_TEST_GUILDS", [], subcast=int)
DISCORD_BOOS_REACT: str = env.str("DISC_BOOS_REACT", "<:boos:629603785840263179>")
DISCORD_CUSTOM_COMMAND_PREFIX: str = env.str("DISC_CUSTOM_COMMAND_PREFIX", "?")
DISCORD_TOKEN: str = env.str("DISCORD_TOKEN")
DISCORD_READY_MESSAGE: str = env.str("DISCORD_READY_MESSAGE", "I'M READY I'M READY I'M READY")
DISCORD_STATUS_MESSAGE: str = env.str("DISCORD_STATUS_MESSAGE", "with your Didier Dinks.")
DISCORD_TEST_GUILDS: list[int] = env.list("DISCORD_TEST_GUILDS", [], subcast=int)
DISCORD_BOOS_REACT: str = env.str("DISCORD_BOOS_REACT", "<:boos:629603785840263179>")
DISCORD_CUSTOM_COMMAND_PREFIX: str = env.str("DISCORD_CUSTOM_COMMAND_PREFIX", "?")
UFORA_ANNOUNCEMENTS_CHANNEL: Optional[int] = env.int("UFORA_ANNOUNCEMENTS_CHANNEL", None)
"""API Keys"""
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
}
]
}