From f799d03756e4dd61c9407baa552409adc482e212 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Sun, 18 Aug 2019 10:05:54 +0800 Subject: Add utils function to replace multiple words in a given string --- bot/utils/__init__.py | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) (limited to 'bot/utils') diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py index ef18a1b9..ad019357 100644 --- a/bot/utils/__init__.py +++ b/bot/utils/__init__.py @@ -1,4 +1,6 @@ import asyncio +import re +import string from typing import List import discord @@ -77,3 +79,57 @@ async def disambiguate( return entries[index - 1] except IndexError: raise BadArgument('Invalid choice.') + + +def replace_many( + sentence: str, replacements: dict, *, ignore_case: bool = False, match_case: bool = False +) -> str: + """ + Replaces multiple substrings in a string given a mapping of strings. + + By default replaces long strings before short strings, and lowercase before uppercase. + Example: + var = replace_many("This is a sentence", {"is": "was", "This": "That"}) + assert var == "That was a sentence" + + If `ignore_case` is given, does a case insensitive match. + Example: + var = replace_many("THIS is a sentence", {"IS": "was", "tHiS": "That"}, ignore_case=True) + assert var == "That was a sentence" + + If `match_case` is given, matches the case of the replacement with the replaced word. + Example: + var = replace_many( + "This IS a sentence", {"is": "was", "this": "that"}, ignore_case=True, match_case=True + ) + assert var == "That WAS a sentence" + """ + if ignore_case: + replacements = dict( + (word.lower(), replacement) for word, replacement in replacements.items() + ) + + words_to_replace = sorted(replacements, key=lambda s: (-len(s), s)) + + # Join and compile words to replace into a regex + pattern = "|".join(re.escape(word) for word in words_to_replace) + regex = re.compile(pattern, re.I if ignore_case else 0) + + def _repl(match): + """Returns replacement depending on `ignore_case` and `match_case`""" + word = match.group(0) + replacement = replacements[word.lower() if ignore_case else word] + + if not match_case: + return replacement + + # Clean punctuation from word so string methods work + cleaned_word = word.translate(str.maketrans('', '', string.punctuation)) + if cleaned_word.isupper(): + return replacement.upper() + elif cleaned_word[0].isupper(): + return replacement.capitalize() + else: + return replacement.lower() + + return regex.sub(_repl, sentence) -- cgit v1.2.3 From 4b18d7e430d5cea16406c65349718f72919c01c3 Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Mon, 9 Sep 2019 16:37:13 -0400 Subject: Lint remaining files hacktoberstats cog handled in separate PR --- bot/__init__.py | 2 +- bot/decorators.py | 6 +++--- bot/seasons/pride/pride_anthem.py | 6 +++--- bot/seasons/pride/pride_avatar.py | 12 +++++------ bot/seasons/valentines/be_my_valentine.py | 34 +++++++++++++++++------------- bot/seasons/valentines/lovecalculator.py | 6 +++--- bot/seasons/valentines/movie_generator.py | 6 +++--- bot/seasons/valentines/myvalenstate.py | 8 +++---- bot/seasons/valentines/pickuplines.py | 6 +++--- bot/seasons/valentines/savethedate.py | 6 +++--- bot/seasons/valentines/valentine_zodiac.py | 8 +++---- bot/seasons/valentines/whoisvalentine.py | 8 +++---- bot/utils/__init__.py | 4 ++-- bot/utils/halloween/spookifications.py | 8 +++---- 14 files changed, 62 insertions(+), 58 deletions(-) (limited to 'bot/utils') diff --git a/bot/__init__.py b/bot/__init__.py index 8950423f..4729e50c 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -13,7 +13,7 @@ logging.TRACE = 5 logging.addLevelName(logging.TRACE, "TRACE") -def monkeypatch_trace(self, msg: str, *args, **kwargs) -> None: +def monkeypatch_trace(self: logging.Logger, msg: str, *args, **kwargs) -> None: """ Log 'msg % args' with severity 'TRACE'. diff --git a/bot/decorators.py b/bot/decorators.py index e12c3f34..dbaad4a2 100644 --- a/bot/decorators.py +++ b/bot/decorators.py @@ -117,7 +117,7 @@ def override_in_channel(func: typing.Callable) -> typing.Callable: return func -def locked(): +def locked() -> typing.Union[typing.Callable, None]: """ Allows the user to only run one instance of the decorated command at a time. @@ -125,11 +125,11 @@ def locked(): This decorator has to go before (below) the `command` decorator. """ - def wrap(func: typing.Callable): + def wrap(func: typing.Callable) -> typing.Union[typing.Callable, None]: func.__locks = WeakValueDictionary() @wraps(func) - async def inner(self, ctx: Context, *args, **kwargs): + async def inner(self: typing.Callable, ctx: Context, *args, **kwargs) -> typing.Union[typing.Callable, None]: lock = func.__locks.setdefault(ctx.author.id, Lock()) if lock.locked(): embed = Embed() diff --git a/bot/seasons/pride/pride_anthem.py b/bot/seasons/pride/pride_anthem.py index f226f4bb..b0c6d34e 100644 --- a/bot/seasons/pride/pride_anthem.py +++ b/bot/seasons/pride/pride_anthem.py @@ -11,7 +11,7 @@ log = logging.getLogger(__name__) class PrideAnthem(commands.Cog): """Embed a random youtube video for a gay anthem!""" - def __init__(self, bot): + def __init__(self, bot: commands.Bot): self.bot = bot self.anthems = self.load_vids() @@ -39,7 +39,7 @@ class PrideAnthem(commands.Cog): return anthems @commands.command(name="prideanthem", aliases=["anthem", "pridesong"]) - async def prideanthem(self, ctx, genre: str = None): + async def prideanthem(self, ctx: commands.Context, genre: str = None) -> None: """ Sends a message with a video of a random pride anthem. @@ -52,7 +52,7 @@ class PrideAnthem(commands.Cog): await ctx.send("I couldn't find a video, sorry!") -def setup(bot): +def setup(bot: commands.Bot) -> None: """Cog loader for pride anthem.""" bot.add_cog(PrideAnthem(bot)) log.info("Pride anthems cog loaded!") diff --git a/bot/seasons/pride/pride_avatar.py b/bot/seasons/pride/pride_avatar.py index a5b38d20..85e49d5c 100644 --- a/bot/seasons/pride/pride_avatar.py +++ b/bot/seasons/pride/pride_avatar.py @@ -56,11 +56,11 @@ OPTIONS = { class PrideAvatar(commands.Cog): """Put an LGBT spin on your avatar!""" - def __init__(self, bot): + def __init__(self, bot: commands.Bot): self.bot = bot @staticmethod - def crop_avatar(avatar): + def crop_avatar(avatar: Image) -> Image: """This crops the avatar into a circle.""" mask = Image.new("L", avatar.size, 0) draw = ImageDraw.Draw(mask) @@ -69,7 +69,7 @@ class PrideAvatar(commands.Cog): return avatar @staticmethod - def crop_ring(ring, px): + def crop_ring(ring: Image, px: int) -> Image: """This crops the ring into a circle.""" mask = Image.new("L", ring.size, 0) draw = ImageDraw.Draw(mask) @@ -79,7 +79,7 @@ class PrideAvatar(commands.Cog): return ring @commands.group(aliases=["avatarpride", "pridepfp", "prideprofile"], invoke_without_command=True) - async def prideavatar(self, ctx, option="lgbt", pixels: int = 64): + async def prideavatar(self, ctx: commands.Context, option: str = "lgbt", pixels: int = 64) -> None: """ This surrounds an avatar with a border of a specified LGBT flag. @@ -126,7 +126,7 @@ class PrideAvatar(commands.Cog): await ctx.send(file=file, embed=embed) @prideavatar.command() - async def flags(self, ctx): + async def flags(self, ctx: commands.Context) -> None: """This lists the flags that can be used with the prideavatar command.""" choices = sorted(set(OPTIONS.values())) options = "• " + "\n• ".join(choices) @@ -139,7 +139,7 @@ class PrideAvatar(commands.Cog): await ctx.send(embed=embed) -def setup(bot): +def setup(bot: commands.Bot) -> None: """Cog load.""" bot.add_cog(PrideAvatar(bot)) log.info("PrideAvatar cog loaded") diff --git a/bot/seasons/valentines/be_my_valentine.py b/bot/seasons/valentines/be_my_valentine.py index c4acf17a..a073e1bd 100644 --- a/bot/seasons/valentines/be_my_valentine.py +++ b/bot/seasons/valentines/be_my_valentine.py @@ -1,8 +1,8 @@ import logging import random -import typing from json import load from pathlib import Path +from typing import Optional, Tuple import discord from discord.ext import commands @@ -18,12 +18,12 @@ HEART_EMOJIS = [":heart:", ":gift_heart:", ":revolving_hearts:", ":sparkling_hea class BeMyValentine(commands.Cog): """A cog that sends Valentines to other users!""" - def __init__(self, bot): + def __init__(self, bot: commands.Bot): self.bot = bot self.valentines = self.load_json() @staticmethod - def load_json(): + def load_json() -> dict: """Load Valentines messages from the static resources.""" p = Path("bot/resources/valentines/bemyvalentine_valentines.json") with p.open() as json_data: @@ -31,7 +31,7 @@ class BeMyValentine(commands.Cog): return valentines @commands.group(name="lovefest", invoke_without_command=True) - async def lovefest_role(self, ctx): + async def lovefest_role(self, ctx: commands.Context) -> None: """ Subscribe or unsubscribe from the lovefest role. @@ -43,7 +43,7 @@ class BeMyValentine(commands.Cog): await ctx.send_help(ctx.command) @lovefest_role.command(name="sub") - async def add_role(self, ctx): + async def add_role(self, ctx: commands.Context) -> None: """Adds the lovefest role.""" user = ctx.author role = discord.utils.get(ctx.guild.roles, id=Lovefest.role_id) @@ -54,7 +54,7 @@ class BeMyValentine(commands.Cog): await ctx.send("You already have the role !") @lovefest_role.command(name="unsub") - async def remove_role(self, ctx): + async def remove_role(self, ctx: commands.Context) -> None: """Removes the lovefest role.""" user = ctx.author role = discord.utils.get(ctx.guild.roles, id=Lovefest.role_id) @@ -66,7 +66,9 @@ class BeMyValentine(commands.Cog): @commands.cooldown(1, 1800, BucketType.user) @commands.group(name='bemyvalentine', invoke_without_command=True) - async def send_valentine(self, ctx, user: typing.Optional[discord.Member] = None, *, valentine_type=None): + async def send_valentine( + self, ctx: commands.Context, user: Optional[discord.Member] = None, *, valentine_type: str = None + ) -> None: """ Send a valentine to user, if specified, or to a random user with the lovefest role. @@ -112,7 +114,9 @@ class BeMyValentine(commands.Cog): @commands.cooldown(1, 1800, BucketType.user) @send_valentine.command(name='secret') - async def anonymous(self, ctx, user: typing.Optional[discord.Member] = None, *, valentine_type=None): + async def anonymous( + self, ctx: commands.Context, user: Optional[discord.Member] = None, *, valentine_type: str = None + ) -> None: """ Send an anonymous Valentine via DM to to a user, if specified, or to a random with the lovefest role. @@ -164,7 +168,7 @@ class BeMyValentine(commands.Cog): else: await ctx.author.send(f"Your message has been sent to {user}") - def valentine_check(self, valentine_type): + def valentine_check(self, valentine_type: str) -> Tuple[str, str]: """Return the appropriate Valentine type & title based on the invoking user's input.""" if valentine_type is None: valentine, title = self.random_valentine() @@ -184,7 +188,7 @@ class BeMyValentine(commands.Cog): return valentine, title @staticmethod - def random_user(author: discord.Member, members: discord.Member): + def random_user(author: discord.Member, members: discord.Member) -> None: """ Picks a random member from the list provided in `members`. @@ -196,13 +200,13 @@ class BeMyValentine(commands.Cog): return random.choice(members) if members else None @staticmethod - def random_emoji(): + def random_emoji() -> Tuple[str, str]: """Return two random emoji from the module-defined constants.""" EMOJI_1 = random.choice(HEART_EMOJIS) EMOJI_2 = random.choice(HEART_EMOJIS) return EMOJI_1, EMOJI_2 - def random_valentine(self): + def random_valentine(self) -> Tuple[str, str]: """Grabs a random poem or a compliment (any message).""" valentine_poem = random.choice(self.valentines['valentine_poems']) valentine_compliment = random.choice(self.valentines['valentine_compliments']) @@ -213,18 +217,18 @@ class BeMyValentine(commands.Cog): title = 'A compliment for ' return random_valentine, title - def valentine_poem(self): + def valentine_poem(self) -> str: """Grabs a random poem.""" valentine_poem = random.choice(self.valentines['valentine_poems']) return valentine_poem - def valentine_compliment(self): + def valentine_compliment(self) -> str: """Grabs a random compliment.""" valentine_compliment = random.choice(self.valentines['valentine_compliments']) return valentine_compliment -def setup(bot): +def setup(bot: commands.Bot) -> None: """Be my Valentine Cog load.""" bot.add_cog(BeMyValentine(bot)) log.info("BeMyValentine cog loaded") diff --git a/bot/seasons/valentines/lovecalculator.py b/bot/seasons/valentines/lovecalculator.py index 1d5a028d..207ef557 100644 --- a/bot/seasons/valentines/lovecalculator.py +++ b/bot/seasons/valentines/lovecalculator.py @@ -23,12 +23,12 @@ with Path("bot/resources/valentines/love_matches.json").open() as file: class LoveCalculator(Cog): """A cog for calculating the love between two people.""" - def __init__(self, bot): + def __init__(self, bot: commands.Bot): self.bot = bot @commands.command(aliases=('love_calculator', 'love_calc')) @commands.cooldown(rate=1, per=5, type=commands.BucketType.user) - async def love(self, ctx, who: Union[Member, str], whom: Union[Member, str] = None): + async def love(self, ctx: commands.Context, who: Union[Member, str], whom: Union[Member, str] = None) -> None: """ Tells you how much the two love each other. @@ -98,7 +98,7 @@ class LoveCalculator(Cog): await ctx.send(embed=embed) -def setup(bot): +def setup(bot: commands.Bot) -> None: """Love calculator Cog load.""" bot.add_cog(LoveCalculator(bot)) log.info("LoveCalculator cog loaded") diff --git a/bot/seasons/valentines/movie_generator.py b/bot/seasons/valentines/movie_generator.py index fa5f236a..ce1d7d5b 100644 --- a/bot/seasons/valentines/movie_generator.py +++ b/bot/seasons/valentines/movie_generator.py @@ -14,11 +14,11 @@ log = logging.getLogger(__name__) class RomanceMovieFinder(commands.Cog): """A Cog that returns a random romance movie suggestion to a user.""" - def __init__(self, bot): + def __init__(self, bot: commands.Bot): self.bot = bot @commands.command(name="romancemovie") - async def romance_movie(self, ctx): + async def romance_movie(self, ctx: commands.Context) -> None: """Randomly selects a romance movie and displays information about it.""" # Selecting a random int to parse it to the page parameter random_page = random.randint(0, 20) @@ -57,7 +57,7 @@ class RomanceMovieFinder(commands.Cog): await ctx.send(embed=embed) -def setup(bot): +def setup(bot: commands.Bot) -> None: """Romance movie Cog load.""" bot.add_cog(RomanceMovieFinder(bot)) log.info("RomanceMovieFinder cog loaded") diff --git a/bot/seasons/valentines/myvalenstate.py b/bot/seasons/valentines/myvalenstate.py index fad202e3..0256c39a 100644 --- a/bot/seasons/valentines/myvalenstate.py +++ b/bot/seasons/valentines/myvalenstate.py @@ -18,10 +18,10 @@ with open(Path("bot/resources/valentines/valenstates.json"), "r") as file: class MyValenstate(commands.Cog): """A Cog to find your most likely Valentine's vacation destination.""" - def __init__(self, bot): + def __init__(self, bot: commands.Bot): self.bot = bot - def levenshtein(self, source, goal): + def levenshtein(self, source: str, goal: str) -> int: """Calculates the Levenshtein Distance between source and goal.""" if len(source) < len(goal): return self.levenshtein(goal, source) @@ -42,7 +42,7 @@ class MyValenstate(commands.Cog): return pre_row[-1] @commands.command() - async def myvalenstate(self, ctx, *, name=None): + async def myvalenstate(self, ctx: commands.Context, *, name: str = None) -> None: """Find the vacation spot(s) with the most matching characters to the invoking user.""" eq_chars = collections.defaultdict(int) if name is None: @@ -81,7 +81,7 @@ class MyValenstate(commands.Cog): await ctx.channel.send(embed=embed) -def setup(bot): +def setup(bot: commands.Bot) -> None: """Valenstate Cog load.""" bot.add_cog(MyValenstate(bot)) log.info("MyValenstate cog loaded") diff --git a/bot/seasons/valentines/pickuplines.py b/bot/seasons/valentines/pickuplines.py index 46772197..8b2c9822 100644 --- a/bot/seasons/valentines/pickuplines.py +++ b/bot/seasons/valentines/pickuplines.py @@ -17,11 +17,11 @@ with open(Path("bot/resources/valentines/pickup_lines.json"), "r", encoding="utf class PickupLine(commands.Cog): """A cog that gives random cheesy pickup lines.""" - def __init__(self, bot): + def __init__(self, bot: commands.Bot): self.bot = bot @commands.command() - async def pickupline(self, ctx): + async def pickupline(self, ctx: commands.Context) -> None: """ Gives you a random pickup line. @@ -39,7 +39,7 @@ class PickupLine(commands.Cog): await ctx.send(embed=embed) -def setup(bot): +def setup(bot: commands.Bot) -> None: """Pickup lines Cog load.""" bot.add_cog(PickupLine(bot)) log.info('PickupLine cog loaded') diff --git a/bot/seasons/valentines/savethedate.py b/bot/seasons/valentines/savethedate.py index 34264183..e0bc3904 100644 --- a/bot/seasons/valentines/savethedate.py +++ b/bot/seasons/valentines/savethedate.py @@ -19,11 +19,11 @@ with open(Path("bot/resources/valentines/date_ideas.json"), "r", encoding="utf8" class SaveTheDate(commands.Cog): """A cog that gives random suggestion for a Valentine's date.""" - def __init__(self, bot): + def __init__(self, bot: commands.Bot): self.bot = bot @commands.command() - async def savethedate(self, ctx): + async def savethedate(self, ctx: commands.Context) -> None: """Gives you ideas for what to do on a date with your valentine.""" random_date = random.choice(VALENTINES_DATES['ideas']) emoji_1 = random.choice(HEART_EMOJIS) @@ -36,7 +36,7 @@ class SaveTheDate(commands.Cog): await ctx.send(embed=embed) -def setup(bot): +def setup(bot: commands.Bot) -> None: """Save the date Cog Load.""" bot.add_cog(SaveTheDate(bot)) log.info("SaveTheDate cog loaded") diff --git a/bot/seasons/valentines/valentine_zodiac.py b/bot/seasons/valentines/valentine_zodiac.py index fa849cb2..c8d77e75 100644 --- a/bot/seasons/valentines/valentine_zodiac.py +++ b/bot/seasons/valentines/valentine_zodiac.py @@ -17,12 +17,12 @@ HEART_EMOJIS = [":heart:", ":gift_heart:", ":revolving_hearts:", ":sparkling_hea class ValentineZodiac(commands.Cog): """A Cog that returns a counter compatible zodiac sign to the given user's zodiac sign.""" - def __init__(self, bot): + def __init__(self, bot: commands.Bot): self.bot = bot self.zodiacs = self.load_json() @staticmethod - def load_json(): + def load_json() -> dict: """Load zodiac compatibility from static JSON resource.""" p = Path("bot/resources/valentines/zodiac_compatibility.json") with p.open() as json_data: @@ -30,7 +30,7 @@ class ValentineZodiac(commands.Cog): return zodiacs @commands.command(name="partnerzodiac") - async def counter_zodiac(self, ctx, zodiac_sign): + async def counter_zodiac(self, ctx: commands.Context, zodiac_sign: str) -> None: """Provides a counter compatible zodiac sign to the given user's zodiac sign.""" try: compatible_zodiac = random.choice(self.zodiacs[zodiac_sign.lower()]) @@ -52,7 +52,7 @@ class ValentineZodiac(commands.Cog): await ctx.send(embed=embed) -def setup(bot): +def setup(bot: commands.Bot) -> None: """Valentine zodiac Cog load.""" bot.add_cog(ValentineZodiac(bot)) log.info("ValentineZodiac cog loaded") diff --git a/bot/seasons/valentines/whoisvalentine.py b/bot/seasons/valentines/whoisvalentine.py index d73ccd9b..b8586dca 100644 --- a/bot/seasons/valentines/whoisvalentine.py +++ b/bot/seasons/valentines/whoisvalentine.py @@ -17,11 +17,11 @@ with open(Path("bot/resources/valentines/valentine_facts.json"), "r") as file: class ValentineFacts(commands.Cog): """A Cog for displaying facts about Saint Valentine.""" - def __init__(self, bot): + def __init__(self, bot: commands.Bot): self.bot = bot @commands.command(aliases=('whoisvalentine', 'saint_valentine')) - async def who_is_valentine(self, ctx): + async def who_is_valentine(self, ctx: commands.Context) -> None: """Displays info about Saint Valentine.""" embed = discord.Embed( title="Who is Saint Valentine?", @@ -36,7 +36,7 @@ class ValentineFacts(commands.Cog): await ctx.channel.send(embed=embed) @commands.command() - async def valentine_fact(self, ctx): + async def valentine_fact(self, ctx: commands.Context) -> None: """Shows a random fact about Valentine's Day.""" embed = discord.Embed( title=choice(FACTS['titles']), @@ -47,7 +47,7 @@ class ValentineFacts(commands.Cog): await ctx.channel.send(embed=embed) -def setup(bot): +def setup(bot: commands.Bot) -> None: """Who is Valentine Cog load.""" bot.add_cog(ValentineFacts(bot)) log.info("ValentineFacts cog loaded") diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py index 3249a9cf..8732eb22 100644 --- a/bot/utils/__init__.py +++ b/bot/utils/__init__.py @@ -29,7 +29,7 @@ async def disambiguate( choices = (f'{index}: {entry}' for index, entry in enumerate(entries, start=1)) - def check(message): + def check(message: discord.Message) -> bool: return (message.content.isdigit() and message.author == ctx.author and message.channel == ctx.channel) @@ -109,7 +109,7 @@ def replace_many( pattern = "|".join(re.escape(word) for word in words_to_replace) regex = re.compile(pattern, re.I if ignore_case else 0) - def _repl(match): + def _repl(match: re.Match) -> str: """Returns replacement depending on `ignore_case` and `match_case`""" word = match.group(0) replacement = replacements[word.lower() if ignore_case else word] diff --git a/bot/utils/halloween/spookifications.py b/bot/utils/halloween/spookifications.py index 69b49919..11f69850 100644 --- a/bot/utils/halloween/spookifications.py +++ b/bot/utils/halloween/spookifications.py @@ -7,7 +7,7 @@ from PIL import ImageOps log = logging.getLogger() -def inversion(im): +def inversion(im: Image) -> Image: """ Inverts the image. @@ -18,7 +18,7 @@ def inversion(im): return inv -def pentagram(im): +def pentagram(im: Image) -> Image: """Adds pentagram to the image.""" im = im.convert('RGB') wt, ht = im.size @@ -28,7 +28,7 @@ def pentagram(im): return im -def bat(im): +def bat(im: Image) -> Image: """ Adds a bat silhoutte to the image. @@ -50,7 +50,7 @@ def bat(im): return im -def get_random_effect(im): +def get_random_effect(im: Image) -> Image: """Randomly selects and applies an effect.""" effects = [inversion, pentagram, bat] effect = choice(effects) -- cgit v1.2.3 From da20c52802cbb4c68cfbb058e9ffc986b591240f Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Wed, 11 Sep 2019 11:33:16 -0400 Subject: Fix incorrect merge conflict resolutions, lint remaining items --- bot/seasons/easter/avatar_easterifier.py | 2 +- bot/seasons/easter/bunny_name_generator.py | 6 +++--- bot/seasons/easter/easter_riddle.py | 2 +- bot/seasons/easter/egghead_quiz.py | 4 ++-- bot/seasons/easter/traditions.py | 2 +- bot/seasons/evergreen/minesweeper.py | 8 ++++---- bot/seasons/evergreen/showprojects.py | 4 ++-- bot/seasons/evergreen/snakes/utils.py | 26 ++++++++++++++++---------- bot/seasons/evergreen/speedrun.py | 2 +- bot/seasons/halloween/hacktoberstats.py | 2 +- bot/utils/__init__.py | 2 +- tox.ini | 2 +- 12 files changed, 34 insertions(+), 28 deletions(-) (limited to 'bot/utils') diff --git a/bot/seasons/easter/avatar_easterifier.py b/bot/seasons/easter/avatar_easterifier.py index f056068e..85c32909 100644 --- a/bot/seasons/easter/avatar_easterifier.py +++ b/bot/seasons/easter/avatar_easterifier.py @@ -34,7 +34,7 @@ class AvatarEasterifier(commands.Cog): r1, g1, b1 = x def distance(point: Tuple[int, int, int]) -> Tuple[int, int, int]: - """Finds the difference between a pastel colour and the original pixel colour""" + """Finds the difference between a pastel colour and the original pixel colour.""" r2, g2, b2 = point return ((r1 - r2)**2 + (g1 - g2)**2 + (b1 - b2)**2) diff --git a/bot/seasons/easter/bunny_name_generator.py b/bot/seasons/easter/bunny_name_generator.py index 22957b7f..97c467e1 100644 --- a/bot/seasons/easter/bunny_name_generator.py +++ b/bot/seasons/easter/bunny_name_generator.py @@ -47,7 +47,7 @@ class BunnyNameGenerator(commands.Cog): return new_name def append_name(self, displayname: str) -> str: - """Adds a suffix to the end of the Discord name""" + """Adds a suffix to the end of the Discord name.""" extensions = ['foot', 'ear', 'nose', 'tail'] suffix = random.choice(extensions) appended_name = displayname + suffix @@ -56,12 +56,12 @@ class BunnyNameGenerator(commands.Cog): @commands.command() async def bunnyname(self, ctx: commands.Context) -> None: - """Picks a random bunny name from a JSON file""" + """Picks a random bunny name from a JSON file.""" await ctx.send(random.choice(BUNNY_NAMES["names"])) @commands.command() async def bunnifyme(self, ctx: commands.Context) -> None: - """Gets your Discord username and bunnifies it""" + """Gets your Discord username and bunnifies it.""" username = ctx.message.author.display_name # If name contains spaces or other separators, get the individual words to randomly bunnify diff --git a/bot/seasons/easter/easter_riddle.py b/bot/seasons/easter/easter_riddle.py index c3f19055..4b98b204 100644 --- a/bot/seasons/easter/easter_riddle.py +++ b/bot/seasons/easter/easter_riddle.py @@ -84,7 +84,7 @@ class EasterRiddle(commands.Cog): @commands.Cog.listener() async def on_message(self, message: discord.Messaged) -> None: - """If a non-bot user enters a correct answer, their username gets added to self.winners""" + """If a non-bot user enters a correct answer, their username gets added to self.winners.""" if self.current_channel != message.channel: return diff --git a/bot/seasons/easter/egghead_quiz.py b/bot/seasons/easter/egghead_quiz.py index 0b175bf1..bd179fe2 100644 --- a/bot/seasons/easter/egghead_quiz.py +++ b/bot/seasons/easter/egghead_quiz.py @@ -97,13 +97,13 @@ class EggheadQuiz(commands.Cog): @staticmethod async def already_reacted(message: discord.Message, user: Union[discord.Member, discord.User]) -> bool: - """Returns whether a given user has reacted more than once to a given message""" + """Returns whether a given user has reacted more than once to a given message.""" users = [u.id for reaction in [await r.users().flatten() for r in message.reactions] for u in reaction] return users.count(user.id) > 1 # Old reaction plus new reaction @commands.Cog.listener() async def on_reaction_add(self, reaction: discord.Reaction, user: Union[discord.Member, discord.User]) -> None: - """Listener to listen specifically for reactions of quiz messages""" + """Listener to listen specifically for reactions of quiz messages.""" if user.bot: return if reaction.message.id not in self.quiz_messages: diff --git a/bot/seasons/easter/traditions.py b/bot/seasons/easter/traditions.py index 4fb4694f..9529823f 100644 --- a/bot/seasons/easter/traditions.py +++ b/bot/seasons/easter/traditions.py @@ -19,7 +19,7 @@ class Traditions(commands.Cog): @commands.command(aliases=('eastercustoms',)) async def easter_tradition(self, ctx: commands.Context) -> None: - """Responds with a random tradition or custom""" + """Responds with a random tradition or custom.""" random_country = random.choice(list(traditions)) await ctx.send(f"{random_country}:\n{traditions[random_country]}") diff --git a/bot/seasons/evergreen/minesweeper.py b/bot/seasons/evergreen/minesweeper.py index 015b09df..b0ba8145 100644 --- a/bot/seasons/evergreen/minesweeper.py +++ b/bot/seasons/evergreen/minesweeper.py @@ -33,7 +33,7 @@ class CoordinateConverter(commands.Converter): """Converter for Coordinates.""" async def convert(self, ctx: commands.Context, coordinate: str) -> typing.Tuple[int, int]: - """Take in a coordinate string and turn it into x, y""" + """Take in a coordinate string and turn it into an (x, y) tuple.""" if not 2 <= len(coordinate) <= 3: raise commands.BadArgument('Invalid co-ordinate provided') @@ -81,7 +81,7 @@ class Minesweeper(commands.Cog): @commands.group(name='minesweeper', aliases=('ms',), invoke_without_command=True) async def minesweeper_group(self, ctx: commands.Context) -> None: - """Commands for Playing Minesweeper""" + """Commands for Playing Minesweeper.""" await ctx.send_help(ctx.command) @staticmethod @@ -216,7 +216,7 @@ class Minesweeper(commands.Cog): self.reveal_zeros(revealed, board, x_, y_) async def check_if_won(self, ctx: commands.Context, revealed: GameBoard, board: GameBoard) -> bool: - """Checks if a player has won""" + """Checks if a player has won.""" if any( revealed[y][x] in ["hidden", "flag"] and board[y][x] != "bomb" for x in range(10) @@ -268,7 +268,7 @@ class Minesweeper(commands.Cog): @minesweeper_group.command(name="end") async def end_command(self, ctx: commands.Context) -> None: - """End your current game""" + """End your current game.""" game = self.games[ctx.author.id] game.revealed = game.board await self.update_boards(ctx) diff --git a/bot/seasons/evergreen/showprojects.py b/bot/seasons/evergreen/showprojects.py index d41132aa..a943e548 100644 --- a/bot/seasons/evergreen/showprojects.py +++ b/bot/seasons/evergreen/showprojects.py @@ -17,7 +17,7 @@ class ShowProjects(commands.Cog): @commands.Cog.listener() async def on_message(self, message: Message) -> None: - """Adds reactions to posts in #show-your-projects""" + """Adds reactions to posts in #show-your-projects.""" reactions = ["\U0001f44d", "\U00002764", "\U0001f440", "\U0001f389", "\U0001f680", "\U00002b50", "\U0001f6a9"] if (message.channel.id == Channels.show_your_projects and message.author.bot is False @@ -29,6 +29,6 @@ class ShowProjects(commands.Cog): def setup(bot: commands.Bot) -> None: - """Show Projects Reaction Cog""" + """Show Projects Reaction Cog.""" bot.add_cog(ShowProjects(bot)) log.info("ShowProjects cog loaded") diff --git a/bot/seasons/evergreen/snakes/utils.py b/bot/seasons/evergreen/snakes/utils.py index 76809bd4..7d6caf04 100644 --- a/bot/seasons/evergreen/snakes/utils.py +++ b/bot/seasons/evergreen/snakes/utils.py @@ -388,8 +388,7 @@ class SnakeAndLaddersGame: """ Create a new Snakes and Ladders game. - Listen for reactions until players have joined, - and the game has been started. + Listen for reactions until players have joined, and the game has been started. """ def startup_event_check(reaction_: Reaction, user_: Member) -> bool: """Make sure that this reaction is what we want to operate on.""" @@ -457,6 +456,7 @@ class SnakeAndLaddersGame: return # We're done, no reactions for the last 5 minutes async def _add_player(self, user: Member) -> None: + """Add player to game.""" self.players.append(user) self.player_tiles[user.id] = 1 @@ -490,7 +490,7 @@ class SnakeAndLaddersGame: delete_after=10 ) - async def player_leave(self, user: Member) -> None: + async def player_leave(self, user: Member) -> bool: """ Handle players leaving the game. @@ -515,11 +515,13 @@ class SnakeAndLaddersGame: is_surrendered = True self._destruct() - async def cancel_game(self, user: Member) -> None: - """Allow the game author to cancel the running game.""" - if not user == self.author: - await self.channel.send(user.mention + " Only the author of the game can cancel it.", delete_after=10) - return + return is_surrendered + else: + await self.channel.send(user.mention + " You are not in the match.", delete_after=10) + return is_surrendered + + async def cancel_game(self) -> None: + """Cancel the running game.""" await self.channel.send("**Snakes and Ladders**: Game has been canceled.") self._destruct() @@ -670,6 +672,7 @@ class SnakeAndLaddersGame: self.round_has_rolled[user.id] = True async def _complete_round(self) -> None: + """At the conclusion of a round check to see if there's been a winner.""" self.state = 'post_round' # check for winner @@ -684,19 +687,22 @@ class SnakeAndLaddersGame: self._destruct() def _check_winner(self) -> Member: + """Return a winning member if we're in the post-round state and there's a winner.""" if self.state != 'post_round': return None return next((player for player in self.players if self.player_tiles[player.id] == 100), None) def _check_all_rolled(self) -> bool: + """Check if all members have made their roll.""" return all(rolled for rolled in self.round_has_rolled.values()) def _destruct(self) -> None: + """Clean up the finished game object.""" del self.snakes.active_sal[self.channel] - def _board_coordinate_from_index(self, index: int) -> Tuple[float, float]: - # converts the tile number to the x/y coordinates for graphical purposes + def _board_coordinate_from_index(self, index: int) -> Tuple[int, int]: + """Convert the tile number to the x/y coordinates for graphical purposes.""" y_level = 9 - math.floor((index - 1) / 10) is_reversed = math.floor((index - 1) / 10) % 2 != 0 x_level = (index - 1) % 10 diff --git a/bot/seasons/evergreen/speedrun.py b/bot/seasons/evergreen/speedrun.py index 2f59c886..76c5e8d3 100644 --- a/bot/seasons/evergreen/speedrun.py +++ b/bot/seasons/evergreen/speedrun.py @@ -23,6 +23,6 @@ class Speedrun(commands.Cog): def setup(bot: commands.Bot) -> None: - """Load the Speedrun cog""" + """Load the Speedrun cog.""" bot.add_cog(Speedrun(bot)) log.info("Speedrun cog loaded") diff --git a/bot/seasons/halloween/hacktoberstats.py b/bot/seasons/halloween/hacktoberstats.py index 5687a5c7..0f513953 100644 --- a/bot/seasons/halloween/hacktoberstats.py +++ b/bot/seasons/halloween/hacktoberstats.py @@ -19,7 +19,7 @@ PRS_FOR_SHIRT = 4 # Minimum number of PRs before a shirt is awarded class HacktoberStats(commands.Cog): """Hacktoberfest statistics Cog.""" - def __init__(self, bot): + def __init__(self, bot: commands.Bot): self.bot = bot self.link_json = Path("bot/resources/github_links.json") self.linked_accounts = self.load_linked_users() diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py index 8732eb22..0aa50af6 100644 --- a/bot/utils/__init__.py +++ b/bot/utils/__init__.py @@ -110,7 +110,7 @@ def replace_many( regex = re.compile(pattern, re.I if ignore_case else 0) def _repl(match: re.Match) -> str: - """Returns replacement depending on `ignore_case` and `match_case`""" + """Returns replacement depending on `ignore_case` and `match_case`.""" word = match.group(0) replacement = replacements[word.lower() if ignore_case else word] diff --git a/tox.ini b/tox.ini index dec88854..ee898b0d 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ ignore= # Docstring Quotes D301,D302, # Docstring Content - D400,D401,D402,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414 + D400,D401,D402,D404,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414,D416,D417 # Type Annotations TYP002,TYP003,TYP101,TYP102,TYP204,TYP206 exclude= -- cgit v1.2.3 From f91d49d987065df43cbaa8264d349885f001aa17 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Thu, 19 Sep 2019 22:59:46 +1000 Subject: Add persistent datafile utils. --- bot/seasons/halloween/hacktoberstats.py | 4 +++- bot/utils/persist.py | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 bot/utils/persist.py (limited to 'bot/utils') diff --git a/bot/seasons/halloween/hacktoberstats.py b/bot/seasons/halloween/hacktoberstats.py index 0f513953..9dfb20bd 100644 --- a/bot/seasons/halloween/hacktoberstats.py +++ b/bot/seasons/halloween/hacktoberstats.py @@ -10,6 +10,8 @@ import aiohttp import discord from discord.ext import commands +from bot.utils.persist import datafile + log = logging.getLogger(__name__) CURRENT_YEAR = datetime.now().year # Used to construct GH API query @@ -21,7 +23,7 @@ class HacktoberStats(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - self.link_json = Path("bot/resources/github_links.json") + self.link_json = datafile(Path("bot", "resources", "halloween", "github_links.json")) self.linked_accounts = self.load_linked_users() @commands.group(name="hacktoberstats", aliases=("hackstats",), invoke_without_command=True) diff --git a/bot/utils/persist.py b/bot/utils/persist.py new file mode 100644 index 00000000..ec6f306a --- /dev/null +++ b/bot/utils/persist.py @@ -0,0 +1,24 @@ +import sqlite3 +from pathlib import Path +from shutil import copyfile + +DIRECTORY = Path("data") # directory that has a persistent volume mapped to it + + +def datafile(file_path: Path) -> Path: + """Copy datafile at the provided file_path to the persistent data directory.""" + if not file_path.exists(): + raise OSError(f"File not found at {file_path}.") + + persistant_path = Path(DIRECTORY, file_path.name) + + if not persistant_path.exists(): + copyfile(file_path, persistant_path) + + return persistant_path + + +def sqlite(db_path: Path) -> sqlite3.Connection: + """Copy sqlite file to the persistent data directory and return an open connection.""" + persistant_path = datafile(db_path) + return sqlite3.connect(persistant_path) -- cgit v1.2.3 From 4d7c93296d67f182b849d2a5227d692640452085 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Fri, 20 Sep 2019 11:10:30 +1000 Subject: Add better explanatory docstring and example for persist.datafile. --- bot/utils/persist.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) (limited to 'bot/utils') diff --git a/bot/utils/persist.py b/bot/utils/persist.py index ec6f306a..06c3764a 100644 --- a/bot/utils/persist.py +++ b/bot/utils/persist.py @@ -6,7 +6,23 @@ DIRECTORY = Path("data") # directory that has a persistent volume mapped to it def datafile(file_path: Path) -> Path: - """Copy datafile at the provided file_path to the persistent data directory.""" + """ + Copy datafile at the provided file_path to the persistent data directory. + + A persistent data file is needed by some features in order to not lose data + after bot rebuilds. + + This function will ensure that a clean data file with default schema, + structure or data is copied over to the persistent volume before returning + the path to this new persistent version of the file. + + If the persistent file already exists, it won't be overwritten with the + clean default file, just returning the Path instead to the existing file. + + Example Usage: + >>> clean_default_datafile = Path("bot", "resources", "datafile.json") + >>> persistent_file_path = datafile(clean_default_datafile) + """ if not file_path.exists(): raise OSError(f"File not found at {file_path}.") -- cgit v1.2.3 From b385db0f08fb8bb3d46cf8f820d5dd525d7b2272 Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Mon, 23 Sep 2019 12:01:57 +1000 Subject: Check explicitly if file exists rather than any existing path. Co-Authored-By: Mark --- bot/utils/persist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot/utils') diff --git a/bot/utils/persist.py b/bot/utils/persist.py index 06c3764a..35e1e41a 100644 --- a/bot/utils/persist.py +++ b/bot/utils/persist.py @@ -23,7 +23,7 @@ def datafile(file_path: Path) -> Path: >>> clean_default_datafile = Path("bot", "resources", "datafile.json") >>> persistent_file_path = datafile(clean_default_datafile) """ - if not file_path.exists(): + if not file_path.is_file(): raise OSError(f"File not found at {file_path}.") persistant_path = Path(DIRECTORY, file_path.name) -- cgit v1.2.3 From c1b8b23e4a327631f287b1dc68ab76967651e57e Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Mon, 23 Sep 2019 16:12:18 +1000 Subject: Improve func name, example, directory management Function name has been changed to `make_persistent` after prompt by @lemonsaurus asking for a more descriptive name. Thanks @MarkKoz for providing the alternate name. During local testing, the `data` directory doesn't exist yet. In prod, this isn't an issue as the persistent volume is mounted at that location. To make local testing more convenient, the directory is checked and made if not found. Persistent data files will be placed in a seasonal subdirectory so long as they have a valid season name somewhere in their path, otherwise they will be placed directly in the data directory. Added a note to docstring to avoid same-named files in the same seasons or it will conflict with each other in the persistent data directory. The example was extended a little bit to make it both actually valid if tested and hopefully make it easier to understand what's going on. --- bot/utils/persist.py | 46 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 9 deletions(-) (limited to 'bot/utils') diff --git a/bot/utils/persist.py b/bot/utils/persist.py index 35e1e41a..939a95c9 100644 --- a/bot/utils/persist.py +++ b/bot/utils/persist.py @@ -2,10 +2,12 @@ import sqlite3 from pathlib import Path from shutil import copyfile +from bot.seasons.season import get_seasons + DIRECTORY = Path("data") # directory that has a persistent volume mapped to it -def datafile(file_path: Path) -> Path: +def make_persistent(file_path: Path) -> Path: """ Copy datafile at the provided file_path to the persistent data directory. @@ -19,22 +21,48 @@ def datafile(file_path: Path) -> Path: If the persistent file already exists, it won't be overwritten with the clean default file, just returning the Path instead to the existing file. + Note: Avoid using the same file name as other features in the same seasons + as otherwise only one datafile can be persistent and will be returned for + both cases. + Example Usage: - >>> clean_default_datafile = Path("bot", "resources", "datafile.json") - >>> persistent_file_path = datafile(clean_default_datafile) + >>> import json + >>> template_datafile = Path("bot", "resources", "evergreen", "myfile.json") + >>> path_to_persistent_file = make_persistent(template_datafile) + >>> print(path_to_persistent_file) + data/evergreen/myfile.json + >>> with path_to_persistent_file.open("w+") as f: + >>> data = json.load(f) """ + # ensure the persistent data directory exists + if not DIRECTORY.exists(): + DIRECTORY.mkdir() + if not file_path.is_file(): raise OSError(f"File not found at {file_path}.") - persistant_path = Path(DIRECTORY, file_path.name) + # detect season in datafile path for assigning to subdirectory + season = next((s for s in get_seasons() if s in file_path.parts), None) + + if season: + # make sure subdirectory exists first + subdirectory = Path(DIRECTORY, season) + if not subdirectory.exists(): + subdirectory.mkdir() + + persistent_path = Path(subdirectory, file_path.name) + + else: + persistent_path = Path(DIRECTORY, file_path.name) - if not persistant_path.exists(): - copyfile(file_path, persistant_path) + # copy base/template datafile to persistent directory + if not persistent_path.exists(): + copyfile(file_path, persistent_path) - return persistant_path + return persistent_path def sqlite(db_path: Path) -> sqlite3.Connection: """Copy sqlite file to the persistent data directory and return an open connection.""" - persistant_path = datafile(db_path) - return sqlite3.connect(persistant_path) + persistent_path = make_persistent(db_path) + return sqlite3.connect(persistent_path) -- cgit v1.2.3 From 3ea2130d71b69bd733675c43f91d7d081472735c Mon Sep 17 00:00:00 2001 From: scragly <29337040+scragly@users.noreply.github.com> Date: Mon, 23 Sep 2019 17:34:30 +1000 Subject: Use mkdir exists kwarg instead of checking existing ahead of time. --- bot/utils/persist.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'bot/utils') diff --git a/bot/utils/persist.py b/bot/utils/persist.py index 939a95c9..a60a1219 100644 --- a/bot/utils/persist.py +++ b/bot/utils/persist.py @@ -35,8 +35,7 @@ def make_persistent(file_path: Path) -> Path: >>> data = json.load(f) """ # ensure the persistent data directory exists - if not DIRECTORY.exists(): - DIRECTORY.mkdir() + DIRECTORY.mkdir(exist_ok=True) if not file_path.is_file(): raise OSError(f"File not found at {file_path}.") @@ -47,8 +46,7 @@ def make_persistent(file_path: Path) -> Path: if season: # make sure subdirectory exists first subdirectory = Path(DIRECTORY, season) - if not subdirectory.exists(): - subdirectory.mkdir() + subdirectory.mkdir(exist_ok=True) persistent_path = Path(subdirectory, file_path.name) -- cgit v1.2.3