From 04c5de6840d5fdb95e09e8a1b6cddc9543d856f1 Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 14 Nov 2021 14:48:34 +0100 Subject: [PATCH 001/134] :sparkles: new example with an Image generator --- examples/Segoe UI Bold.ttf | Bin 0 -> 36052 bytes examples/Segoe UI.ttf | Bin 0 -> 34161 bytes examples/tweet_generator.py | 150 ++++++++++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 examples/Segoe UI Bold.ttf create mode 100644 examples/Segoe UI.ttf create mode 100644 examples/tweet_generator.py diff --git a/examples/Segoe UI Bold.ttf b/examples/Segoe UI Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..49fdf2773d926c85fd00ab44e2cd3748d08d5b69 GIT binary patch literal 36052 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 literal 0 HcmV?d00001 diff --git a/examples/Segoe UI.ttf b/examples/Segoe UI.ttf new file mode 100644 index 0000000000000000000000000000000000000000..01b1ca8db3988502a9da31e5d508bfcd85c79586 GIT binary patch literal 34161 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 literal 0 HcmV?d00001 diff --git a/examples/tweet_generator.py b/examples/tweet_generator.py new file mode 100644 index 00000000..d0138166 --- /dev/null +++ b/examples/tweet_generator.py @@ -0,0 +1,150 @@ +import io +import textwrap +import urllib.request +from datetime import datetime +from PIL import Image, ImageFont, ImageDraw, ImageOps +from pincer import command, Client, Descripted +from pincer.objects import Message, Embed, MessageContext + + +class Bot(Client): + + @Client.event + async def on_ready(self): + print( + f"Started client on {self.bot}\n" + "Registered commands: " + ", ".join(self.chat_commands) + ) + + @command( + name='twitter', + description='to create fake tweets', + guild=690604075775164437 + ) + async def twitter(self, ctx: MessageContext, content: Descripted[str, '...']): + await ctx.interaction.ack() + + message = content + if len(message) > 280: + return 'A tweet can be at maximum 280 characters long' + + # wrap the message to be multi-line + message = textwrap.wrap(message, 38) + + # download the profile picture and convert it into Image object + request = urllib.request.Request( + f"https://cdn.discordapp.com/avatars/{ctx.author.user.id}/{ctx.author.user.avatar}.webp?size=128", + headers={ + 'User-Agent': 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.7) Gecko/2009021910 Firefox/3.0.7'} + ) + avatar = urllib.request.urlopen(request).read() + avatar = io.BytesIO(avatar) + avatar = Image.open(avatar).convert("RGBA").resize((128, 128)) + + # modify profile picture to be circular + mask = Image.new('L', (128, 128), 0) + draw = ImageDraw.Draw(mask) + draw.ellipse((0, 0) + (128, 128), fill=255) + avatar = ImageOps.fit(avatar, mask.size, centering=(0.5, 0.5)) + avatar.putalpha(mask) + + # create the tweet by pasting the profile picture into a white image + tweet = trans_paste( + avatar, + + # background + Image.new('RGBA', (800, 250 + 50 * len(message)), (255, 255, 255)), + + box=(15, 15) + ) + + # add the fonts + font = ImageFont.truetype('Segoe UI.ttf', 40) + font_small = ImageFont.truetype('Segoe UI.ttf', 30) + font_bold = ImageFont.truetype('Segoe UI Bold.ttf', 40) + + # write the name and username on the Image + draw = ImageDraw.Draw(tweet) + draw.text((180, 20), str(ctx.author.user), fill=(0, 0, 0), + font=font_bold) + draw.text((180, 70), '@' + ctx.author.user.username, fill=(120, 120, 120), + font=font) + + # write the content of the tweet on the Image + message = '\n'.join(message).split(' ') + result = [] + + # generate a dict to set were the text need to be in different color. + # for example, if a word starts with '@' it will be write in blue. + # example: + # [ + # {'color': (0, 0, 0), 'text': 'hello world '}, + # {'color': (0, 154, 234), 'text': '@drawbu'} + # ] + for word in message: + for i_o, o in enumerate(word.split('\n')): + + o += '\n' if i_o != len(word.split('\n')) - 1 else ' ' + + if not result: + result.append({'color': (0, 0, 0), 'text': o}) + continue + + if not o.startswith('@'): + if result[-1:][0]['color'] == (0, 0, 0): + result[-1:][0]['text'] += o + continue + + result.append({'color': (0, 0, 0), 'text': o}) + continue + + result.append({'color': (0, 154, 234), 'text': o}) + + # write the text + draw = ImageDraw.Draw(tweet) + x = 30 + y = 170 + for o in result: + y -= font.getsize(' ')[1] + for l_index, line in enumerate(o['text'].split('\n')): + if l_index != 0: + x = 30 + y += font.getsize(' ')[1] + draw.text((x, y), line, fill=o['color'], font=font) + x += font.getsize(line)[0] + + # write the footer + draw.text( + (30, tweet.size[1] - 60), + datetime.now().strftime( + '%I:%M %p · %d %b. %Y · Twitter for Discord'), + fill=(120, 120, 120), + font=font_small) + + return Message( + embeds=[ + Embed( + title='Twitter for Discord', + description='' + ).set_image(url="attachment://image0.png") + ], + attachments=[tweet] + ) + + +# https://stackoverflow.com/a/53663233/15485584 +def trans_paste(fg_img, bg_img, alpha=1.0, box=(0, 0)): + """ + paste an image into one another + """ + fg_img_trans = Image.new("RGBA", fg_img.size) + fg_img_trans = Image.blend(fg_img_trans, fg_img, alpha) + bg_img.paste(fg_img_trans, box, fg_img_trans) + return bg_img + + +if __name__ == "__main__": + # Of course we have to run our client, you can replace the + # XXXYOURBOTTOKENHEREXXX with your token, or dynamically get it + # through a dotenv/env. + Bot("XXXYOURBOTTOKENHEREXXX").run() From 2b17668ccd55c3198d50d92cefbe69edcc53ad00 Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sat, 20 Nov 2021 23:05:48 +0100 Subject: [PATCH 002/134] :lipstick: changed raw user ping to an nice `@ping` thing --- examples/tweet_generator.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/tweet_generator.py b/examples/tweet_generator.py index d0138166..dd0b044e 100644 --- a/examples/tweet_generator.py +++ b/examples/tweet_generator.py @@ -1,4 +1,5 @@ import io +import re import textwrap import urllib.request from datetime import datetime @@ -25,6 +26,10 @@ async def twitter(self, ctx: MessageContext, content: Descripted[str, '...']): await ctx.interaction.ack() message = content + for text_match, user_id in re.findall(re.compile(r"(<@!(\d+)>)"), message): + print(str(await self.get_user(user_id))) + message = message.replace(text_match, '@' + str(await self.get_user(user_id))) + if len(message) > 280: return 'A tweet can be at maximum 280 characters long' From 148207590c8f75342fb0d74e63c92f0ae95729ba Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sat, 20 Nov 2021 23:27:40 +0100 Subject: [PATCH 003/134] :recycle: download avatar with Pincer method --- examples/tweet_generator.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/examples/tweet_generator.py b/examples/tweet_generator.py index dd0b044e..48d3725e 100644 --- a/examples/tweet_generator.py +++ b/examples/tweet_generator.py @@ -27,7 +27,6 @@ async def twitter(self, ctx: MessageContext, content: Descripted[str, '...']): message = content for text_match, user_id in re.findall(re.compile(r"(<@!(\d+)>)"), message): - print(str(await self.get_user(user_id))) message = message.replace(text_match, '@' + str(await self.get_user(user_id))) if len(message) > 280: @@ -37,14 +36,9 @@ async def twitter(self, ctx: MessageContext, content: Descripted[str, '...']): message = textwrap.wrap(message, 38) # download the profile picture and convert it into Image object - request = urllib.request.Request( - f"https://cdn.discordapp.com/avatars/{ctx.author.user.id}/{ctx.author.user.avatar}.webp?size=128", - headers={ - 'User-Agent': 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.7) Gecko/2009021910 Firefox/3.0.7'} - ) - avatar = urllib.request.urlopen(request).read() - avatar = io.BytesIO(avatar) - avatar = Image.open(avatar).convert("RGBA").resize((128, 128)) + avatar = await self.get_user(ctx.author.user.id) + avatar = await avatar.get_avatar() + avatar = avatar.resize((128, 128)) # modify profile picture to be circular mask = Image.new('L', (128, 128), 0) From 72cd5297933bfb1989d513c76b6c36d10ec2cd94 Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sat, 20 Nov 2021 23:31:26 +0100 Subject: [PATCH 004/134] :heavy_minus_sign: unused dependencies: `io`, `urllib.request` --- examples/tweet_generator.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/tweet_generator.py b/examples/tweet_generator.py index 48d3725e..4cd523f9 100644 --- a/examples/tweet_generator.py +++ b/examples/tweet_generator.py @@ -1,7 +1,5 @@ -import io import re import textwrap -import urllib.request from datetime import datetime from PIL import Image, ImageFont, ImageDraw, ImageOps from pincer import command, Client, Descripted From c32548528c1555fd00359fee21fe8c7068be2df0 Mon Sep 17 00:00:00 2001 From: sigmanificient Date: Sat, 20 Nov 2021 23:57:23 +0100 Subject: [PATCH 005/134] :bug: argument were not passed --- pincer/objects/user/user.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pincer/objects/user/user.py b/pincer/objects/user/user.py index 30945547..a4a00ce7 100644 --- a/pincer/objects/user/user.py +++ b/pincer/objects/user/user.py @@ -134,8 +134,9 @@ def premium(self) -> APINullable[PremiumTypes]: user their premium type in a usable enum. """ return ( - MISSING if self.premium_type is MISSING else PremiumTypes( - self.premium_type) + MISSING + if self.premium_type is MISSING + else PremiumTypes(self.premium_type) ) @property @@ -166,7 +167,9 @@ async def get_avatar(self, size: int = 512, ext: str = "png") -> Image: :class: Image The user's avatar as a Pillow image. """ - async with ClientSession().get(url=self.get_avatar_url()) as resp: + async with ClientSession().get( + url=self.get_avatar_url(size, ext) + ) as resp: avatar = io.BytesIO(await resp.read()) print(Image, dir(Image)) return Image.open(avatar).convert("RGBA") @@ -190,9 +193,8 @@ async def get_dm_channel(self) -> channel.Channel: construct_client_dict( self._client, await self._http.post( - "/users/@me/channels", - data={"recipient_id": self.id} - ) + "/users/@me/channels", data={"recipient_id": self.id} + ), ) ) From a60d18e1558dc847adda6582beb42895dd69b224 Mon Sep 17 00:00:00 2001 From: Yohann Boniface Date: Sun, 21 Nov 2021 00:00:26 +0100 Subject: [PATCH 006/134] Update examples/tweet_generator.py Co-authored-by: Lunarmagpie <65521138+Lunarmagpie@users.noreply.github.com> --- examples/tweet_generator.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/tweet_generator.py b/examples/tweet_generator.py index 4cd523f9..61f9e518 100644 --- a/examples/tweet_generator.py +++ b/examples/tweet_generator.py @@ -33,10 +33,7 @@ async def twitter(self, ctx: MessageContext, content: Descripted[str, '...']): # wrap the message to be multi-line message = textwrap.wrap(message, 38) - # download the profile picture and convert it into Image object - avatar = await self.get_user(ctx.author.user.id) - avatar = await avatar.get_avatar() - avatar = avatar.resize((128, 128)) + avatar = (await ctx.author.user.get_avatar()).resize((128, 128)) # modify profile picture to be circular mask = Image.new('L', (128, 128), 0) From 2a0f4e0d0f45a50d275d28c94dd1e6f983da2e25 Mon Sep 17 00:00:00 2001 From: Yohann Boniface Date: Sun, 21 Nov 2021 00:00:34 +0100 Subject: [PATCH 007/134] Update examples/tweet_generator.py Co-authored-by: Lunarmagpie <65521138+Lunarmagpie@users.noreply.github.com> --- examples/tweet_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tweet_generator.py b/examples/tweet_generator.py index 61f9e518..0c289625 100644 --- a/examples/tweet_generator.py +++ b/examples/tweet_generator.py @@ -25,7 +25,7 @@ async def twitter(self, ctx: MessageContext, content: Descripted[str, '...']): message = content for text_match, user_id in re.findall(re.compile(r"(<@!(\d+)>)"), message): - message = message.replace(text_match, '@' + str(await self.get_user(user_id))) + message = message.replace(text_match, "@%s" % await self.get_user(user_id)) if len(message) > 280: return 'A tweet can be at maximum 280 characters long' From 8356852678e48602f732c21c97ab07818822ff3e Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 21 Nov 2021 00:18:52 +0100 Subject: [PATCH 008/134] :art: fixing formatting --- examples/tweet_generator.py | 91 ++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/examples/tweet_generator.py b/examples/tweet_generator.py index 0c289625..5952a07a 100644 --- a/examples/tweet_generator.py +++ b/examples/tweet_generator.py @@ -7,7 +7,6 @@ class Bot(Client): - @Client.event async def on_ready(self): print( @@ -16,27 +15,33 @@ async def on_ready(self): ) @command( - name='twitter', - description='to create fake tweets', - guild=690604075775164437 + name="twitter", + description="to create fake tweets", ) - async def twitter(self, ctx: MessageContext, content: Descripted[str, '...']): + async def twitter( + self, ctx: MessageContext, content: Descripted[str, "..."] + ): await ctx.interaction.ack() message = content - for text_match, user_id in re.findall(re.compile(r"(<@!(\d+)>)"), message): - message = message.replace(text_match, "@%s" % await self.get_user(user_id)) + for text_match, user_id in re.findall( + re.compile(r"(<@!(\d+)>)"), message + ): + message = message.replace( + text_match, "@%s" % await self.get_user(user_id) + ) if len(message) > 280: - return 'A tweet can be at maximum 280 characters long' + return "A tweet can be at maximum 280 characters long" # wrap the message to be multi-line message = textwrap.wrap(message, 38) + # download the profile picture and convert it into Image object avatar = (await ctx.author.user.get_avatar()).resize((128, 128)) # modify profile picture to be circular - mask = Image.new('L', (128, 128), 0) + mask = Image.new("L", (128, 128), 0) draw = ImageDraw.Draw(mask) draw.ellipse((0, 0) + (128, 128), fill=255) avatar = ImageOps.fit(avatar, mask.size, centering=(0.5, 0.5)) @@ -45,27 +50,30 @@ async def twitter(self, ctx: MessageContext, content: Descripted[str, '...']): # create the tweet by pasting the profile picture into a white image tweet = trans_paste( avatar, - # background - Image.new('RGBA', (800, 250 + 50 * len(message)), (255, 255, 255)), - - box=(15, 15) + Image.new("RGBA", (800, 250 + 50 * len(message)), (255, 255, 255)), + box=(15, 15), ) # add the fonts - font = ImageFont.truetype('Segoe UI.ttf', 40) - font_small = ImageFont.truetype('Segoe UI.ttf', 30) - font_bold = ImageFont.truetype('Segoe UI Bold.ttf', 40) + font = ImageFont.truetype("Segoe UI.ttf", 40) + font_small = ImageFont.truetype("Segoe UI.ttf", 30) + font_bold = ImageFont.truetype("Segoe UI Bold.ttf", 40) # write the name and username on the Image draw = ImageDraw.Draw(tweet) - draw.text((180, 20), str(ctx.author.user), fill=(0, 0, 0), - font=font_bold) - draw.text((180, 70), '@' + ctx.author.user.username, fill=(120, 120, 120), - font=font) + draw.text( + (180, 20), str(ctx.author.user), fill=(0, 0, 0), font=font_bold + ) + draw.text( + (180, 70), + "@" + ctx.author.user.username, + fill=(120, 120, 120), + font=font, + ) # write the content of the tweet on the Image - message = '\n'.join(message).split(' ') + message = "\n".join(message).split(" ") result = [] # generate a dict to set were the text need to be in different color. @@ -76,53 +84,54 @@ async def twitter(self, ctx: MessageContext, content: Descripted[str, '...']): # {'color': (0, 154, 234), 'text': '@drawbu'} # ] for word in message: - for i_o, o in enumerate(word.split('\n')): + for index, text in enumerate(word.split("\n")): - o += '\n' if i_o != len(word.split('\n')) - 1 else ' ' + text += "\n" if index != len(word.split("\n")) - 1 else " " if not result: - result.append({'color': (0, 0, 0), 'text': o}) + result.append({"color": (0, 0, 0), "text": text}) continue - if not o.startswith('@'): - if result[-1:][0]['color'] == (0, 0, 0): - result[-1:][0]['text'] += o + if not text.startswith("@"): + if result[-1:][0]["color"] == (0, 0, 0): + result[-1:][0]["text"] += text continue - result.append({'color': (0, 0, 0), 'text': o}) + result.append({"color": (0, 0, 0), "text": text}) continue - result.append({'color': (0, 154, 234), 'text': o}) + result.append({"color": (0, 154, 234), "text": text}) # write the text draw = ImageDraw.Draw(tweet) x = 30 y = 170 - for o in result: - y -= font.getsize(' ')[1] - for l_index, line in enumerate(o['text'].split('\n')): + for text in result: + y -= font.getsize(" ")[1] + for l_index, line in enumerate(text["text"].split("\n")): if l_index != 0: x = 30 - y += font.getsize(' ')[1] - draw.text((x, y), line, fill=o['color'], font=font) + y += font.getsize(" ")[1] + draw.text((x, y), line, fill=text["color"], font=font) x += font.getsize(line)[0] # write the footer draw.text( (30, tweet.size[1] - 60), datetime.now().strftime( - '%I:%M %p · %d %b. %Y · Twitter for Discord'), + "%I:%M %p · %d %b. %Y · Twitter for Discord" + ), fill=(120, 120, 120), - font=font_small) + font=font_small, + ) return Message( embeds=[ - Embed( - title='Twitter for Discord', - description='' - ).set_image(url="attachment://image0.png") + Embed(title="Twitter for Discord", description="").set_image( + url="attachment://image0.png" + ) ], - attachments=[tweet] + attachments=[tweet], ) From 83cef971160d070eaafdfefe43c8eec8eba5769a Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 21 Nov 2021 00:24:57 +0100 Subject: [PATCH 009/134] moved everything in a subfolder and the resources use _ instead of spaces in the name --- .../{Segoe UI.ttf => tweet_generator/Segoe_UI.ttf} | Bin .../Segoe_UI_Bold.ttf} | Bin examples/{ => tweet_generator}/tweet_generator.py | 6 +++--- 3 files changed, 3 insertions(+), 3 deletions(-) rename examples/{Segoe UI.ttf => tweet_generator/Segoe_UI.ttf} (100%) rename examples/{Segoe UI Bold.ttf => tweet_generator/Segoe_UI_Bold.ttf} (100%) rename examples/{ => tweet_generator}/tweet_generator.py (96%) diff --git a/examples/Segoe UI.ttf b/examples/tweet_generator/Segoe_UI.ttf similarity index 100% rename from examples/Segoe UI.ttf rename to examples/tweet_generator/Segoe_UI.ttf diff --git a/examples/Segoe UI Bold.ttf b/examples/tweet_generator/Segoe_UI_Bold.ttf similarity index 100% rename from examples/Segoe UI Bold.ttf rename to examples/tweet_generator/Segoe_UI_Bold.ttf diff --git a/examples/tweet_generator.py b/examples/tweet_generator/tweet_generator.py similarity index 96% rename from examples/tweet_generator.py rename to examples/tweet_generator/tweet_generator.py index 5952a07a..eef81add 100644 --- a/examples/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -56,9 +56,9 @@ async def twitter( ) # add the fonts - font = ImageFont.truetype("Segoe UI.ttf", 40) - font_small = ImageFont.truetype("Segoe UI.ttf", 30) - font_bold = ImageFont.truetype("Segoe UI Bold.ttf", 40) + font = ImageFont.truetype("Segoe_UI.ttf", 40) + font_small = ImageFont.truetype("Segoe_UI.ttf", 30) + font_bold = ImageFont.truetype("Segoe_UI_Bold.ttf", 40) # write the name and username on the Image draw = ImageDraw.Draw(tweet) From 1bcd656c06d40dcd733db4a2a7c6ef0d3a9b82eb Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Sat, 20 Nov 2021 22:13:29 -0500 Subject: [PATCH 010/134] :recycle: moved commands to subdirectory --- pincer/commands/__init__.py | 8 ++++++++ pincer/commands/arg_types.py | 3 +++ pincer/{ => commands}/commands.py | 16 ++++++++-------- 3 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 pincer/commands/__init__.py create mode 100644 pincer/commands/arg_types.py rename pincer/{ => commands}/commands.py (98%) diff --git a/pincer/commands/__init__.py b/pincer/commands/__init__.py new file mode 100644 index 00000000..6a98d5d8 --- /dev/null +++ b/pincer/commands/__init__.py @@ -0,0 +1,8 @@ +# Copyright Pincer 2021-Present +# Full MIT License can be found in `LICENSE` at the project root. + +from .commands import command, ChatCommandHandler + +__all__ = [ + "command", "ChatCommandHandler" +] diff --git a/pincer/commands/arg_types.py b/pincer/commands/arg_types.py new file mode 100644 index 00000000..dd7a5751 --- /dev/null +++ b/pincer/commands/arg_types.py @@ -0,0 +1,3 @@ +# Copyright Pincer 2021-Present +# Full MIT License can be found in `LICENSE` at the project root. + diff --git a/pincer/commands.py b/pincer/commands/commands.py similarity index 98% rename from pincer/commands.py rename to pincer/commands/commands.py index 729c24ac..d19bcc67 100644 --- a/pincer/commands.py +++ b/pincer/commands/commands.py @@ -12,8 +12,8 @@ from typing import TYPE_CHECKING, get_origin, get_args, Union, Tuple, List from . import __package__ -from .utils.snowflake import Snowflake -from .exceptions import ( +from ..utils.snowflake import Snowflake +from ..exceptions import ( CommandIsNotCoroutine, CommandAlreadyRegistered, TooManyArguments, @@ -23,7 +23,7 @@ InvalidCommandName, ForbiddenError, ) -from .objects import ( +from ..objects import ( ThrottleScope, AppCommand, Role, @@ -32,17 +32,17 @@ Guild, MessageContext, ) -from .objects.app import ( +from ..objects.app import ( AppCommandOptionType, AppCommandOption, AppCommandOptionChoice, ClientCommandStructure, AppCommandType, ) -from .utils import get_index, should_pass_ctx -from .utils.signature import get_signature_and_params -from .utils.types import Coro, MISSING, choice_value_types, Choices -from .utils.types import Singleton, TypeCache, Descripted +from ..utils import get_index, should_pass_ctx +from ..utils.signature import get_signature_and_params +from ..utils.types import Coro, MISSING, choice_value_types, Choices +from ..utils.types import Singleton, TypeCache, Descripted if TYPE_CHECKING: from typing import Any, Optional, Dict From e70f19933b4b63155fde796c8ba5521c8b490ded Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Sat, 20 Nov 2021 22:30:25 -0500 Subject: [PATCH 011/134] :sparkles: added new types --- pincer/__init__.py | 5 ++--- pincer/commands/__init__.py | 4 +++- pincer/commands/arg_types.py | 32 +++++++++++++++++++++++++++++++ pincer/commands/commands.py | 6 +++--- pincer/utils/__init__.py | 7 +++---- pincer/utils/types.py | 37 +----------------------------------- 6 files changed, 44 insertions(+), 47 deletions(-) diff --git a/pincer/__init__.py b/pincer/__init__.py index 894a37d7..9fe1ca11 100644 --- a/pincer/__init__.py +++ b/pincer/__init__.py @@ -27,7 +27,6 @@ RateLimitError, GatewayError, ServerError ) from .objects import Intents -from .utils import Choices, Descripted __package__ = "pincer" __title__ = "Pincer library" @@ -60,11 +59,11 @@ def __repr__(self) -> str: __version__ = repr(version_info) __all__ = ( - "BadRequestError", "Bot", "ChatCommandHandler", "Choices", + "BadRequestError", "Bot", "ChatCommandHandler", "Client", "CogAlreadyExists", "CogError", "CogNotFound", "CommandAlreadyRegistered", "CommandCooldownError", "CommandDescriptionTooLong", "CommandError", "CommandIsNotCoroutine", - "CommandReturnIsEmpty", "Descripted", "DisallowedIntentsError", + "CommandReturnIsEmpty", "DisallowedIntentsError", "DispatchError", "EmbedFieldError", "ForbiddenError", "GatewayConfig", "GatewayError", "HTTPError", "HeartbeatError", "Intents", "InvalidArgumentAnnotation", "InvalidCommandGuild", "InvalidCommandName", diff --git a/pincer/commands/__init__.py b/pincer/commands/__init__.py index 6a98d5d8..182ba456 100644 --- a/pincer/commands/__init__.py +++ b/pincer/commands/__init__.py @@ -2,7 +2,9 @@ # Full MIT License can be found in `LICENSE` at the project root. from .commands import command, ChatCommandHandler +from .arg_types import CommandArg, Description, Name, OptionalArg __all__ = [ - "command", "ChatCommandHandler" + "command", "ChatCommandHandler", "CommandArg", "Description", "Name", + "OptionalArg" ] diff --git a/pincer/commands/arg_types.py b/pincer/commands/arg_types.py index dd7a5751..da862dd8 100644 --- a/pincer/commands/arg_types.py +++ b/pincer/commands/arg_types.py @@ -1,3 +1,35 @@ # Copyright Pincer 2021-Present # Full MIT License can be found in `LICENSE` at the project root. +from typing import Any, Tuple, Union + + +class _CommandTypeMeta(type): + def __getitem__(cls, args: Union[Tuple, Any]): + if not isinstance(args, tuple): + args = args, + + return cls(*args) + + +"""Valid for all Command Types""" + + +class CommandArg(metaclass=_CommandTypeMeta): + def __init__(self, command_type, *args) -> None: + self.command_type = command_type + self.modifiers = args + + +class Description(metaclass=_CommandTypeMeta): + def __init__(self, desc) -> None: + self.desc = desc + + +class Name(metaclass=_CommandTypeMeta): + def __init__(self, name) -> None: + self.name = name + + +class OptionalArg(metaclass=type): + pass diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index d19bcc67..015e7775 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -41,8 +41,8 @@ ) from ..utils import get_index, should_pass_ctx from ..utils.signature import get_signature_and_params -from ..utils.types import Coro, MISSING, choice_value_types, Choices -from ..utils.types import Singleton, TypeCache, Descripted +from ..utils.types import MISSING, choice_value_types +from ..utils.types import Singleton, TypeCache if TYPE_CHECKING: from typing import Any, Optional, Dict @@ -64,7 +64,7 @@ } if TYPE_CHECKING: - from .client import Client + from ..client import Client def command( diff --git a/pincer/utils/__init__.py b/pincer/utils/__init__.py index 023b9a15..3f03dbeb 100644 --- a/pincer/utils/__init__.py +++ b/pincer/utils/__init__.py @@ -14,14 +14,13 @@ from .timestamp import Timestamp from .types import ( - APINullable, Coro, MISSING, Descripted, MissingType, Choices, - choice_value_types, CheckFunction + APINullable, Coro, MISSING, MissingType, choice_value_types, CheckFunction ) __all__ = ( - "APINullable", "APIObject", "CheckFunction", "Choices", "Color", - "Coro", "Descripted", "EventMgr", "HTTPMeta", "MISSING", "MissingType", + "APINullable", "APIObject", "CheckFunction", "Color", + "Coro", "EventMgr", "HTTPMeta", "MISSING", "MissingType", "Snowflake", "Task", "TaskScheduler", "Timestamp", "chdir", "choice_value_types", "convert", "get_index", "get_params", "get_signature_and_params", "should_pass_cls", "should_pass_ctx" diff --git a/pincer/utils/types.py b/pincer/utils/types.py index 160bf6e0..09c091be 100644 --- a/pincer/utils/types.py +++ b/pincer/utils/types.py @@ -4,14 +4,9 @@ from sys import modules from typing import ( - TYPE_CHECKING, TypeVar, Callable, Coroutine, Any, Union, Literal, Optional + TYPE_CHECKING, TypeVar, Callable, Coroutine, Any, Union, Optional ) -from pincer.exceptions import InvalidArgumentAnnotation - -if TYPE_CHECKING: - from typing import Tuple - class MissingType: """Type class for missing attributes and parameters.""" @@ -36,10 +31,6 @@ def __bool__(self) -> bool: # Represents a coroutine. Coro = TypeVar("Coro", bound=Callable[..., Coroutine[Any, Any, Any]]) - -Choices = Literal - - choice_value_types = (str, int, float) CheckFunction = Optional[Callable[[Any], bool]] @@ -71,29 +62,3 @@ def __init__(self): continue TypeCache.cache.update(lcp[module].__dict__) - - -class _TypeInstanceMeta(type): - def __getitem__(cls, args: Tuple[T, str]): - if not isinstance(args, tuple) or len(args) != 2: - raise InvalidArgumentAnnotation( - "Descripted arguments must be a tuple of length 2. " - "(if you are using this as the indented type, just " - "pass two arguments)" - ) - - return cls(*args) - - -class Descripted(metaclass=_TypeInstanceMeta): - # TODO: Write example & more docs - """Description type.""" - - def __init__(self, key: Any, description: str): - if not isinstance(description, str): - raise RuntimeError( - "The description value must always be a string!" - ) - - self.key = key - self.description = description From c1ec2749ba6b3a941a8c7639d2d6bf166fd49dd9 Mon Sep 17 00:00:00 2001 From: Yohann Boniface Date: Sun, 21 Nov 2021 18:01:14 +0100 Subject: [PATCH 012/134] Update examples/tweet_generator/tweet_generator.py Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> --- examples/tweet_generator/tweet_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index eef81add..aaf5a60d 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -73,7 +73,7 @@ async def twitter( ) # write the content of the tweet on the Image - message = "\n".join(message).split(" ") + message = "\n".join(message).split() result = [] # generate a dict to set were the text need to be in different color. From 5f3eb2d39c99c39b2274dbd0141a28dff842ffcc Mon Sep 17 00:00:00 2001 From: Yohann Boniface Date: Sun, 21 Nov 2021 18:01:23 +0100 Subject: [PATCH 013/134] Update examples/tweet_generator/tweet_generator.py Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> --- examples/tweet_generator/tweet_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index aaf5a60d..2a73904e 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -108,7 +108,7 @@ async def twitter( y = 170 for text in result: y -= font.getsize(" ")[1] - for l_index, line in enumerate(text["text"].split("\n")): + for l_index, line in enumerate(text["text"].splitlines()): if l_index != 0: x = 30 y += font.getsize(" ")[1] From 0fbfbaf2d232e5fbbbb46ec67a147c36a772197e Mon Sep 17 00:00:00 2001 From: Yohann Boniface Date: Sun, 21 Nov 2021 18:01:34 +0100 Subject: [PATCH 014/134] Update examples/tweet_generator/tweet_generator.py Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> --- examples/tweet_generator/tweet_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index 2a73904e..7f1a098d 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -109,7 +109,7 @@ async def twitter( for text in result: y -= font.getsize(" ")[1] for l_index, line in enumerate(text["text"].splitlines()): - if l_index != 0: + if l_index: x = 30 y += font.getsize(" ")[1] draw.text((x, y), line, fill=text["color"], font=font) From 7984bcc355e5d3c3bd827571b5652c8a912048d0 Mon Sep 17 00:00:00 2001 From: Yohann Boniface Date: Sun, 21 Nov 2021 18:01:40 +0100 Subject: [PATCH 015/134] Update examples/tweet_generator/tweet_generator.py Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> --- examples/tweet_generator/tweet_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index 7f1a098d..e4a3f4d1 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -84,7 +84,7 @@ async def twitter( # {'color': (0, 154, 234), 'text': '@drawbu'} # ] for word in message: - for index, text in enumerate(word.split("\n")): + for index, text in enumerate(word.splitlines()): text += "\n" if index != len(word.split("\n")) - 1 else " " From 7db46ac0b2e67dc073318e74c9f967351cdca56b Mon Sep 17 00:00:00 2001 From: Yohann Boniface Date: Sun, 21 Nov 2021 18:01:54 +0100 Subject: [PATCH 016/134] Update examples/tweet_generator/tweet_generator.py Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> --- examples/tweet_generator/tweet_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index e4a3f4d1..47f4de66 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -43,7 +43,7 @@ async def twitter( # modify profile picture to be circular mask = Image.new("L", (128, 128), 0) draw = ImageDraw.Draw(mask) - draw.ellipse((0, 0) + (128, 128), fill=255) + draw.ellipse((0, 0, 128, 128), fill=255) avatar = ImageOps.fit(avatar, mask.size, centering=(0.5, 0.5)) avatar.putalpha(mask) From 43e56d6a985899ef8392eb3cc5fc0961b78611c0 Mon Sep 17 00:00:00 2001 From: sigmanificient Date: Sun, 21 Nov 2021 18:21:18 +0100 Subject: [PATCH 017/134] Revert "Update examples/tweet_generator/tweet_generator.py" This reverts commit c1ec2749ba6b3a941a8c7639d2d6bf166fd49dd9. --- examples/tweet_generator/tweet_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index 47f4de66..b68552c4 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -73,7 +73,7 @@ async def twitter( ) # write the content of the tweet on the Image - message = "\n".join(message).split() + message = "\n".join(message).split(" ") result = [] # generate a dict to set were the text need to be in different color. From 82203dae236ebabe7b7f872fe7ce7ea1b275c2c0 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Sun, 21 Nov 2021 16:21:31 -0500 Subject: [PATCH 018/134] :sparkles: implemented more command types --- pincer/commands/__init__.py | 5 +- pincer/commands/arg_types.py | 54 ++++++++++++--- pincer/commands/commands.py | 127 +++++++++------------------------- pincer/objects/app/command.py | 18 ++--- pincer/utils/types.py | 2 +- 5 files changed, 84 insertions(+), 122 deletions(-) diff --git a/pincer/commands/__init__.py b/pincer/commands/__init__.py index 182ba456..ef255937 100644 --- a/pincer/commands/__init__.py +++ b/pincer/commands/__init__.py @@ -2,9 +2,8 @@ # Full MIT License can be found in `LICENSE` at the project root. from .commands import command, ChatCommandHandler -from .arg_types import CommandArg, Description, Name, OptionalArg +from .arg_types import CommandArg, Description __all__ = [ - "command", "ChatCommandHandler", "CommandArg", "Description", "Name", - "OptionalArg" + "command", "ChatCommandHandler", "CommandArg", "Description" ] diff --git a/pincer/commands/arg_types.py b/pincer/commands/arg_types.py index da862dd8..c000b045 100644 --- a/pincer/commands/arg_types.py +++ b/pincer/commands/arg_types.py @@ -1,7 +1,10 @@ # Copyright Pincer 2021-Present # Full MIT License can be found in `LICENSE` at the project root. -from typing import Any, Tuple, Union +from typing import Any, List, Tuple, Union, T + +from ..utils.types import MISSING +from ..objects.app.command import AppCommandOptionChoice class _CommandTypeMeta(type): @@ -12,24 +15,57 @@ def __getitem__(cls, args: Union[Tuple, Any]): return cls(*args) -"""Valid for all Command Types""" - - class CommandArg(metaclass=_CommandTypeMeta): + """Holds all application command options""" + def __init__(self, command_type, *args) -> None: self.command_type = command_type self.modifiers = args + def get_arg(self, arg_type: T) -> T: + for arg in self.modifiers: + if type(arg) == arg_type: + return arg.get_payload() + + return MISSING + class Description(metaclass=_CommandTypeMeta): + """Represents the description application command option type""" + def __init__(self, desc) -> None: - self.desc = desc + self.desc = str(desc) + def get_payload(self) -> str: + return self.desc -class Name(metaclass=_CommandTypeMeta): - def __init__(self, name) -> None: + +class Choice(metaclass=_CommandTypeMeta): + """Represents an application command choice""" + + def __init__(self, name, value) -> None: self.name = name + self.value = value + + +class Choices(metaclass=_CommandTypeMeta): + """Represents the choice application command option type""" + + def __init__(self, *choices) -> None: + self.choices = [] + + for choice in choices: + if isinstance(choice, Choice): + self.choices.append(AppCommandOptionChoice( + name=choice.name, + value=choice.value + )) + continue + self.choices.append(AppCommandOptionChoice( + name=str(choice), + value=choice + )) -class OptionalArg(metaclass=type): - pass + def get_payload(self) -> List[Union[str, int, float]]: + return self.choices diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index 015e7775..43b4468f 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -9,9 +9,10 @@ from copy import deepcopy from functools import partial from inspect import Signature, isasyncgenfunction -from typing import TYPE_CHECKING, get_origin, get_args, Union, Tuple, List +from typing import TYPE_CHECKING, Union, Tuple, List from . import __package__ +from ..commands.arg_types import CommandArg, Description, Choices from ..utils.snowflake import Snowflake from ..exceptions import ( CommandIsNotCoroutine, @@ -35,7 +36,6 @@ from ..objects.app import ( AppCommandOptionType, AppCommandOption, - AppCommandOptionChoice, ClientCommandStructure, AppCommandType, ) @@ -155,7 +155,7 @@ async def test_command( Annotations must consist of name and value """ # noqa: E501 - + if func is None: return partial( command, @@ -223,115 +223,50 @@ async def test_command( # ctx is type MessageContext but should not be included in the # slash command - if annotation == MessageContext: + if annotation == MessageContext and idx == 1: return - argument_description: Optional[str] = None - choices: List[AppCommandOptionChoice] = [] - - if isinstance(annotation, str): - TypeCache() - annotation = eval(annotation, TypeCache.cache, globals()) - - if isinstance(annotation, Descripted): - argument_description = annotation.description - annotation = annotation.key - - if len(argument_description) > 100: - raise CommandDescriptionTooLong( - f"Tuple annotation `{annotation}` on parameter " - f"`{param}` in command `{cmd}` (`{func.__name__}`), " - "argument description too long. (maximum length is 100 " - "characters)" + if type(annotation) != CommandArg: + if annotation in _options_type_link: + options.append( + AppCommandOption( + type=_options_type_link[annotation], + name=param, + description="Description not set", + required=required + ) ) + continue - if get_origin(annotation) is Union: - args = get_args(annotation) - if type(None) in args: - required = False - - # Do NOT use isinstance as this is a comparison between - # two values of the type type and isinstance does NOT - # work here. - union_args = [t for t in args if t is not type(None)] - - annotation = ( - get_index(union_args, 0) - if len(union_args) == 1 - else Tuple[List] + # TODO: Write better exception + raise InvalidArgumentAnnotation( + "Type must be CommandArg or other valid type" ) - if get_origin(annotation) is Choices: - args = get_args(annotation) + command_type = _options_type_link[annotation.command_type] + argument_description = annotation.get_arg( + Description) or "Description not set" + choices = annotation.get_arg( + Choices) or MISSING - if len(args) > 25: + for choice in choices: + if isinstance(choice.value, int) and annotation.command_type == float: + continue + if not isinstance(choice.value, annotation.command_type): raise InvalidArgumentAnnotation( - f"Choices/Literal annotation `{annotation}` on " - f"parameter `{param}` in command `{cmd}` " - f"(`{func.__name__}`) amount exceeds limit of 25 items!" - ) - - choice_type = type(args[0]) - - if choice_type is Descripted: - choice_type = type(args[0].key) - - for choice in args: - choice_description = choice - if isinstance(choice, Descripted): - choice_description = choice.description - choice = choice.key - - if choice_type is tuple: - choice_type = type(choice) - - if type(choice) not in choice_value_types: - # Properly get all the names of the types - valid_types = list( - map(lambda x: x.__name__, choice_value_types) - ) - raise InvalidArgumentAnnotation( - f"Choices/Literal annotation `{annotation}` on " - f"parameter `{param}` in command `{cmd}` " - f"(`{func.__name__}`), invalid type received. " - "Value must be a member of " - f"{', '.join(valid_types)} but " - f"{type(choice).__name__} was given!" - ) - elif not isinstance(choice, choice_type): - raise InvalidArgumentAnnotation( - f"Choices/Literal annotation `{annotation}` on " - f"parameter `{param}` in command `{cmd}` " - f"(`{func.__name__}`), all values must be of the " - "same type!" - ) - - choices.append( - AppCommandOptionChoice( - name=choice_description, value=choice - ) + "Choice value must match the command type" ) - annotation = choice_type - - param_type = _options_type_link.get(annotation) - - if not param_type: - raise InvalidArgumentAnnotation( - f"Annotation `{annotation}` on parameter " - f"`{param}` in command `{cmd}` (`{func.__name__}`) is not " - "a valid type." - ) - options.append( AppCommandOption( - type=param_type, + type=command_type, name=param, - description=argument_description or "Description not set", + description=argument_description, required=required, - choices=choices or MISSING, + choices=choices, ) ) + print(options[0].to_dict()) ChatCommandHandler.register[cmd] = ClientCommandStructure( call=func, diff --git a/pincer/objects/app/command.py b/pincer/objects/app/command.py index beec9663..12627e37 100644 --- a/pincer/objects/app/command.py +++ b/pincer/objects/app/command.py @@ -55,6 +55,11 @@ class AppCommandOptionChoice(APIObject): name: str value: choice_value_types + def __post_init__(self): + # Default serialization causes too many issues with Union + # It isn't needed here anyway + return + @dataclass class AppCommandOption(APIObject): @@ -86,19 +91,6 @@ class AppCommandOption(APIObject): choices: APINullable[List[AppCommandOptionChoice]] = MISSING options: APINullable[List[AppCommandOption]] = MISSING - def __post_init__(self): - self.type = AppCommandOptionType(self.type) - self.choices = convert( - self.choices, - AppCommandOptionChoice.from_dict, - AppCommandOptionChoice - ) - self.options = convert( - self.options, - AppCommandOption.from_dict, - AppCommandOption - ) - @dataclass class AppCommand(APIObject): diff --git a/pincer/utils/types.py b/pincer/utils/types.py index 09c091be..115edd86 100644 --- a/pincer/utils/types.py +++ b/pincer/utils/types.py @@ -31,7 +31,7 @@ def __bool__(self) -> bool: # Represents a coroutine. Coro = TypeVar("Coro", bound=Callable[..., Coroutine[Any, Any, Any]]) -choice_value_types = (str, int, float) +choice_value_types = Union[str, int, float] CheckFunction = Optional[Callable[[Any], bool]] From 6359e7d2bf6a7d4cd2c18576c952eb66886ae47c Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Sun, 21 Nov 2021 16:54:04 -0500 Subject: [PATCH 019/134] :sparkles: added MaxValue, MinValue, and ChannelTypes --- pincer/commands/arg_types.py | 30 ++++++++++++++++++++++++ pincer/commands/commands.py | 43 ++++++++++++++++++++++++++++------- pincer/objects/app/command.py | 11 ++++++++- 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/pincer/commands/arg_types.py b/pincer/commands/arg_types.py index c000b045..532e19d4 100644 --- a/pincer/commands/arg_types.py +++ b/pincer/commands/arg_types.py @@ -69,3 +69,33 @@ def __init__(self, *choices) -> None: def get_payload(self) -> List[Union[str, int, float]]: return self.choices + + +class ChannelTypes(metaclass=_CommandTypeMeta): + """Represents the channel types application command option type""" + + def __init__(self, *types) -> None: + self.types = types + + def get_payload(self): + return self.types + + +class MaxValue(metaclass=_CommandTypeMeta): + """Represents the channel types application command option type""" + + def __init__(self, max_value) -> None: + self.max_value = max_value + + def get_payload(self): + return self.max_value + + +class MinValue(metaclass=_CommandTypeMeta): + """Represents the channel types application command option type""" + + def __init__(self, min_value) -> None: + self.min_value = min_value + + def get_payload(self): + return self.min_value diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index 43b4468f..2b6655bc 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -2,6 +2,7 @@ # Full MIT License can be found in `LICENSE` at the project root. from __future__ import annotations +from enum import auto import logging import re @@ -12,7 +13,7 @@ from typing import TYPE_CHECKING, Union, Tuple, List from . import __package__ -from ..commands.arg_types import CommandArg, Description, Choices +from ..commands.arg_types import ChannelTypes, CommandArg, Description, Choices, MaxValue, MinValue from ..utils.snowflake import Snowflake from ..exceptions import ( CommandIsNotCoroutine, @@ -247,14 +248,38 @@ async def test_command( argument_description = annotation.get_arg( Description) or "Description not set" choices = annotation.get_arg( - Choices) or MISSING + Choices) - for choice in choices: - if isinstance(choice.value, int) and annotation.command_type == float: - continue - if not isinstance(choice.value, annotation.command_type): + if choices is not MISSING and annotation.command_type not in [int, float, str]: + raise InvalidArgumentAnnotation( + "Choice type is only allowed for str, int, and float" + ) + if choices is not MISSING: + for choice in choices: + if isinstance(choice.value, int) and annotation.command_type == float: + continue + if not isinstance(choice.value, annotation.command_type): + raise InvalidArgumentAnnotation( + "Choice value must match the command type" + ) + + cannel_types = annotation.get_arg(ChannelTypes) + if cannel_types is not MISSING and annotation.command_type != Channel: + raise InvalidArgumentAnnotation( + "ChannelTypes is only available for Channel") + + max_value = annotation.get_arg(MaxValue) + min_value = annotation.get_arg(MinValue) + + for i, value in enumerate((min_value, max_value)): + if ( + value is not MISSING + and annotation.command_type != int + and annotation.command_type != float + ): + t = ("MinValue", "MaxValue") raise InvalidArgumentAnnotation( - "Choice value must match the command type" + f"{t[i]} is only available for int and float" ) options.append( @@ -264,9 +289,11 @@ async def test_command( description=argument_description, required=required, choices=choices, + channel_types=cannel_types, + max_value=max_value, + min_value=min_value, ) ) - print(options[0].to_dict()) ChatCommandHandler.register[cmd] = ClientCommandStructure( call=func, diff --git a/pincer/objects/app/command.py b/pincer/objects/app/command.py index 12627e37..7049ce81 100644 --- a/pincer/objects/app/command.py +++ b/pincer/objects/app/command.py @@ -6,7 +6,9 @@ from dataclasses import dataclass from typing import List, Union, TYPE_CHECKING + from .command_types import AppCommandOptionType, AppCommandType +from ...objects.guild.channel import ChannelType from ...utils.api_object import APIObject from ...utils.conversion import convert from ...utils.snowflake import Snowflake @@ -87,10 +89,17 @@ class AppCommandOption(APIObject): name: str description: str - required: APINullable[bool] = False + required: APINullable[bool] = MISSING + autocomplete: APINullable[bool] = MISSING choices: APINullable[List[AppCommandOptionChoice]] = MISSING options: APINullable[List[AppCommandOption]] = MISSING + channel_types: APINullable[List[ChannelType]] = MISSING + min_value: APINullable[Union[int, float]] = MISSING + max_value: APINullable[Union[int, float]] = MISSING + def __post_init__(self): + # Auto conversion is not needed for this class + pass @dataclass class AppCommand(APIObject): From fc6f9cddb2b00a9ef1ca675b9a0d9245ddae3f59 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Sun, 21 Nov 2021 17:22:40 -0500 Subject: [PATCH 020/134] :bug: optional arg system now works with default values --- pincer/commands/commands.py | 10 ++++++---- pincer/middleware/interaction_create.py | 4 +++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index 2b6655bc..315b9cac 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -9,7 +9,7 @@ from asyncio import iscoroutinefunction, gather from copy import deepcopy from functools import partial -from inspect import Signature, isasyncgenfunction +from inspect import Signature, isasyncgenfunction, _empty from typing import TYPE_CHECKING, Union, Tuple, List from . import __package__ @@ -204,8 +204,8 @@ async def test_command( f"registered by `{reg.call.__name__}`." ) - sig, params = get_signature_and_params(func) - pass_context = should_pass_ctx(sig, params) + signature, params = get_signature_and_params(func) + pass_context = should_pass_ctx(signature, params) if len(params) > (25 + pass_context): raise TooManyArguments( @@ -220,7 +220,9 @@ async def test_command( if idx == 0 and pass_context: continue - annotation, required = sig[param].annotation, True + sig = signature[param] + + annotation, required = sig.annotation, sig.default is _empty # ctx is type MessageContext but should not be included in the # slash command diff --git a/pincer/middleware/interaction_create.py b/pincer/middleware/interaction_create.py index 65098b09..32e80917 100644 --- a/pincer/middleware/interaction_create.py +++ b/pincer/middleware/interaction_create.py @@ -88,7 +88,9 @@ async def interaction_handler( """ self.throttler.handle(context) - defaults = {param: None for param in get_params(command)} + sig, params = get_signature_and_params(command) + + defaults = {key: value.default for key, value in sig.items()} params = {} if interaction.data.options is not MISSING: From 0ac074f154492045142b98b3b315229f1fbf6749 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Sun, 21 Nov 2021 19:11:52 -0500 Subject: [PATCH 021/134] :memo: added command args to docs --- docs/api.rst | 1 + docs/pincer.rst | 16 ---------------- pincer/commands/__init__.py | 7 +++++-- pincer/commands/commands.py | 23 ++++++++++++++++++----- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index e4b9c5d1..36e88733 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -9,6 +9,7 @@ Subpages pincer pincer.core + pincer.commands pincer.middleware pincer.objects pincer.utils \ No newline at end of file diff --git a/docs/pincer.rst b/docs/pincer.rst index 5b2f5924..bc06210f 100644 --- a/docs/pincer.rst +++ b/docs/pincer.rst @@ -17,22 +17,6 @@ Client .. automethod:: Client.event() :decorator: -Commands --------- - -command -~~~~~~~ - -.. autofunction:: command - :decorator: - -ChatCommandHandler -~~~~~~~~~~~~~~~~~~ - -.. attributetable:: ChatCommandHandler - -.. autoclass:: ChatCommandHandler() - Exceptions ---------- diff --git a/pincer/commands/__init__.py b/pincer/commands/__init__.py index ef255937..de338670 100644 --- a/pincer/commands/__init__.py +++ b/pincer/commands/__init__.py @@ -2,8 +2,11 @@ # Full MIT License can be found in `LICENSE` at the project root. from .commands import command, ChatCommandHandler -from .arg_types import CommandArg, Description +from .arg_types import ( + CommandArg, Description, Choice, Choices, ChannelTypes, MaxValue, MinValue +) __all__ = [ - "command", "ChatCommandHandler", "CommandArg", "Description" + "command", "ChatCommandHandler", "CommandArg", "Description", "Choice", + "Choices", "ChannelTypes", "MaxValue", "MinValue" ] diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index 315b9cac..67fa7da7 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -102,8 +102,16 @@ async def test_command( self, ctx, amount: int, - name: Descripted[str, "ah yes"], - letter: Choices["a", "b", "c"] + name: CommandArg[ + str, + Description["Do something cool"], + Choices[Choice["first value", 1], 5] + ], + optional_int: CommandArg[ + int, + MinValue[10], + MaxValue[100], + ] = 50 ): return Message( f"You chose {amount}, {name}, {letter}", @@ -113,9 +121,14 @@ async def test_command( References from above: :class:`~client.Client`, :class:`~objects.message.message.Message`, - :class:`~utils.types.Choices`, - :class:`~utils.types.Descripted`, - :class:`~objects.app.interactions.InteractionFlags` + :class:`~pincer.objects.app.interaction_flags.InteractionFlags`, + :class:`~pincer.commands.arg_types.Choices`, + :class:`~pincer.commands.arg_types.Choice`, + :class:`~pincer.commands.arg_types.CommandArg`, + :class:`~pincer.commands.arg_types.Description`, + :class:`~pincer.commands.arg_types.MinValue`, + :class:`~pincer.commands.arg_types.MaxValue` + Parameters ---------- From 5aae9214df1d3e60fcc9fea41aa8ad4a87462931 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Sun, 21 Nov 2021 23:30:41 -0500 Subject: [PATCH 022/134] :memo: autodoc for command args --- docs/pincer.commands.rst | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 docs/pincer.commands.rst diff --git a/docs/pincer.commands.rst b/docs/pincer.commands.rst new file mode 100644 index 00000000..b27df211 --- /dev/null +++ b/docs/pincer.commands.rst @@ -0,0 +1,39 @@ + +.. currentmodule:: pincer.commands + +Pincer Commands Module +================== + +Commands +-------- + +command +~~~~~~~ + +.. autofunction:: command + :decorator: + +Command Types +------------- + +.. autoclass:: CommandArg() + :exclude-members: get_payload +.. autoclass:: Description() + :exclude-members: get_payload +.. autoclass:: Choice() + :exclude-members: get_payload +.. autoclass:: Choices() + :exclude-members: get_payload +.. autoclass:: ChannelTypes() + :exclude-members: get_payload +.. autoclass:: MaxValue() + :exclude-members: get_payload +.. autoclass:: MinValue() + :exclude-members: get_payload + +ChatCommandHandler +~~~~~~~~~~~~~~~~~~ + +.. attributetable:: ChatCommandHandler + +.. autoclass:: ChatCommandHandler() \ No newline at end of file From d49f762455ae3746d4af0173cd25d19b9e044e7a Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Mon, 22 Nov 2021 15:26:46 -0500 Subject: [PATCH 023/134] :construction: started command types --- docs/pincer.commands.rst | 26 ++++-------- pincer/commands/__init__.py | 5 ++- pincer/commands/commands.py | 79 +++++++++++++++++++++++++++++++++-- pincer/objects/app/command.py | 3 +- 4 files changed, 88 insertions(+), 25 deletions(-) diff --git a/docs/pincer.commands.rst b/docs/pincer.commands.rst index b27df211..2a57e023 100644 --- a/docs/pincer.commands.rst +++ b/docs/pincer.commands.rst @@ -13,27 +13,15 @@ command .. autofunction:: command :decorator: -Command Types -------------- - -.. autoclass:: CommandArg() - :exclude-members: get_payload -.. autoclass:: Description() - :exclude-members: get_payload -.. autoclass:: Choice() - :exclude-members: get_payload -.. autoclass:: Choices() - :exclude-members: get_payload -.. autoclass:: ChannelTypes() - :exclude-members: get_payload -.. autoclass:: MaxValue() - :exclude-members: get_payload -.. autoclass:: MinValue() - :exclude-members: get_payload - ChatCommandHandler ~~~~~~~~~~~~~~~~~~ .. attributetable:: ChatCommandHandler -.. autoclass:: ChatCommandHandler() \ No newline at end of file +.. autoclass:: ChatCommandHandler() + +Command Types +------------- + +.. automodule:: pincer.commands.arg_types + :members: diff --git a/pincer/commands/__init__.py b/pincer/commands/__init__.py index de338670..d63e4f77 100644 --- a/pincer/commands/__init__.py +++ b/pincer/commands/__init__.py @@ -1,12 +1,13 @@ # Copyright Pincer 2021-Present # Full MIT License can be found in `LICENSE` at the project root. -from .commands import command, ChatCommandHandler +from .commands import command, user_command, message_command, ChatCommandHandler from .arg_types import ( CommandArg, Description, Choice, Choices, ChannelTypes, MaxValue, MinValue ) __all__ = [ "command", "ChatCommandHandler", "CommandArg", "Description", "Choice", - "Choices", "ChannelTypes", "MaxValue", "MinValue" + "Choices", "ChannelTypes", "MaxValue", "MinValue", "user_command", + "message_command" ] diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index 67fa7da7..0dc05242 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -2,7 +2,6 @@ # Full MIT License can be found in `LICENSE` at the project root. from __future__ import annotations -from enum import auto import logging import re @@ -78,6 +77,79 @@ def command( cooldown: Optional[int] = 0, cooldown_scale: Optional[float] = 60, cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, +): + return register_command( + func=func, + app_command_type=AppCommandType.CHAT_INPUT, + name=name, + description=description, + enable_default=enable_default, + guild=guild, + cooldown=cooldown, + cooldown_scale=cooldown_scale, + cooldown_scope=cooldown_scope + ) + + +def user_command( + func=None, + *, + name: Optional[str] = None, + description: Optional[str] = "Description not set", + enable_default: Optional[bool] = True, + guild: Union[Snowflake, int, str] = None, + cooldown: Optional[int] = 0, + cooldown_scale: Optional[float] = 60, + cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, +): + return register_command( + func=func, + app_command_type=AppCommandType.USER, + name=name, + description=description, + enable_default=enable_default, + guild=guild, + cooldown=cooldown, + cooldown_scale=cooldown_scale, + cooldown_scope=cooldown_scope + ) + + +def message_command( + func=None, + *, + name: Optional[str] = None, + description: Optional[str] = "Description not set", + enable_default: Optional[bool] = True, + guild: Union[Snowflake, int, str] = None, + cooldown: Optional[int] = 0, + cooldown_scale: Optional[float] = 60, + cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, +): + return register_command( + func=func, + app_command_type=AppCommandType.USER, + name=name, + description=description, + enable_default=enable_default, + guild=guild, + cooldown=cooldown, + cooldown_scale=cooldown_scale, + cooldown_scope=cooldown_scope + ) + + +def register_command( + func=None, + *, + app_command_type: AppCommandType = None, + name: Optional[str] = None, + description: Optional[str] = "Description not set", + enable_default: Optional[bool] = True, + guild: Union[Snowflake, int, str] = None, + cooldown: Optional[int] = 0, + cooldown_scale: Optional[float] = 60, + cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, ): """A decorator to create a command to register and respond to with the discord API from a function. @@ -172,8 +244,9 @@ async def test_command( if func is None: return partial( - command, + register_command, name=name, + app_command_type=app_command_type, description=description, enable_default=enable_default, guild=guild, @@ -318,7 +391,7 @@ async def test_command( app=AppCommand( name=cmd, description=description, - type=AppCommandType.CHAT_INPUT, + type=app_command_type, default_permission=enable_default, options=options, guild_id=guild_id, diff --git a/pincer/objects/app/command.py b/pincer/objects/app/command.py index 7049ce81..3bf50cf7 100644 --- a/pincer/objects/app/command.py +++ b/pincer/objects/app/command.py @@ -101,6 +101,7 @@ def __post_init__(self): # Auto conversion is not needed for this class pass + @dataclass class AppCommand(APIObject): """Represents a Discord Application Command object @@ -163,7 +164,7 @@ def __post_init__(self): ) self.guild_id = convert(self.guild_id, Snowflake.from_string) - self.options = [] if self.options is MISSING else self.options + self.options = [] if self.options is MISSING and self.type == AppCommandType.MESSAGE else self.options def __eq__(self, other: Union[AppCommand, ClientCommandStructure]): if isinstance(other, ClientCommandStructure): From 9350bd2a2cfd367abb7f374f8ed059b9f83b05c6 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Mon, 22 Nov 2021 15:37:01 -0500 Subject: [PATCH 024/134] :recycle: moved where command args are found --- pincer/commands/commands.py | 205 +++++++++++++++++++----------------- 1 file changed, 110 insertions(+), 95 deletions(-) diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index 0dc05242..9d047411 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -78,6 +78,112 @@ def command( cooldown_scale: Optional[float] = 60, cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, ): + if func is None: + return partial( + command, + name=name, + description=description, + enable_default=enable_default, + guild=guild, + cooldown=cooldown, + cooldown_scale=cooldown_scale, + cooldown_scope=cooldown_scope, + ) + + options: List[AppCommandOption] = [] + + signature, params = get_signature_and_params(func) + pass_context = should_pass_ctx(signature, params) + + if len(params) > (25 + pass_context): + cmd = name or func.__name__ + raise TooManyArguments( + f"Command `{cmd}` (`{func.__name__}`) can only have 25 " + f"arguments (excluding the context and self) yet {len(params)} " + "were provided!" + ) + + for idx, param in enumerate(params): + if idx == 0 and pass_context: + continue + + sig = signature[param] + + annotation, required = sig.annotation, sig.default is _empty + + # ctx is type MessageContext but should not be included in the + # slash command + if annotation == MessageContext and idx == 1: + return + + if type(annotation) != CommandArg: + if annotation in _options_type_link: + options.append( + AppCommandOption( + type=_options_type_link[annotation], + name=param, + description="Description not set", + required=required + ) + ) + continue + + # TODO: Write better exception + raise InvalidArgumentAnnotation( + "Type must be CommandArg or other valid type" + ) + + command_type = _options_type_link[annotation.command_type] + argument_description = annotation.get_arg( + Description) or "Description not set" + choices = annotation.get_arg( + Choices) + + if choices is not MISSING and annotation.command_type not in [int, float, str]: + raise InvalidArgumentAnnotation( + "Choice type is only allowed for str, int, and float" + ) + if choices is not MISSING: + for choice in choices: + if isinstance(choice.value, int) and annotation.command_type == float: + continue + if not isinstance(choice.value, annotation.command_type): + raise InvalidArgumentAnnotation( + "Choice value must match the command type" + ) + + cannel_types = annotation.get_arg(ChannelTypes) + if cannel_types is not MISSING and annotation.command_type != Channel: + raise InvalidArgumentAnnotation( + "ChannelTypes is only available for Channel") + + max_value = annotation.get_arg(MaxValue) + min_value = annotation.get_arg(MinValue) + + for i, value in enumerate((min_value, max_value)): + if ( + value is not MISSING + and annotation.command_type != int + and annotation.command_type != float + ): + t = ("MinValue", "MaxValue") + raise InvalidArgumentAnnotation( + f"{t[i]} is only available for int and float" + ) + + options.append( + AppCommandOption( + type=command_type, + name=param, + description=argument_description, + required=required, + choices=choices, + channel_types=cannel_types, + max_value=max_value, + min_value=min_value, + ) + ) + return register_command( func=func, app_command_type=AppCommandType.CHAT_INPUT, @@ -87,7 +193,8 @@ def command( guild=guild, cooldown=cooldown, cooldown_scale=cooldown_scale, - cooldown_scope=cooldown_scope + cooldown_scope=cooldown_scope, + command_options=options ) @@ -150,6 +257,7 @@ def register_command( cooldown: Optional[int] = 0, cooldown_scale: Optional[float] = 60, cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, + command_options=MISSING ): """A decorator to create a command to register and respond to with the discord API from a function. @@ -290,99 +398,6 @@ async def test_command( f"registered by `{reg.call.__name__}`." ) - signature, params = get_signature_and_params(func) - pass_context = should_pass_ctx(signature, params) - - if len(params) > (25 + pass_context): - raise TooManyArguments( - f"Command `{cmd}` (`{func.__name__}`) can only have 25 " - f"arguments (excluding the context and self) yet {len(params)} " - "were provided!" - ) - - options: List[AppCommandOption] = [] - - for idx, param in enumerate(params): - if idx == 0 and pass_context: - continue - - sig = signature[param] - - annotation, required = sig.annotation, sig.default is _empty - - # ctx is type MessageContext but should not be included in the - # slash command - if annotation == MessageContext and idx == 1: - return - - if type(annotation) != CommandArg: - if annotation in _options_type_link: - options.append( - AppCommandOption( - type=_options_type_link[annotation], - name=param, - description="Description not set", - required=required - ) - ) - continue - - # TODO: Write better exception - raise InvalidArgumentAnnotation( - "Type must be CommandArg or other valid type" - ) - - command_type = _options_type_link[annotation.command_type] - argument_description = annotation.get_arg( - Description) or "Description not set" - choices = annotation.get_arg( - Choices) - - if choices is not MISSING and annotation.command_type not in [int, float, str]: - raise InvalidArgumentAnnotation( - "Choice type is only allowed for str, int, and float" - ) - if choices is not MISSING: - for choice in choices: - if isinstance(choice.value, int) and annotation.command_type == float: - continue - if not isinstance(choice.value, annotation.command_type): - raise InvalidArgumentAnnotation( - "Choice value must match the command type" - ) - - cannel_types = annotation.get_arg(ChannelTypes) - if cannel_types is not MISSING and annotation.command_type != Channel: - raise InvalidArgumentAnnotation( - "ChannelTypes is only available for Channel") - - max_value = annotation.get_arg(MaxValue) - min_value = annotation.get_arg(MinValue) - - for i, value in enumerate((min_value, max_value)): - if ( - value is not MISSING - and annotation.command_type != int - and annotation.command_type != float - ): - t = ("MinValue", "MaxValue") - raise InvalidArgumentAnnotation( - f"{t[i]} is only available for int and float" - ) - - options.append( - AppCommandOption( - type=command_type, - name=param, - description=argument_description, - required=required, - choices=choices, - channel_types=cannel_types, - max_value=max_value, - min_value=min_value, - ) - ) - ChatCommandHandler.register[cmd] = ClientCommandStructure( call=func, cooldown=cooldown, @@ -393,7 +408,7 @@ async def test_command( description=description, type=app_command_type, default_permission=enable_default, - options=options, + options=command_options, guild_id=guild_id, ), ) From a909f2f4b8cd43d82f34366a15a6f8002e4b911d Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Mon, 22 Nov 2021 16:50:48 -0500 Subject: [PATCH 025/134] :sparkles: added missing application command types --- pincer/commands/commands.py | 10 +++------- pincer/objects/message/user_message.py | 1 + 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index 9d047411..efc45a43 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -202,7 +202,6 @@ def user_command( func=None, *, name: Optional[str] = None, - description: Optional[str] = "Description not set", enable_default: Optional[bool] = True, guild: Union[Snowflake, int, str] = None, cooldown: Optional[int] = 0, @@ -213,7 +212,6 @@ def user_command( func=func, app_command_type=AppCommandType.USER, name=name, - description=description, enable_default=enable_default, guild=guild, cooldown=cooldown, @@ -226,7 +224,6 @@ def message_command( func=None, *, name: Optional[str] = None, - description: Optional[str] = "Description not set", enable_default: Optional[bool] = True, guild: Union[Snowflake, int, str] = None, cooldown: Optional[int] = 0, @@ -235,9 +232,8 @@ def message_command( ): return register_command( func=func, - app_command_type=AppCommandType.USER, + app_command_type=AppCommandType.MESSAGE, name=name, - description=description, enable_default=enable_default, guild=guild, cooldown=cooldown, @@ -251,7 +247,7 @@ def register_command( *, app_command_type: AppCommandType = None, name: Optional[str] = None, - description: Optional[str] = "Description not set", + description: Optional[str] = MISSING, enable_default: Optional[bool] = True, guild: Union[Snowflake, int, str] = None, cooldown: Optional[int] = 0, @@ -386,7 +382,7 @@ async def test_command( "contains a non valid guild id." ) - if len(description) > 100: + if description and len(description) > 100: raise CommandDescriptionTooLong( f"Command `{cmd}` (`{func.__name__}`) its description exceeds " "the 100 character limit." diff --git a/pincer/objects/message/user_message.py b/pincer/objects/message/user_message.py index 8c4593cd..d75c24aa 100644 --- a/pincer/objects/message/user_message.py +++ b/pincer/objects/message/user_message.py @@ -226,6 +226,7 @@ class MessageType(IntEnum): THREAD_STARTER_MESSAGE = 21 GUILD_INVITE_REMINDER = 22 + CONTEXT_MENU_COMMAND = 23 @dataclass From f6c3b245cff839157cf69da57c072a0883d6c3dc Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Mon, 22 Nov 2021 17:17:22 -0500 Subject: [PATCH 026/134] :sparkles: dict deconstruction to APIObject --- pincer/commands/commands.py | 230 ++++++++++++++++++++++-------------- pincer/utils/api_object.py | 6 +- 2 files changed, 148 insertions(+), 88 deletions(-) diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index efc45a43..c974994a 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -78,6 +78,97 @@ def command( cooldown_scale: Optional[float] = 60, cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, ): + """A decorator to slash create a command to register and respond to + with the discord API from a function. + + str - String + int - Integer + bool - Boolean + float - Number + pincer.objects.User - User + pincer.objects.Channel - Channel + pincer.objects.Role - Role + Mentionable is not implemented + + .. code-block:: python3 + + class Bot(Client): + @command( + name="test", + description="placeholder" + ) + async def test_command( + self, + ctx: MessageContext, + amount: int, + name: CommandArg[ + str, + Description["Do something cool"], + Choices[Choice["first value", 1], 5] + ], + optional_int: CommandArg[ + int, + MinValue[10], + MaxValue[100], + ] = 50 + ): + return Message( + f"You chose {amount}, {name}, {letter}", + flags=InteractionFlags.EPHEMERAL + ) + + References from above: + :class:`~client.Client`, + :class:`~objects.message.message.Message`, + :class:`~objects.message.context.MessageContext`, + :class:`~pincer.objects.app.interaction_flags.InteractionFlags`, + :class:`~pincer.commands.arg_types.Choices`, + :class:`~pincer.commands.arg_types.Choice`, + :class:`~pincer.commands.arg_types.CommandArg`, + :class:`~pincer.commands.arg_types.Description`, + :class:`~pincer.commands.arg_types.MinValue`, + :class:`~pincer.commands.arg_types.MaxValue` + + + Parameters + ---------- + name : Optional[:class:`str`] + The name of the command |default| :data:`None` + description : Optional[:class:`str`] + The description of the command |default| ``Description not set`` + enable_default : Optional[:class:`bool`] + Whether the command is enabled by default |default| :data:`True` + guild : Optional[Union[:class:`~pincer.utils.snowflake.Snowflake`, :class:`int`, :class:`str`]] + What guild to add it to (don't specify for global) |default| :data:`None` + cooldown : Optional[:class:`int`] + The amount of times in the cooldown_scale the command can be invoked + |default| ``0`` + cooldown_scale : Optional[:class:`float`] + The 'checking time' of the cooldown |default| ``60`` + cooldown_scope : :class:`~pincer.objects.app.throttle_scope.ThrottleScope` + What type of cooldown strategy to use |default| :attr:`ThrottleScope.USER` + + Raises + ------ + CommandIsNotCoroutine + If the command function is not a coro + InvalidCommandName + If the command name does not follow the regex ``^[\\w-]{1,32}$`` + InvalidCommandGuild + If the guild id is invalid + CommandDescriptionTooLong + Descriptions max 100 characters + If the annotation on an argument is too long (also max 100) + CommandAlreadyRegistered + If the command already exists + TooManyArguments + Max 25 arguments to pass for commands + InvalidArgumentAnnotation + Annotation amount is max 25, + Not a valid argument type, + Annotations must consist of name and value + """ + # noqa: E501 if func is None: return partial( command, @@ -208,110 +299,31 @@ def user_command( cooldown_scale: Optional[float] = 60, cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, ): - return register_command( - func=func, - app_command_type=AppCommandType.USER, - name=name, - enable_default=enable_default, - guild=guild, - cooldown=cooldown, - cooldown_scale=cooldown_scale, - cooldown_scope=cooldown_scope - ) - - -def message_command( - func=None, - *, - name: Optional[str] = None, - enable_default: Optional[bool] = True, - guild: Union[Snowflake, int, str] = None, - cooldown: Optional[int] = 0, - cooldown_scale: Optional[float] = 60, - cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, -): - return register_command( - func=func, - app_command_type=AppCommandType.MESSAGE, - name=name, - enable_default=enable_default, - guild=guild, - cooldown=cooldown, - cooldown_scale=cooldown_scale, - cooldown_scope=cooldown_scope - ) - - -def register_command( - func=None, - *, - app_command_type: AppCommandType = None, - name: Optional[str] = None, - description: Optional[str] = MISSING, - enable_default: Optional[bool] = True, - guild: Union[Snowflake, int, str] = None, - cooldown: Optional[int] = 0, - cooldown_scale: Optional[float] = 60, - cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, - command_options=MISSING -): - """A decorator to create a command to register and respond to + """A decorator to create a user command to register and respond to with the discord API from a function. - str - String - int - Integer - bool - Boolean - float - Number - pincer.objects.User - User - pincer.objects.Channel - Channel - pincer.objects.Role - Role - Mentionable is not implemented - .. code-block:: python3 class Bot(Client): - @command( - name="test", - description="placeholder" - ) - async def test_command( + @user_command + async def test_user_command( self, - ctx, - amount: int, - name: CommandArg[ - str, - Description["Do something cool"], - Choices[Choice["first value", 1], 5] - ], - optional_int: CommandArg[ - int, - MinValue[10], - MaxValue[100], - ] = 50 + ctx: MessageContext, ): return Message( - f"You chose {amount}, {name}, {letter}", - flags=InteractionFlags.EPHEMERAL + f"The messages author is {}" ) References from above: :class:`~client.Client`, :class:`~objects.message.message.Message`, - :class:`~pincer.objects.app.interaction_flags.InteractionFlags`, - :class:`~pincer.commands.arg_types.Choices`, - :class:`~pincer.commands.arg_types.Choice`, - :class:`~pincer.commands.arg_types.CommandArg`, - :class:`~pincer.commands.arg_types.Description`, - :class:`~pincer.commands.arg_types.MinValue`, - :class:`~pincer.commands.arg_types.MaxValue` + :class:`~objects.message.context.MessageContext`, Parameters ---------- name : Optional[:class:`str`] The name of the command |default| :data:`None` - description : Optional[:class:`str`] - The description of the command |default| ``Description not set`` enable_default : Optional[:class:`bool`] Whether the command is enabled by default |default| :data:`True` guild : Optional[Union[:class:`~pincer.utils.snowflake.Snowflake`, :class:`int`, :class:`str`]] @@ -337,15 +349,59 @@ async def test_command( If the annotation on an argument is too long (also max 100) CommandAlreadyRegistered If the command already exists - TooManyArguments - Max 25 arguments to pass for commands InvalidArgumentAnnotation Annotation amount is max 25, Not a valid argument type, Annotations must consist of name and value """ # noqa: E501 + return register_command( + func=func, + app_command_type=AppCommandType.USER, + name=name, + enable_default=enable_default, + guild=guild, + cooldown=cooldown, + cooldown_scale=cooldown_scale, + cooldown_scope=cooldown_scope + ) + + +def message_command( + func=None, + *, + name: Optional[str] = None, + enable_default: Optional[bool] = True, + guild: Union[Snowflake, int, str] = None, + cooldown: Optional[int] = 0, + cooldown_scale: Optional[float] = 60, + cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, +): + return register_command( + func=func, + app_command_type=AppCommandType.MESSAGE, + name=name, + enable_default=enable_default, + guild=guild, + cooldown=cooldown, + cooldown_scale=cooldown_scale, + cooldown_scope=cooldown_scope + ) + +def register_command( + func=None, + *, + app_command_type: AppCommandType = None, + name: Optional[str] = None, + description: Optional[str] = MISSING, + enable_default: Optional[bool] = True, + guild: Union[Snowflake, int, str] = None, + cooldown: Optional[int] = 0, + cooldown_scale: Optional[float] = 60, + cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, + command_options=MISSING +): if func is None: return partial( register_command, diff --git a/pincer/utils/api_object.py b/pincer/utils/api_object.py index 79bea7b8..803dfcf2 100644 --- a/pincer/utils/api_object.py +++ b/pincer/utils/api_object.py @@ -210,7 +210,11 @@ def __post_init__(self): self.__attr_convert(attr_item, classes[0]) for attr_item in attr_gotten ] - + elif tp == dict and attr_gotten and (classes := get_args(types[0])): + attr_value = { + key: self.__attr_convert(value, classes[1]) + for key, value in attr_gotten.items() + } else: attr_value = self.__attr_convert(attr_gotten, specific_tp) From 793785783bef0d026cd6d4ba5671541df6b68bb5 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Mon, 22 Nov 2021 17:58:34 -0500 Subject: [PATCH 027/134] :bug: changed interaction_create to more easily allow for args --- pincer/middleware/interaction_create.py | 27 ++++++++++++++++--------- pincer/objects/guild/member.py | 4 ++-- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/pincer/middleware/interaction_create.py b/pincer/middleware/interaction_create.py index 32e80917..37384969 100644 --- a/pincer/middleware/interaction_create.py +++ b/pincer/middleware/interaction_create.py @@ -4,13 +4,14 @@ from __future__ import annotations import logging -from inspect import isasyncgenfunction, getfullargspec +from inspect import isasyncgenfunction, getfullargspec, _empty from typing import Dict, Any from typing import TYPE_CHECKING + from ..commands import ChatCommandHandler from ..core.dispatch import GatewayDispatch -from ..objects import Interaction, MessageContext +from ..objects import Interaction, MessageContext, AppCommandType from ..utils import MISSING, should_pass_cls, Coro, should_pass_ctx from ..utils import get_index from ..utils.conversion import construct_client_dict @@ -28,6 +29,7 @@ async def interaction_response_handler( command: Coro, context: MessageContext, interaction: Interaction, + args: List[Any], kwargs: Dict[str, Any] ): """|coro| @@ -45,16 +47,21 @@ async def interaction_response_handler( \\*\\*kwargs : The arguments to be passed to the command. """ + maybe_self_and_context: List[Any] = [] + if should_pass_cls(command): - cls_keyword = getfullargspec(command).args[0] - kwargs[cls_keyword] = ChatCommandHandler.managers[command.__module__] + maybe_self_and_context.append( + ChatCommandHandler.managers[command.__module__] + ) sig, params = get_signature_and_params(command) if should_pass_ctx(sig, params): - kwargs[params[0]] = context + maybe_self_and_context.append(context) + + args = maybe_self_and_context + args if isasyncgenfunction(command): - message = command(**kwargs) + message = command(*args, **kwargs) async for msg in message: if interaction.has_replied: @@ -62,7 +69,7 @@ async def interaction_response_handler( else: await interaction.reply(msg) else: - message = await command(**kwargs) + message = await command(*args, **kwargs) if not interaction.has_replied: await interaction.reply(message) @@ -90,7 +97,8 @@ async def interaction_handler( sig, params = get_signature_and_params(command) - defaults = {key: value.default for key, value in sig.items()} + defaults = {key: value.default for key, + value in sig.items() if value.default is not _empty} params = {} if interaction.data.options is not MISSING: @@ -98,10 +106,11 @@ async def interaction_handler( opt.name: opt.value for opt in interaction.data.options } + args = [] kwargs = {**defaults, **params} await interaction_response_handler( - self, command, context, interaction, kwargs + self, command, context, interaction, args, kwargs ) diff --git a/pincer/objects/guild/member.py b/pincer/objects/guild/member.py index 4e0bc314..a00b3535 100644 --- a/pincer/objects/guild/member.py +++ b/pincer/objects/guild/member.py @@ -36,10 +36,10 @@ class BaseMember(APIObject): hoisted_role: APINullable[:class:`~pincer.utils.snowflake.Snowflake`] The user's top role in the guild. """ - deaf: bool joined_at: Timestamp - mute: bool roles: List[Snowflake] + deaf: bool = MISSING + mute: bool = MISSING hoisted_role: APINullable[Snowflake] = MISSING From 80336134fe6bab7effbed5f1bff8965d2f71167a Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Mon, 22 Nov 2021 18:40:24 -0500 Subject: [PATCH 028/134] :sparkles: params are now passed into function --- pincer/commands/commands.py | 6 +++++- pincer/middleware/interaction_create.py | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index c974994a..7f867388 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -309,15 +309,19 @@ class Bot(Client): async def test_user_command( self, ctx: MessageContext, + user: User, + member: GuildMember ): return Message( - f"The messages author is {}" + f"The messages author is {user}" ) References from above: :class:`~client.Client`, :class:`~objects.message.message.Message`, :class:`~objects.message.context.MessageContext`, + :class:`~objects.user.user.User`, + :class:`~objects.guild.member.GuildMember`, Parameters diff --git a/pincer/middleware/interaction_create.py b/pincer/middleware/interaction_create.py index 37384969..788f9c17 100644 --- a/pincer/middleware/interaction_create.py +++ b/pincer/middleware/interaction_create.py @@ -107,6 +107,15 @@ async def interaction_handler( } args = [] + + if interaction.data.type == AppCommandType.USER: + # Add User and Member args + args.append(next(iter(interaction.data.resolved.users.values()))) + args.append(next(iter(interaction.data.resolved.members.values()))) + elif interaction.data.type == AppCommandType.MESSAGE: + # Add Message to args + args.append(next(iter(interaction.data.resolved.messages.values()))) + kwargs = {**defaults, **params} await interaction_response_handler( From fe10d443c3a550e042843c907fe3a4a69a6c916f Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Mon, 22 Nov 2021 18:57:34 -0500 Subject: [PATCH 029/134] :bug: message_commands can now be used in guilds --- pincer/middleware/interaction_create.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pincer/middleware/interaction_create.py b/pincer/middleware/interaction_create.py index 788f9c17..9d264544 100644 --- a/pincer/middleware/interaction_create.py +++ b/pincer/middleware/interaction_create.py @@ -111,7 +111,13 @@ async def interaction_handler( if interaction.data.type == AppCommandType.USER: # Add User and Member args args.append(next(iter(interaction.data.resolved.users.values()))) - args.append(next(iter(interaction.data.resolved.members.values()))) + + members = interaction.data.resolved.members + if members: + args.append(next(iter(members.values()))) + else: + args.append(MISSING) + elif interaction.data.type == AppCommandType.MESSAGE: # Add Message to args args.append(next(iter(interaction.data.resolved.messages.values()))) From 4fb927e293cd72b1237329f012d4f239b8a30876 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Mon, 22 Nov 2021 21:50:01 -0500 Subject: [PATCH 030/134] :zap: optimized interaction create --- pincer/commands/commands.py | 68 +++++++++++++++++++++++-- pincer/middleware/interaction_create.py | 12 ++--- 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index 7f867388..1d5caf16 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -312,13 +312,16 @@ async def test_user_command( user: User, member: GuildMember ): - return Message( - f"The messages author is {user}" - ) + if not member: + # member is missing if this is a DM + # This bot doesn't like being DMed so it won't respond + return + + return f"Hello {user.name}, this is a Guild." + References from above: :class:`~client.Client`, - :class:`~objects.message.message.Message`, :class:`~objects.message.context.MessageContext`, :class:`~objects.user.user.User`, :class:`~objects.guild.member.GuildMember`, @@ -381,6 +384,63 @@ def message_command( cooldown_scale: Optional[float] = 60, cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, ): + """A decorator to create a user command to register and respond to + with the discord API from a function. + + .. code-block:: python3 + + class Bot(Client): + @user_command + async def test_message_command( + self, + ctx: MessageContext, + message: UserMessage, + ): + return message.content + + + References from above: + :class:`~client.Client`, + :class:`~objects.message.context.MessageContext`, + :class:`~objects.message.message.UserMessage`, + :class:`~objects.user.user.User`, + :class:`~objects.guild.member.GuildMember`, + + + Parameters + ---------- + name : Optional[:class:`str`] + The name of the command |default| :data:`None` + enable_default : Optional[:class:`bool`] + Whether the command is enabled by default |default| :data:`True` + guild : Optional[Union[:class:`~pincer.utils.snowflake.Snowflake`, :class:`int`, :class:`str`]] + What guild to add it to (don't specify for global) |default| :data:`None` + cooldown : Optional[:class:`int`] + The amount of times in the cooldown_scale the command can be invoked + |default| ``0`` + cooldown_scale : Optional[:class:`float`] + The 'checking time' of the cooldown |default| ``60`` + cooldown_scope : :class:`~pincer.objects.app.throttle_scope.ThrottleScope` + What type of cooldown strategy to use |default| :attr:`ThrottleScope.USER` + + Raises + ------ + CommandIsNotCoroutine + If the command function is not a coro + InvalidCommandName + If the command name does not follow the regex ``^[\\w-]{1,32}$`` + InvalidCommandGuild + If the guild id is invalid + CommandDescriptionTooLong + Descriptions max 100 characters + If the annotation on an argument is too long (also max 100) + CommandAlreadyRegistered + If the command already exists + InvalidArgumentAnnotation + Annotation amount is max 25, + Not a valid argument type, + Annotations must consist of name and value + """ return register_command( func=func, app_command_type=AppCommandType.MESSAGE, diff --git a/pincer/middleware/interaction_create.py b/pincer/middleware/interaction_create.py index 9d264544..3320fbbc 100644 --- a/pincer/middleware/interaction_create.py +++ b/pincer/middleware/interaction_create.py @@ -47,18 +47,12 @@ async def interaction_response_handler( \\*\\*kwargs : The arguments to be passed to the command. """ - maybe_self_and_context: List[Any] = [] - - if should_pass_cls(command): - maybe_self_and_context.append( - ChatCommandHandler.managers[command.__module__] - ) - sig, params = get_signature_and_params(command) if should_pass_ctx(sig, params): - maybe_self_and_context.append(context) + args.insert(0, context) - args = maybe_self_and_context + args + if should_pass_cls(command): + args.insert(0, ChatCommandHandler.managers[command.__module__]) if isasyncgenfunction(command): message = command(*args, **kwargs) From 2ad9a08693a37010fb5b8c0f053501ca970a0d6f Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Mon, 22 Nov 2021 22:23:44 -0500 Subject: [PATCH 031/134] :memo: updated docs --- docs/pincer.commands.rst | 23 ++++-- pincer/commands/arg_types.py | 153 ++++++++++++++++++++++++++++++++--- 2 files changed, 157 insertions(+), 19 deletions(-) diff --git a/docs/pincer.commands.rst b/docs/pincer.commands.rst index 2a57e023..949c0dbb 100644 --- a/docs/pincer.commands.rst +++ b/docs/pincer.commands.rst @@ -12,6 +12,23 @@ command .. autofunction:: command :decorator: +.. autofunction:: message_command + :decorator: +.. autofunction:: user_command + :decorator: + +Command Types +------------- + +.. autoclass:: Modifier() +.. autoclass:: Description() +.. autoclass:: Choices() +.. autoclass:: Choice() +.. autoclass:: MaxValue() +.. autoclass:: MinValue() +.. autoclass:: ChannelTypes() + + ChatCommandHandler ~~~~~~~~~~~~~~~~~~ @@ -19,9 +36,3 @@ ChatCommandHandler .. attributetable:: ChatCommandHandler .. autoclass:: ChatCommandHandler() - -Command Types -------------- - -.. automodule:: pincer.commands.arg_types - :members: diff --git a/pincer/commands/arg_types.py b/pincer/commands/arg_types.py index 532e19d4..e2e3c2be 100644 --- a/pincer/commands/arg_types.py +++ b/pincer/commands/arg_types.py @@ -16,7 +16,28 @@ def __getitem__(cls, args: Union[Tuple, Any]): class CommandArg(metaclass=_CommandTypeMeta): - """Holds all application command options""" + """ + Holds all application command options + + .. code-block:: python3 + + CommandArg[ + # This is the type of command. + # Supported types are str, int, bool, float, User, Channel, and Role + int, + # The modifiers to the command go here + Description["Pick a number 1-10"], + MinValue[1], + MaxValue[10] + ] + + Parameters + ---------- + command_type : T + The type of the command + *args : :class:`pincer.commands.arg_types.Modifier` + + """ def __init__(self, command_type, *args) -> None: self.command_type = command_type @@ -30,8 +51,30 @@ def get_arg(self, arg_type: T) -> T: return MISSING -class Description(metaclass=_CommandTypeMeta): - """Represents the description application command option type""" +class Modifier(metaclass=_CommandTypeMeta): + """ + Modifies a CommandArg by being added to + :class:`pincer.commands.arg_types.CommandArg`'s args. + """ + + +class Description(Modifier): + """ + Represents the description application command option type + + .. code-block:: python3 + + # Creates an int argument with the description "example description" + CommandArg[ + int, + Description["example description"] + ] + + Parameters + ---------- + desc : str + The description for the command. + """ def __init__(self, desc) -> None: self.desc = str(desc) @@ -40,16 +83,51 @@ def get_payload(self) -> str: return self.desc -class Choice(metaclass=_CommandTypeMeta): - """Represents an application command choice""" +class Choice(Modifier): + """ + Represents an application command choice + + .. code-block:: python3 + + Choices[ + Choice["First Number", 10], + Choice["Second Number", 20] + ] + + Parameters + ---------- + name : str + The name of the choice + value : Union[int, str, float] + The value of the choice + """ def __init__(self, name, value) -> None: self.name = name self.value = value -class Choices(metaclass=_CommandTypeMeta): - """Represents the choice application command option type""" +class Choices(Modifier): + """ + Represents the choice application command option type + + .. code-block:: python3 + + CommandArg[ + int, + Choices[ + Choice["First Number", 10], + 20, + 50 + ] + ] + + Parameters + ---------- + *choices : Union[:class:`pincer.commands.arg_types.Choice`, str, int, float] + A choice. If the type is not :class:`pincer.commands.arg_types.Choice`, + the same value will be used for the choice name and value. + """ def __init__(self, *choices) -> None: self.choices = [] @@ -71,8 +149,27 @@ def get_payload(self) -> List[Union[str, int, float]]: return self.choices -class ChannelTypes(metaclass=_CommandTypeMeta): - """Represents the channel types application command option type""" +class ChannelTypes(Modifier): + """ + Represents the channel types application command option type. + + .. code-block:: python3 + + CommandArg[ + Channel, + # The user will only be able to choice between GUILD_TEXT and + GUILD_TEXT channels. + ChannelTypes[ + ChannelType.GUILD_TEXT, + ChannelType.GUILD_VOICE + ] + ] + + Parameters + ---------- + *types : :class:`pincer.objects.guild.channel.ChannelType` + A list of channel types that the user can pick from. + """ def __init__(self, *types) -> None: self.types = types @@ -81,8 +178,23 @@ def get_payload(self): return self.types -class MaxValue(metaclass=_CommandTypeMeta): - """Represents the channel types application command option type""" +class MaxValue(Modifier): + """ + Represents the channel types application command option type + + .. code-block:: python3 + + CommandArg[ + int, + # The user can't pick a number above 10 + MaxValue[10] + ] + + Parameters + ---------- + max_value : Union[float, int] + The max value a user can choose. + """ def __init__(self, max_value) -> None: self.max_value = max_value @@ -91,8 +203,23 @@ def get_payload(self): return self.max_value -class MinValue(metaclass=_CommandTypeMeta): - """Represents the channel types application command option type""" +class MinValue(Modifier): + """ + Represents the channel types application command option type + + .. code-block:: python3 + + CommandArg[ + int, + # The user can't pick a number below 10 + MinValue[10] + ] + + Parameters + ---------- + min_value : Union[float, int] + The minimum value a user can choose. + """ def __init__(self, min_value) -> None: self.min_value = min_value From 3dd352a3bdb8c7e824230564907f0608c7c38e19 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Mon, 22 Nov 2021 22:26:59 -0500 Subject: [PATCH 032/134] :memo: updated readme and pypy.md --- docs/PYPI.md | 10 +++++----- docs/README.md | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/PYPI.md b/docs/PYPI.md index 977de25a..47da0d66 100644 --- a/docs/PYPI.md +++ b/docs/PYPI.md @@ -122,7 +122,7 @@ You have the possibility to use your own class to inherit from the Pincer bot base. ```py -from pincer import Client, command, Descripted +from pincer import Client, command, CommandArg, Description class Bot(Client): @@ -139,10 +139,10 @@ class Bot(Client): @command(description="Add two numbers!") async def add( - self, - first: Descripted[int, "The first number"], - second: Descripted[int, "The second number"] - ): + self, + first: CommandArg[int, Description["The first number"]], + second: CommandArg[int, Description["The second number"]] + ): return f"The addition of `{first}` and `{second}` is `{first + second}`" ``` diff --git a/docs/README.md b/docs/README.md index cccf24c1..4a74e123 100644 --- a/docs/README.md +++ b/docs/README.md @@ -128,7 +128,7 @@ You have the possibility to use your own class to inherit from the Pincer bot base. ```py -from pincer import Client, command, Descripted +from pincer import Client, command, CommandArg, Description class Bot(Client): @@ -154,10 +154,10 @@ class Bot(Client): @command(description="Add two numbers!") async def add( - self, - first: Descripted[int, "The first number"], - second: Descripted[int, "The second number"] - ): + self, + first: CommandArg[int, Description["The first number"]], + second: CommandArg[int, Description["The second number"]] + ): return f"The addition of `{first}` and `{second}` is `{first + second}`" ``` From dae9cbb10b5ce05ac783093491ba9434dbb20bac Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Mon, 22 Nov 2021 22:32:37 -0500 Subject: [PATCH 033/134] :memo: updated examples to not use Descripted --- docs/PYPI.md | 4 ++-- docs/README.md | 3 ++- examples/chat_commands_class_based.py | 9 +++++---- examples/chat_commands_function_based.py | 11 ++++++----- examples/context.py | 5 +++-- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/PYPI.md b/docs/PYPI.md index 47da0d66..1088c575 100644 --- a/docs/PYPI.md +++ b/docs/PYPI.md @@ -122,8 +122,8 @@ You have the possibility to use your own class to inherit from the Pincer bot base. ```py -from pincer import Client, command, CommandArg, Description - +from pincer import Client +from pincer.commands import command, CommandArg, Description class Bot(Client): def __init__(self) -> None: diff --git a/docs/README.md b/docs/README.md index 4a74e123..826c99b3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -128,7 +128,8 @@ You have the possibility to use your own class to inherit from the Pincer bot base. ```py -from pincer import Client, command, CommandArg, Description +from pincer import Client +from pincer.commands import command, CommandArg, Description class Bot(Client): diff --git a/examples/chat_commands_class_based.py b/examples/chat_commands_class_based.py index b4226ab9..b2533f7d 100644 --- a/examples/chat_commands_class_based.py +++ b/examples/chat_commands_class_based.py @@ -1,4 +1,5 @@ -from pincer import Client, command, Descripted +from pincer import Client +from pincer.commands import command, CommandArg, Description from pincer.objects import Message, InteractionFlags, Embed @@ -17,13 +18,13 @@ async def say(self, message: str): @command(description="Add two numbers!") async def add( self, - first: Descripted[int, "The first number"], - second: Descripted[int, "The second number"] + first: CommandArg[int, Description["The first number"]], + second: CommandArg[int, Description["The second number"]] ): return f"The addition of `{first}` and `{second}` is `{first + second}`" @command(guild=1324567890) - async def private_say(self, message: Descripted[str, "The content of the message"]): + async def private_say(self, message: CommandArg[str, Description["The content of the message"]]): return Message(message, flags=InteractionFlags.EPHEMERAL) @command(description="How to make embed!") diff --git a/examples/chat_commands_function_based.py b/examples/chat_commands_function_based.py index 89fd4542..19dac22d 100644 --- a/examples/chat_commands_function_based.py +++ b/examples/chat_commands_function_based.py @@ -1,4 +1,5 @@ -from pincer import command, Client, Descripted +from pincer import Client +from pincer.commands import command, CommandArg, Description from pincer.objects import Message, InteractionFlags, Embed @@ -11,20 +12,20 @@ async def on_ready(self): @command(description="Say something as the bot!") -async def say(message: Descripted[str, "The content of the message"]): +async def say(message: CommandArg[str, Description["The content of the message"]]): return message @command(description="Add two numbers!") async def add( - first: Descripted[int, "The first number"], - second: Descripted[int, "The second number"] + first: CommandArg[int, Description["The first number"]], + second: CommandArg[int, Description["The second number"]] ): return f"The addition of `{first}` and `{second}` is `{first + second}`" @command(guild=1324567890) -async def private_say(message: Descripted[str, "The content of the message"]): +async def private_say(message: CommandArg[str, Description["The content of the message"]]): return Message(message, flags=InteractionFlags.EPHEMERAL) diff --git a/examples/context.py b/examples/context.py index a25107f4..013c10a0 100644 --- a/examples/context.py +++ b/examples/context.py @@ -1,4 +1,5 @@ -from pincer import Client, command, Descripted +from pincer import Client +from pincer.commands import command, CommandArg, Description from pincer.objects import Embed @@ -7,7 +8,7 @@ class Bot(Client): @command(description="Say something as the bot!") async def say( self, ctx, - content: Descripted[str, "The content of the message"] + content: CommandArg[str, Description["The content of the message"]] ) -> Embed: # Using the ctx to get the command author return Embed( From a6e0ecf696261cec0efe447ab6144bfae421837e Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Mon, 22 Nov 2021 22:44:42 -0500 Subject: [PATCH 034/134] :art: codacity changes --- pincer/commands/arg_types.py | 2 +- pincer/commands/commands.py | 8 +++++--- pincer/middleware/interaction_create.py | 4 ++-- pincer/objects/app/command.py | 3 ++- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pincer/commands/arg_types.py b/pincer/commands/arg_types.py index e2e3c2be..e6922d90 100644 --- a/pincer/commands/arg_types.py +++ b/pincer/commands/arg_types.py @@ -45,7 +45,7 @@ def __init__(self, command_type, *args) -> None: def get_arg(self, arg_type: T) -> T: for arg in self.modifiers: - if type(arg) == arg_type: + if isinstance(arg, arg_type): return arg.get_payload() return MISSING diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index 1d5caf16..bf3ab025 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -12,7 +12,9 @@ from typing import TYPE_CHECKING, Union, Tuple, List from . import __package__ -from ..commands.arg_types import ChannelTypes, CommandArg, Description, Choices, MaxValue, MinValue +from ..commands.arg_types import ( + ChannelTypes, CommandArg, Description, Choices, MaxValue, MinValue +) from ..utils.snowflake import Snowflake from ..exceptions import ( CommandIsNotCoroutine, @@ -41,8 +43,8 @@ ) from ..utils import get_index, should_pass_ctx from ..utils.signature import get_signature_and_params -from ..utils.types import MISSING, choice_value_types -from ..utils.types import Singleton, TypeCache +from ..utils.types import MISSING +from ..utils.types import Singleton if TYPE_CHECKING: from typing import Any, Optional, Dict diff --git a/pincer/middleware/interaction_create.py b/pincer/middleware/interaction_create.py index 3320fbbc..d242d923 100644 --- a/pincer/middleware/interaction_create.py +++ b/pincer/middleware/interaction_create.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging -from inspect import isasyncgenfunction, getfullargspec, _empty +from inspect import isasyncgenfunction, _empty from typing import Dict, Any from typing import TYPE_CHECKING @@ -15,7 +15,7 @@ from ..utils import MISSING, should_pass_cls, Coro, should_pass_ctx from ..utils import get_index from ..utils.conversion import construct_client_dict -from ..utils.signature import get_params, get_signature_and_params +from ..utils.signature import get_signature_and_params if TYPE_CHECKING: from typing import List, Tuple diff --git a/pincer/objects/app/command.py b/pincer/objects/app/command.py index 49091398..0dcbe8d5 100644 --- a/pincer/objects/app/command.py +++ b/pincer/objects/app/command.py @@ -147,7 +147,8 @@ class AppCommand(APIObject): def __post_init__(self): super().__post_init__() - self.options = [] if self.options is MISSING and self.type == AppCommandType.MESSAGE else self.options + if self.options is MISSING and self.type == AppCommandType.MESSAGE: + self.options = [] def __eq__(self, other: Union[AppCommand, ClientCommandStructure]): if isinstance(other, ClientCommandStructure): From ceb33db7547848fa19c6ce8a15c630616c4a0ed6 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Mon, 22 Nov 2021 22:47:12 -0500 Subject: [PATCH 035/134] :art: codacity changes --- pincer/commands/arg_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pincer/commands/arg_types.py b/pincer/commands/arg_types.py index e6922d90..1d4ffe4e 100644 --- a/pincer/commands/arg_types.py +++ b/pincer/commands/arg_types.py @@ -23,7 +23,7 @@ class CommandArg(metaclass=_CommandTypeMeta): CommandArg[ # This is the type of command. - # Supported types are str, int, bool, float, User, Channel, and Role + # Supported types are str, int, bool, float, User, Channel, and Role int, # The modifiers to the command go here Description["Pick a number 1-10"], From 5767adec90fb1fc462765a81822dbe4316d5b1ec Mon Sep 17 00:00:00 2001 From: sigmanificient Date: Tue, 23 Nov 2021 19:14:52 +0100 Subject: [PATCH 036/134] :fire: removing `None` return annotations --- pincer/commands/arg_types.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pincer/commands/arg_types.py b/pincer/commands/arg_types.py index 1d4ffe4e..a0ed2266 100644 --- a/pincer/commands/arg_types.py +++ b/pincer/commands/arg_types.py @@ -39,7 +39,7 @@ class CommandArg(metaclass=_CommandTypeMeta): """ - def __init__(self, command_type, *args) -> None: + def __init__(self, command_type, *args): self.command_type = command_type self.modifiers = args @@ -76,7 +76,7 @@ class Description(Modifier): The description for the command. """ - def __init__(self, desc) -> None: + def __init__(self, desc): self.desc = str(desc) def get_payload(self) -> str: @@ -102,7 +102,7 @@ class Choice(Modifier): The value of the choice """ - def __init__(self, name, value) -> None: + def __init__(self, name, value): self.name = name self.value = value @@ -129,7 +129,7 @@ class Choices(Modifier): the same value will be used for the choice name and value. """ - def __init__(self, *choices) -> None: + def __init__(self, *choices): self.choices = [] for choice in choices: @@ -171,7 +171,7 @@ class ChannelTypes(Modifier): A list of channel types that the user can pick from. """ - def __init__(self, *types) -> None: + def __init__(self, *types): self.types = types def get_payload(self): @@ -196,7 +196,7 @@ class MaxValue(Modifier): The max value a user can choose. """ - def __init__(self, max_value) -> None: + def __init__(self, max_value): self.max_value = max_value def get_payload(self): @@ -221,7 +221,7 @@ class MinValue(Modifier): The minimum value a user can choose. """ - def __init__(self, min_value) -> None: + def __init__(self, min_value): self.min_value = min_value def get_payload(self): From a43cf20b50e1bf0843e766a455ff463be48cd5a6 Mon Sep 17 00:00:00 2001 From: Yohann Boniface Date: Tue, 23 Nov 2021 19:16:07 +0100 Subject: [PATCH 037/134] =?UTF-8?q?=F0=9F=93=9D=20Improving=20docstring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Endercheif <45527309+Endercheif@users.noreply.github.com> 📝 Improving docstring Co-authored-by: Endercheif <45527309+Endercheif@users.noreply.github.com> 📝 Improving docstring Co-authored-by: Endercheif <45527309+Endercheif@users.noreply.github.com> 📝 Improving docstring Co-authored-by: Endercheif <45527309+Endercheif@users.noreply.github.com> 📝 Improving docstring Co-authored-by: Endercheif <45527309+Endercheif@users.noreply.github.com> 📝 Improving docstring Co-authored-by: Endercheif <45527309+Endercheif@users.noreply.github.com> 📝 Improving docstring Co-authored-by: Endercheif <45527309+Endercheif@users.noreply.github.com> 📝 Improving docstring Co-authored-by: RPS 📝 Improving docstring Co-authored-by: RPS 📝 Improving docstring Co-authored-by: RPS 📝 Improving docstring Co-authored-by: RPS 📝 Improving docstring Co-authored-by: RPS 📝 Improving docstring Co-authored-by: RPS 📝 Improving docstring Co-authored-by: RPS 📝 Improving docstring Co-authored-by: RPS --- pincer/commands/arg_types.py | 18 +++++++++--------- pincer/commands/commands.py | 12 ++++++------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pincer/commands/arg_types.py b/pincer/commands/arg_types.py index a0ed2266..53d2c691 100644 --- a/pincer/commands/arg_types.py +++ b/pincer/commands/arg_types.py @@ -35,7 +35,7 @@ class CommandArg(metaclass=_CommandTypeMeta): ---------- command_type : T The type of the command - *args : :class:`pincer.commands.arg_types.Modifier` + \*args : :class:`~pincer.commands.arg_types.Modifier` """ @@ -54,13 +54,13 @@ def get_arg(self, arg_type: T) -> T: class Modifier(metaclass=_CommandTypeMeta): """ Modifies a CommandArg by being added to - :class:`pincer.commands.arg_types.CommandArg`'s args. + :class:`~pincer.commands.arg_types.CommandArg`'s args. """ class Description(Modifier): """ - Represents the description application command option type + Represents the description of an application command option type .. code-block:: python3 @@ -109,7 +109,7 @@ def __init__(self, name, value): class Choices(Modifier): """ - Represents the choice application command option type + Represents a Group of Application Command Choices .. code-block:: python3 @@ -124,8 +124,8 @@ class Choices(Modifier): Parameters ---------- - *choices : Union[:class:`pincer.commands.arg_types.Choice`, str, int, float] - A choice. If the type is not :class:`pincer.commands.arg_types.Choice`, + \*choices : Union[:class:`~pincer.commands.arg_types.Choice`, str, int, float] + A choice. If the type is not :class:`~pincer.commands.arg_types.Choice`, the same value will be used for the choice name and value. """ @@ -167,7 +167,7 @@ class ChannelTypes(Modifier): Parameters ---------- - *types : :class:`pincer.objects.guild.channel.ChannelType` + \*types : :class:`~pincer.objects.guild.channel.ChannelType` A list of channel types that the user can pick from. """ @@ -192,7 +192,7 @@ class MaxValue(Modifier): Parameters ---------- - max_value : Union[float, int] + max_value : Union[:class:`float`, :class:`int`] The max value a user can choose. """ @@ -217,7 +217,7 @@ class MinValue(Modifier): Parameters ---------- - min_value : Union[float, int] + min_value : Union[:class:`float`, :class:`int`] The minimum value a user can choose. """ diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index bf3ab025..b3ab9542 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -80,7 +80,7 @@ def command( cooldown_scale: Optional[float] = 60, cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, ): - """A decorator to slash create a command to register and respond to + """A decorator to create a slash command to register and respond to with the discord API from a function. str - String @@ -248,7 +248,7 @@ async def test_command( cannel_types = annotation.get_arg(ChannelTypes) if cannel_types is not MISSING and annotation.command_type != Channel: raise InvalidArgumentAnnotation( - "ChannelTypes is only available for Channel") + "ChannelTypes are only available for Channels") max_value = annotation.get_arg(MaxValue) min_value = annotation.get_arg(MinValue) @@ -301,8 +301,8 @@ def user_command( cooldown_scale: Optional[float] = 60, cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, ): - """A decorator to create a user command to register and respond to - with the discord API from a function. + """A decorator to create a user command registering and responding + to the Discord API from a function. .. code-block:: python3 @@ -386,8 +386,8 @@ def message_command( cooldown_scale: Optional[float] = 60, cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, ): - """A decorator to create a user command to register and respond to - with the discord API from a function. + """A decorator to create a user command to register and respond + to the Discord API from a function. .. code-block:: python3 From 022283c7f98edf091f3227e5050141a088a5119a Mon Sep 17 00:00:00 2001 From: sigmanificient Date: Tue, 23 Nov 2021 19:51:38 +0100 Subject: [PATCH 038/134] :fire: removing empty post init --- pincer/objects/app/command.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pincer/objects/app/command.py b/pincer/objects/app/command.py index 0dcbe8d5..327e4686 100644 --- a/pincer/objects/app/command.py +++ b/pincer/objects/app/command.py @@ -56,11 +56,6 @@ class AppCommandOptionChoice(APIObject): name: str value: choice_value_types - def __post_init__(self): - # Default serialization causes too many issues with Union - # It isn't needed here anyway - return - @dataclass class AppCommandOption(APIObject): From 8eb976d8c4312bdf4799f8357a8d31ded008dba8 Mon Sep 17 00:00:00 2001 From: Yohann Boniface Date: Tue, 23 Nov 2021 23:51:12 +0100 Subject: [PATCH 039/134] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Using=20`is`=20ins?= =?UTF-8?q?tead=20of=20`=3D=3D`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> ♻️ Using `is` instead of `==` Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> ♻️ Using `is` instead of `==` Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> ♻️ Using `is` instead of `==` Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> ♻️ Using `is` instead of `==` Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> ♻️ Using `is` instead of `==` Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> ♻️ Using `is` instead of `==` Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> --- pincer/commands/commands.py | 10 +++++----- pincer/middleware/interaction_create.py | 4 ++-- pincer/objects/app/command.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index b3ab9542..8d53435f 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -209,7 +209,7 @@ async def test_command( if annotation == MessageContext and idx == 1: return - if type(annotation) != CommandArg: + if type(annotation) is not CommandArg: if annotation in _options_type_link: options.append( AppCommandOption( @@ -238,7 +238,7 @@ async def test_command( ) if choices is not MISSING: for choice in choices: - if isinstance(choice.value, int) and annotation.command_type == float: + if isinstance(choice.value, int) and annotation.command_type is float: continue if not isinstance(choice.value, annotation.command_type): raise InvalidArgumentAnnotation( @@ -246,7 +246,7 @@ async def test_command( ) cannel_types = annotation.get_arg(ChannelTypes) - if cannel_types is not MISSING and annotation.command_type != Channel: + if cannel_types is not MISSING and annotation.command_type is not Channel: raise InvalidArgumentAnnotation( "ChannelTypes are only available for Channels") @@ -256,8 +256,8 @@ async def test_command( for i, value in enumerate((min_value, max_value)): if ( value is not MISSING - and annotation.command_type != int - and annotation.command_type != float + and annotation.command_type is not int + and annotation.command_type is not float ): t = ("MinValue", "MaxValue") raise InvalidArgumentAnnotation( diff --git a/pincer/middleware/interaction_create.py b/pincer/middleware/interaction_create.py index d242d923..5985cec1 100644 --- a/pincer/middleware/interaction_create.py +++ b/pincer/middleware/interaction_create.py @@ -102,7 +102,7 @@ async def interaction_handler( args = [] - if interaction.data.type == AppCommandType.USER: + if interaction.data.type is AppCommandType.USER: # Add User and Member args args.append(next(iter(interaction.data.resolved.users.values()))) @@ -112,7 +112,7 @@ async def interaction_handler( else: args.append(MISSING) - elif interaction.data.type == AppCommandType.MESSAGE: + elif interaction.data.type is AppCommandType.MESSAGE: # Add Message to args args.append(next(iter(interaction.data.resolved.messages.values()))) diff --git a/pincer/objects/app/command.py b/pincer/objects/app/command.py index 327e4686..9d1654d7 100644 --- a/pincer/objects/app/command.py +++ b/pincer/objects/app/command.py @@ -142,7 +142,7 @@ class AppCommand(APIObject): def __post_init__(self): super().__post_init__() - if self.options is MISSING and self.type == AppCommandType.MESSAGE: + if self.options is MISSING and self.type is AppCommandType.MESSAGE: self.options = [] def __eq__(self, other: Union[AppCommand, ClientCommandStructure]): From 3e39d1e7201522004017c245fa8b26b04e7f5887 Mon Sep 17 00:00:00 2001 From: Yohann Boniface Date: Tue, 23 Nov 2021 23:55:09 +0100 Subject: [PATCH 040/134] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Using=20a=20set=20?= =?UTF-8?q?tuple=20for=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> --- pincer/commands/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index 8d53435f..542a1376 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -232,7 +232,7 @@ async def test_command( choices = annotation.get_arg( Choices) - if choices is not MISSING and annotation.command_type not in [int, float, str]: + if choices is not MISSING and annotation.command_type not in {int, float, str}: raise InvalidArgumentAnnotation( "Choice type is only allowed for str, int, and float" ) From e9d19eb819cb6059e71f6bb44167a133f29b60f6 Mon Sep 17 00:00:00 2001 From: sigmanificient Date: Tue, 23 Nov 2021 23:56:51 +0100 Subject: [PATCH 041/134] :recycle: Using parenthesis for __all__ --- pincer/commands/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pincer/commands/__init__.py b/pincer/commands/__init__.py index d63e4f77..4219ae49 100644 --- a/pincer/commands/__init__.py +++ b/pincer/commands/__init__.py @@ -6,8 +6,8 @@ CommandArg, Description, Choice, Choices, ChannelTypes, MaxValue, MinValue ) -__all__ = [ +__all__ = ( "command", "ChatCommandHandler", "CommandArg", "Description", "Choice", "Choices", "ChannelTypes", "MaxValue", "MinValue", "user_command", "message_command" -] +) From 14711d390e206db97e56218624af67c2a5b21e9f Mon Sep 17 00:00:00 2001 From: Yohann Boniface Date: Tue, 23 Nov 2021 23:58:43 +0100 Subject: [PATCH 042/134] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Missing=20Typehint?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> --- pincer/commands/commands.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index 542a1376..eb9b3f7d 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -456,17 +456,17 @@ async def test_message_command( def register_command( - func=None, + func=None, # Missing typehint? *, - app_command_type: AppCommandType = None, + app_command_type: Optional[AppCommandType] = None, name: Optional[str] = None, description: Optional[str] = MISSING, enable_default: Optional[bool] = True, - guild: Union[Snowflake, int, str] = None, + guild: Optional[Union[Snowflake, int, str]] = None, cooldown: Optional[int] = 0, cooldown_scale: Optional[float] = 60, cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, - command_options=MISSING + command_options=MISSING # Missing typehint? ): if func is None: return partial( From 28f85e565a9c09300706b4f2feee6424720d9f25 Mon Sep 17 00:00:00 2001 From: Yohann Boniface Date: Tue, 23 Nov 2021 23:59:16 +0100 Subject: [PATCH 043/134] =?UTF-8?q?=F0=9F=8E=A8=20Missing=20whitespace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> --- pincer/commands/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index eb9b3f7d..c348f383 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -492,7 +492,7 @@ def register_command( if not re.match(COMMAND_NAME_REGEX, cmd): raise InvalidCommandName( f"Command `{cmd}` doesn't follow the name requirements." - "Ensure to match the following regex:" + " Ensure to match the following regex:" f" {COMMAND_NAME_REGEX.pattern}" ) From 65e72c2c86c442ff71abece3ceed15c99db24d3f Mon Sep 17 00:00:00 2001 From: Yohann Boniface Date: Tue, 23 Nov 2021 23:59:58 +0100 Subject: [PATCH 044/134] =?UTF-8?q?=F0=9F=8E=A8=20Using=20pythonic=20usele?= =?UTF-8?q?ss=20var=20declaration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> --- pincer/middleware/interaction_create.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pincer/middleware/interaction_create.py b/pincer/middleware/interaction_create.py index 5985cec1..70bc0bf9 100644 --- a/pincer/middleware/interaction_create.py +++ b/pincer/middleware/interaction_create.py @@ -89,7 +89,7 @@ async def interaction_handler( """ self.throttler.handle(context) - sig, params = get_signature_and_params(command) + sig, _ = get_signature_and_params(command) defaults = {key: value.default for key, value in sig.items() if value.default is not _empty} From e84aa98150489161e1f8b548ce284d8d23b173ca Mon Sep 17 00:00:00 2001 From: Yohann Boniface Date: Wed, 24 Nov 2021 00:00:56 +0100 Subject: [PATCH 045/134] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Shorten=20with=20w?= =?UTF-8?q?alrus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> --- pincer/middleware/interaction_create.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pincer/middleware/interaction_create.py b/pincer/middleware/interaction_create.py index 70bc0bf9..04531ec8 100644 --- a/pincer/middleware/interaction_create.py +++ b/pincer/middleware/interaction_create.py @@ -106,8 +106,7 @@ async def interaction_handler( # Add User and Member args args.append(next(iter(interaction.data.resolved.users.values()))) - members = interaction.data.resolved.members - if members: + if members := interaction.data.resolved.members: args.append(next(iter(members.values()))) else: args.append(MISSING) From 51889300608070c97711beb2c146d884ca18e782 Mon Sep 17 00:00:00 2001 From: sigmanificient Date: Wed, 24 Nov 2021 00:03:28 +0100 Subject: [PATCH 046/134] :art: black formatting --- pincer/commands/arg_types.py | 16 ++-- pincer/commands/commands.py | 164 +++++++++++++++++++---------------- 2 files changed, 97 insertions(+), 83 deletions(-) diff --git a/pincer/commands/arg_types.py b/pincer/commands/arg_types.py index 53d2c691..052751e4 100644 --- a/pincer/commands/arg_types.py +++ b/pincer/commands/arg_types.py @@ -10,7 +10,7 @@ class _CommandTypeMeta(type): def __getitem__(cls, args: Union[Tuple, Any]): if not isinstance(args, tuple): - args = args, + args = (args,) return cls(*args) @@ -134,16 +134,14 @@ def __init__(self, *choices): for choice in choices: if isinstance(choice, Choice): - self.choices.append(AppCommandOptionChoice( - name=choice.name, - value=choice.value - )) + self.choices.append( + AppCommandOptionChoice(name=choice.name, value=choice.value) + ) continue - self.choices.append(AppCommandOptionChoice( - name=str(choice), - value=choice - )) + self.choices.append( + AppCommandOptionChoice(name=str(choice), value=choice) + ) def get_payload(self) -> List[Union[str, int, float]]: return self.choices diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index c348f383..4b5a44f7 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -13,7 +13,12 @@ from . import __package__ from ..commands.arg_types import ( - ChannelTypes, CommandArg, Description, Choices, MaxValue, MinValue + ChannelTypes, + CommandArg, + Description, + Choices, + MaxValue, + MinValue, ) from ..utils.snowflake import Snowflake from ..exceptions import ( @@ -216,7 +221,7 @@ async def test_command( type=_options_type_link[annotation], name=param, description="Description not set", - required=required + required=required, ) ) continue @@ -227,18 +232,25 @@ async def test_command( ) command_type = _options_type_link[annotation.command_type] - argument_description = annotation.get_arg( - Description) or "Description not set" - choices = annotation.get_arg( - Choices) + argument_description = ( + annotation.get_arg(Description) or "Description not set" + ) + choices = annotation.get_arg(Choices) - if choices is not MISSING and annotation.command_type not in {int, float, str}: + if choices is not MISSING and annotation.command_type not in { + int, + float, + str, + }: raise InvalidArgumentAnnotation( "Choice type is only allowed for str, int, and float" ) if choices is not MISSING: for choice in choices: - if isinstance(choice.value, int) and annotation.command_type is float: + if ( + isinstance(choice.value, int) + and annotation.command_type is float + ): continue if not isinstance(choice.value, annotation.command_type): raise InvalidArgumentAnnotation( @@ -246,9 +258,13 @@ async def test_command( ) cannel_types = annotation.get_arg(ChannelTypes) - if cannel_types is not MISSING and annotation.command_type is not Channel: + if ( + cannel_types is not MISSING + and annotation.command_type is not Channel + ): raise InvalidArgumentAnnotation( - "ChannelTypes are only available for Channels") + "ChannelTypes are only available for Channels" + ) max_value = annotation.get_arg(MaxValue) min_value = annotation.get_arg(MinValue) @@ -287,7 +303,7 @@ async def test_command( cooldown=cooldown, cooldown_scale=cooldown_scale, cooldown_scope=cooldown_scope, - command_options=options + command_options=options, ) @@ -302,66 +318,66 @@ def user_command( cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, ): """A decorator to create a user command registering and responding - to the Discord API from a function. - - .. code-block:: python3 - - class Bot(Client): - @user_command - async def test_user_command( - self, - ctx: MessageContext, - user: User, - member: GuildMember - ): - if not member: - # member is missing if this is a DM - # This bot doesn't like being DMed so it won't respond - return - - return f"Hello {user.name}, this is a Guild." - - - References from above: - :class:`~client.Client`, - :class:`~objects.message.context.MessageContext`, - :class:`~objects.user.user.User`, - :class:`~objects.guild.member.GuildMember`, - - - Parameters - ---------- - name : Optional[:class:`str`] - The name of the command |default| :data:`None` - enable_default : Optional[:class:`bool`] - Whether the command is enabled by default |default| :data:`True` - guild : Optional[Union[:class:`~pincer.utils.snowflake.Snowflake`, :class:`int`, :class:`str`]] - What guild to add it to (don't specify for global) |default| :data:`None` - cooldown : Optional[:class:`int`] - The amount of times in the cooldown_scale the command can be invoked - |default| ``0`` - cooldown_scale : Optional[:class:`float`] - The 'checking time' of the cooldown |default| ``60`` - cooldown_scope : :class:`~pincer.objects.app.throttle_scope.ThrottleScope` - What type of cooldown strategy to use |default| :attr:`ThrottleScope.USER` + to the Discord API from a function. - Raises - ------ - CommandIsNotCoroutine - If the command function is not a coro - InvalidCommandName - If the command name does not follow the regex ``^[\\w-]{1,32}$`` - InvalidCommandGuild - If the guild id is invalid - CommandDescriptionTooLong - Descriptions max 100 characters - If the annotation on an argument is too long (also max 100) - CommandAlreadyRegistered - If the command already exists - InvalidArgumentAnnotation - Annotation amount is max 25, - Not a valid argument type, - Annotations must consist of name and value + .. code-block:: python3 + + class Bot(Client): + @user_command + async def test_user_command( + self, + ctx: MessageContext, + user: User, + member: GuildMember + ): + if not member: + # member is missing if this is a DM + # This bot doesn't like being DMed so it won't respond + return + + return f"Hello {user.name}, this is a Guild." + + + References from above: + :class:`~client.Client`, + :class:`~objects.message.context.MessageContext`, + :class:`~objects.user.user.User`, + :class:`~objects.guild.member.GuildMember`, + + + Parameters + ---------- + name : Optional[:class:`str`] + The name of the command |default| :data:`None` + enable_default : Optional[:class:`bool`] + Whether the command is enabled by default |default| :data:`True` + guild : Optional[Union[:class:`~pincer.utils.snowflake.Snowflake`, :class:`int`, :class:`str`]] + What guild to add it to (don't specify for global) |default| :data:`None` + cooldown : Optional[:class:`int`] + The amount of times in the cooldown_scale the command can be invoked + |default| ``0`` + cooldown_scale : Optional[:class:`float`] + The 'checking time' of the cooldown |default| ``60`` + cooldown_scope : :class:`~pincer.objects.app.throttle_scope.ThrottleScope` + What type of cooldown strategy to use |default| :attr:`ThrottleScope.USER` + + Raises + ------ + CommandIsNotCoroutine + If the command function is not a coro + InvalidCommandName + If the command name does not follow the regex ``^[\\w-]{1,32}$`` + InvalidCommandGuild + If the guild id is invalid + CommandDescriptionTooLong + Descriptions max 100 characters + If the annotation on an argument is too long (also max 100) + CommandAlreadyRegistered + If the command already exists + InvalidArgumentAnnotation + Annotation amount is max 25, + Not a valid argument type, + Annotations must consist of name and value """ # noqa: E501 return register_command( @@ -372,7 +388,7 @@ async def test_user_command( guild=guild, cooldown=cooldown, cooldown_scale=cooldown_scale, - cooldown_scope=cooldown_scope + cooldown_scope=cooldown_scope, ) @@ -451,12 +467,12 @@ async def test_message_command( guild=guild, cooldown=cooldown, cooldown_scale=cooldown_scale, - cooldown_scope=cooldown_scope + cooldown_scope=cooldown_scope, ) def register_command( - func=None, # Missing typehint? + func=None, # Missing typehint? *, app_command_type: Optional[AppCommandType] = None, name: Optional[str] = None, @@ -466,7 +482,7 @@ def register_command( cooldown: Optional[int] = 0, cooldown_scale: Optional[float] = 60, cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, - command_options=MISSING # Missing typehint? + command_options=MISSING, # Missing typehint? ): if func is None: return partial( From 231ff562b855750f4096f855c164c077b296f71e Mon Sep 17 00:00:00 2001 From: sigmanificient Date: Wed, 24 Nov 2021 00:08:03 +0100 Subject: [PATCH 047/134] :recycle: Fixing typehint --- pincer/commands/arg_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pincer/commands/arg_types.py b/pincer/commands/arg_types.py index 052751e4..d8b34a1d 100644 --- a/pincer/commands/arg_types.py +++ b/pincer/commands/arg_types.py @@ -143,7 +143,7 @@ def __init__(self, *choices): AppCommandOptionChoice(name=str(choice), value=choice) ) - def get_payload(self) -> List[Union[str, int, float]]: + def get_payload(self) -> List[AppCommandOptionChoice]: return self.choices From 5aecb68d3cf7718ed41d9a02cc35823b5931965f Mon Sep 17 00:00:00 2001 From: sigmanificient Date: Wed, 24 Nov 2021 00:11:00 +0100 Subject: [PATCH 048/134] :art: Using black formatting on examples --- examples/chat_commands_class_based.py | 44 ++++++++++++++---------- examples/chat_commands_function_based.py | 43 +++++++++++++---------- 2 files changed, 50 insertions(+), 37 deletions(-) diff --git a/examples/chat_commands_class_based.py b/examples/chat_commands_class_based.py index b2533f7d..02875f00 100644 --- a/examples/chat_commands_class_based.py +++ b/examples/chat_commands_class_based.py @@ -17,34 +17,40 @@ async def say(self, message: str): @command(description="Add two numbers!") async def add( - self, - first: CommandArg[int, Description["The first number"]], - second: CommandArg[int, Description["The second number"]] + self, + first: CommandArg[int, Description["The first number"]], + second: CommandArg[int, Description["The second number"]], ): return f"The addition of `{first}` and `{second}` is `{first + second}`" @command(guild=1324567890) - async def private_say(self, message: CommandArg[str, Description["The content of the message"]]): + async def private_say( + self, + message: CommandArg[str, Description["The content of the message"]], + ): return Message(message, flags=InteractionFlags.EPHEMERAL) @command(description="How to make embed!") async def pincer_embed(self): - return Embed( - title="Pincer - 0.6.4", - description=( - "🚀 An asynchronous python API wrapper meant to replace" - " discord.py\n> Snappy discord api wrapper written " - "with aiohttp & websockets" + return ( + Embed( + title="Pincer - 0.6.4", + description=( + "🚀 An asynchronous python API wrapper meant to replace" + " discord.py\n> Snappy discord api wrapper written " + "with aiohttp & websockets" + ), + ) + .add_field( + name="**Github Repository**", + value="> https://github.com/Pincer-org/Pincer", ) - ).add_field( - name="**Github Repository**", - value="> https://github.com/Pincer-org/Pincer" - ).set_thumbnail( - url="https://pincer.dev/img/icon.png" - ).set_image( - url=( - "https://repository-images.githubusercontent.com" - "/400871418/045ebf39-7c6e-4c3a-b744-0c3122374203" + .set_thumbnail(url="https://pincer.dev/img/icon.png") + .set_image( + url=( + "https://repository-images.githubusercontent.com" + "/400871418/045ebf39-7c6e-4c3a-b744-0c3122374203" + ) ) ) diff --git a/examples/chat_commands_function_based.py b/examples/chat_commands_function_based.py index 19dac22d..37d1d9fe 100644 --- a/examples/chat_commands_function_based.py +++ b/examples/chat_commands_function_based.py @@ -12,41 +12,48 @@ async def on_ready(self): @command(description="Say something as the bot!") -async def say(message: CommandArg[str, Description["The content of the message"]]): +async def say( + message: CommandArg[str, Description["The content of the message"]] +): return message @command(description="Add two numbers!") async def add( first: CommandArg[int, Description["The first number"]], - second: CommandArg[int, Description["The second number"]] + second: CommandArg[int, Description["The second number"]], ): return f"The addition of `{first}` and `{second}` is `{first + second}`" @command(guild=1324567890) -async def private_say(message: CommandArg[str, Description["The content of the message"]]): +async def private_say( + message: CommandArg[str, Description["The content of the message"]] +): return Message(message, flags=InteractionFlags.EPHEMERAL) @command(description="How to make embed!") async def pincer_embed(): - return Embed( - title="Pincer - 0.6.4", - description=( - "🚀 An asynchronous python API wrapper meant to replace" - " discord.py\n> Snappy discord api wrapper written " - "with aiohttp & websockets" + return ( + Embed( + title="Pincer - 0.6.4", + description=( + "🚀 An asynchronous python API wrapper meant to replace" + " discord.py\n> Snappy discord api wrapper written " + "with aiohttp & websockets" + ), + ) + .add_field( + name="**Github Repository**", + value="> https://github.com/Pincer-org/Pincer", ) - ).add_field( - name="**Github Repository**", - value="> https://github.com/Pincer-org/Pincer" - ).set_thumbnail( - url="https://pincer.dev/img/icon.png" - ).set_image( - url=( - "https://repository-images.githubusercontent.com" - "/400871418/045ebf39-7c6e-4c3a-b744-0c3122374203" + .set_thumbnail(url="https://pincer.dev/img/icon.png") + .set_image( + url=( + "https://repository-images.githubusercontent.com" + "/400871418/045ebf39-7c6e-4c3a-b744-0c3122374203" + ) ) ) From a9d9321d369d0b1823069525392a24115ab8dee6 Mon Sep 17 00:00:00 2001 From: sigmanificient Date: Wed, 24 Nov 2021 00:11:00 +0100 Subject: [PATCH 049/134] :art: Using black formatting on examples --- examples/context.py | 12 +++++------- examples/cooldowns.py | 28 +++++++++++++++------------- examples/guessing_game.py | 14 ++++++++++---- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/examples/context.py b/examples/context.py index 013c10a0..49c07379 100644 --- a/examples/context.py +++ b/examples/context.py @@ -1,19 +1,17 @@ from pincer import Client from pincer.commands import command, CommandArg, Description -from pincer.objects import Embed +from pincer.objects import Embed, MessageContext class Bot(Client): - @command(description="Say something as the bot!") async def say( - self, ctx, - content: CommandArg[str, Description["The content of the message"]] + self, + ctx: MessageContext, + content: CommandArg[str, Description["The content of the message"]], ) -> Embed: # Using the ctx to get the command author - return Embed( - description=f"{ctx.author.user.mention} said {content}" - ) + return Embed(description=f"{ctx.author.user.mention} said {content}") @Client.event async def on_ready(self): diff --git a/examples/cooldowns.py b/examples/cooldowns.py index 68d131a2..c5b4d7ed 100644 --- a/examples/cooldowns.py +++ b/examples/cooldowns.py @@ -51,25 +51,27 @@ async def on_ready(self): @command( # We don't want to send too many requests to our `MEME_URL` so # lets use cooldowns! - # Only allow one request cooldown=1, # For every three seconds cooldown_scale=3, - # And just to make things more clear for our user on what this # command does, lets define a description! - description="Get a random meme!" + description="Get a random meme!", ) async def meme(self): # Fetch our caption and image from our `MEME_URL`. caption, image = await self.get_meme() # Respond with an embed which contains the meme and caption! - return Embed(caption, color=self.random_color()) \ - .set_image(image) \ - .set_footer("Provided by some-random-api.ml", - "https://i.some-random-api.ml/logo.png") + return ( + Embed(caption, color=self.random_color()) + .set_image(image) + .set_footer( + "Provided by some-random-api.ml", + "https://i.some-random-api.ml/logo.png", + ) + ) @Client.event async def on_command_error(self, ctx: MessageContext, error: Exception): @@ -82,14 +84,14 @@ async def on_command_error(self, ctx: MessageContext, error: Exception): return Message( embeds=[ Embed( - "Oops...", - f"The `{ctx.command.app.name}` command can only be used" - f" `{ctx.command.cooldown}` time*(s)* every " - f"`{ctx.command.cooldown_scale}` second*(s)*!", - self.random_color() + title="Oops...", + description=f"The `{ctx.command.app.name}` command can " + f"only be used `{ctx.command.cooldown}` time*(s)* every" + f" `{ctx.command.cooldown_scale}` second*(s)*!", + color=self.random_color(), ) ], - flags=InteractionFlags.EPHEMERAL + flags=InteractionFlags.EPHEMERAL, ) # Oh no, it wasn't a cooldown error. Lets throw it! diff --git a/examples/guessing_game.py b/examples/guessing_game.py index dccdfa08..4cc4054d 100644 --- a/examples/guessing_game.py +++ b/examples/guessing_game.py @@ -6,21 +6,27 @@ class Bot(Client): - @command() # note that the parenthesis are optional async def guess(self, ctx: MessageContext, biggest_number: int): + await ctx.reply( + f"Starting the guessing game!" + f" Pick a number between 0 and {biggest_number}." + ) - await ctx.reply(f"Starting the guessing game! Pick a number between 0 and {biggest_number}.") channel = await self.get_channel(ctx.channel_id) number = random.randint(0, biggest_number) try: - async for next_message, in self.loop_for('on_message', loop_timeout=60): + async for next_message, in self.loop_for( + "on_message", loop_timeout=60 + ): if next_message.author.bot: continue if not next_message.content.isdigit(): - await channel.send(f"{next_message.content} is not a number. Try again!") + await channel.send( + f"{next_message.content} is not a number. Try again!" + ) continue guessed_number = int(next_message.content) From bb4f90fefb7167cbfd98c6ad6f1a4284008bc1cc Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Tue, 23 Nov 2021 21:56:05 -0500 Subject: [PATCH 050/134] :memo: updated docs --- pincer/commands/arg_types.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pincer/commands/arg_types.py b/pincer/commands/arg_types.py index d8b34a1d..363038a2 100644 --- a/pincer/commands/arg_types.py +++ b/pincer/commands/arg_types.py @@ -17,7 +17,7 @@ def __getitem__(cls, args: Union[Tuple, Any]): class CommandArg(metaclass=_CommandTypeMeta): """ - Holds all application command options + Holds the parameters of an application command option .. code-block:: python3 @@ -60,7 +60,7 @@ class Modifier(metaclass=_CommandTypeMeta): class Description(Modifier): """ - Represents the description of an application command option type + Represents the description of an application command option .. code-block:: python3 @@ -85,7 +85,7 @@ def get_payload(self) -> str: class Choice(Modifier): """ - Represents an application command choice + Represents a choice that the user can pick from .. code-block:: python3 @@ -109,7 +109,7 @@ def __init__(self, name, value): class Choices(Modifier): """ - Represents a Group of Application Command Choices + Represents a group of application command choices that a user can pick from .. code-block:: python3 @@ -149,7 +149,7 @@ def get_payload(self) -> List[AppCommandOptionChoice]: class ChannelTypes(Modifier): """ - Represents the channel types application command option type. + Represents a group of channel types that a user can pick from .. code-block:: python3 @@ -178,7 +178,7 @@ def get_payload(self): class MaxValue(Modifier): """ - Represents the channel types application command option type + Represents the max value for a number .. code-block:: python3 @@ -203,7 +203,7 @@ def get_payload(self): class MinValue(Modifier): """ - Represents the channel types application command option type + Represents the minimum value for a number .. code-block:: python3 From 53526291c360e05e2178b5cb9c8ea2a6404b684b Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Wed, 24 Nov 2021 16:23:25 -0500 Subject: [PATCH 051/134] :sparkles: made GuildMember inherit from User --- pincer/middleware/interaction_create.py | 44 ++++---------- pincer/objects/app/interactions.py | 78 +++++++++---------------- pincer/objects/app/mentionable.py | 13 +++++ pincer/objects/guild/member.py | 28 ++++++++- pincer/objects/user/user.py | 16 ++--- 5 files changed, 85 insertions(+), 94 deletions(-) create mode 100644 pincer/objects/app/mentionable.py diff --git a/pincer/middleware/interaction_create.py b/pincer/middleware/interaction_create.py index 6f1d955f..69d3d1da 100644 --- a/pincer/middleware/interaction_create.py +++ b/pincer/middleware/interaction_create.py @@ -4,18 +4,17 @@ from __future__ import annotations import logging -from inspect import isasyncgenfunction, _empty +from inspect import isasyncgenfunction, getfullargspec from typing import Dict, Any from typing import TYPE_CHECKING - from ..commands import ChatCommandHandler from ..core.dispatch import GatewayDispatch -from ..objects import Interaction, MessageContext, AppCommandType +from ..objects import Interaction, MessageContext from ..utils import MISSING, should_pass_cls, Coro, should_pass_ctx from ..utils import get_index from ..utils.conversion import construct_client_dict -from ..utils.signature import get_signature_and_params +from ..utils.signature import get_params, get_signature_and_params if TYPE_CHECKING: from typing import List, Tuple @@ -29,7 +28,6 @@ async def interaction_response_handler( command: Coro, context: MessageContext, interaction: Interaction, - args: List[Any], kwargs: Dict[str, Any] ): """|coro| @@ -47,15 +45,16 @@ async def interaction_response_handler( \\*\\*kwargs : The arguments to be passed to the command. """ + if should_pass_cls(command): + cls_keyword = getfullargspec(command).args[0] + kwargs[cls_keyword] = ChatCommandHandler.managers[command.__module__] + sig, params = get_signature_and_params(command) if should_pass_ctx(sig, params): - args.insert(0, context) - - if should_pass_cls(command): - args.insert(0, ChatCommandHandler.managers[command.__module__]) + kwargs[params[0]] = context if isasyncgenfunction(command): - message = command(*args, **kwargs) + message = command(**kwargs) async for msg in message: if interaction.has_replied: @@ -63,7 +62,7 @@ async def interaction_response_handler( else: await interaction.reply(msg) else: - message = await command(*args, **kwargs) + message = await command(**kwargs) if not interaction.has_replied: await interaction.reply(message) @@ -89,10 +88,7 @@ async def interaction_handler( """ self.throttler.handle(context) - sig, _ = get_signature_and_params(command) - - defaults = {key: value.default for key, - value in sig.items() if value.default is not _empty} + defaults = {param: None for param in get_params(command)} params = {} if interaction.data.options is not MISSING: @@ -100,25 +96,10 @@ async def interaction_handler( opt.name: opt.value for opt in interaction.data.options } - args = [] - - if interaction.data.type is AppCommandType.USER: - # Add User and Member args - args.append(next(iter(interaction.data.resolved.users.values()))) - - if members := interaction.data.resolved.members: - args.append(next(iter(members.values()))) - else: - args.append(MISSING) - - elif interaction.data.type is AppCommandType.MESSAGE: - # Add Message to args - args.append(next(iter(interaction.data.resolved.messages.values()))) - kwargs = {**defaults, **params} await interaction_response_handler( - self, command, context, interaction, args, kwargs + self, command, context, interaction, kwargs ) @@ -149,7 +130,6 @@ async def interaction_create_middleware( interaction: Interaction = Interaction.from_dict( construct_client_dict(self, payload.data) ) - await interaction.build() command = ChatCommandHandler.register.get(interaction.data.name) if command: diff --git a/pincer/objects/app/interactions.py b/pincer/objects/app/interactions.py index bea54ae0..8ba0cf7d 100644 --- a/pincer/objects/app/interactions.py +++ b/pincer/objects/app/interactions.py @@ -3,9 +3,10 @@ from __future__ import annotations -from asyncio import gather, iscoroutine, sleep, ensure_future +from asyncio import sleep, ensure_future from dataclasses import dataclass -from typing import Dict, TYPE_CHECKING, Union, Optional, List +from functools import partial +from typing import Dict, TYPE_CHECKING, Union, Optional, List, T from .command_types import AppCommandOptionType from .interaction_base import InteractionType, CallbackType @@ -142,61 +143,36 @@ class Interaction(APIObject): def __post_init__(self): super().__post_init__() - self._convert_functions = { - AppCommandOptionType.SUB_COMMAND: None, - AppCommandOptionType.SUB_COMMAND_GROUP: None, - - AppCommandOptionType.STRING: str, - AppCommandOptionType.INTEGER: int, - AppCommandOptionType.BOOLEAN: bool, - AppCommandOptionType.NUMBER: float, - - AppCommandOptionType.USER: lambda value: - self._client.get_user( - convert(value, Snowflake.from_string) - ), - AppCommandOptionType.CHANNEL: lambda value: - self._client.get_channel( - convert(value, Snowflake.from_string) - ), - AppCommandOptionType.ROLE: lambda value: - self._client.get_role( - convert(self.guild_id, Snowflake.from_string), - convert(value, Snowflake.from_string) - ), - AppCommandOptionType.MENTIONABLE: None - } - - async def build(self): - """|coro| - - Sets the parameters in the interaction that need information - from the discord API. - """ - if not self.data.options: - return + for option in self.data.options: - await gather( - *map(self.convert, self.data.options) - ) + if option.type is AppCommandOptionType.STRING: + option.value = str(option.value) + elif option.type is AppCommandOptionType.INTEGER: + option.value = int(option.value) + elif option.type is AppCommandOptionType.BOOLEAN: + option.value = bool(option.value) + elif option.type is AppCommandOptionType.NUMBER: + option.value = float(option.value) - async def convert(self, option: AppCommandInteractionDataOption): - """|coro| + elif option.type is AppCommandOptionType.USER: + nv = self.return_type(option, self.data.resolved.members) + nv.set_user_data(self.return_type(option, self.data.resolved.users)) + option.value = nv - Sets an ``AppCommandInteractionDataOption`` value parameter to - the payload type - """ - converter = self._convert_functions.get(option.type) + elif option.type is AppCommandOptionType.CHANNEL: + option.value = self.return_type(option, self.data.resolved.channels) + elif option.type is AppCommandOptionType.ROLE: + option.value = self.return_type(option, self.data.resolved.roles) - if not converter: - raise NotImplementedError( - f"Handling for AppCommandOptionType {option.type} is not " - "implemented" - ) + elif option.type is AppCommandOptionType.MENTIONABLE: + pass - res = converter(option.value) + def convert_type(t: T, option) -> T: + return t(option) - option.value = (await res) if iscoroutine(res) else res + def return_type(self, option, t) -> APIObject: + if option.value in t: + return t[option.value] def convert_to_message_context(self, command): return MessageContext( diff --git a/pincer/objects/app/mentionable.py b/pincer/objects/app/mentionable.py new file mode 100644 index 00000000..4aff2619 --- /dev/null +++ b/pincer/objects/app/mentionable.py @@ -0,0 +1,13 @@ +# Copyright Pincer 2021-Present +# Full MIT License can be found in `LICENSE` at the project root. + +from dataclasses import dataclass + +from ...utils.api_object import APIObject +from ...utils.types import MISSING + + +@dataclass +class Mentionable(APIObject): + user = MISSING + role = MISSING diff --git a/pincer/objects/guild/member.py b/pincer/objects/guild/member.py index a00b3535..becd17a2 100644 --- a/pincer/objects/guild/member.py +++ b/pincer/objects/guild/member.py @@ -36,8 +36,8 @@ class BaseMember(APIObject): hoisted_role: APINullable[:class:`~pincer.utils.snowflake.Snowflake`] The user's top role in the guild. """ - joined_at: Timestamp - roles: List[Snowflake] + joined_at: Timestamp = None + roles: List[Snowflake] = None deaf: bool = MISSING mute: bool = MISSING @@ -76,7 +76,7 @@ class PartialGuildMember(APIObject): @dataclass -class GuildMember(BaseMember, APIObject): +class GuildMember(BaseMember, User, APIObject): """Represents a member which resides in a guild/server. Attributes @@ -107,6 +107,28 @@ class GuildMember(BaseMember, APIObject): user: APINullable[User] = MISSING avatar: APINullable[str] = MISSING + def __post_init__(self): + super().__post_init__() + + if self.user is not MISSING: + self.set_user_data(self.user) + + def set_user_data(self, user: User): + """ + Used to set the user parameters of a GuildMember instance + + user: APINullable[:class:`~pincer.objects.user.user.User`] + A user class to copy the fields from + """ + + # Inspired from this thread + # https://stackoverflow.com/questions/57962873/easiest-way-to-copy-all-fields-from-one-dataclass-instance-to-another + + for key, value in user.__dict__.items(): + setattr(self, key, value) + + self.user = MISSING + @classmethod async def from_id( cls, diff --git a/pincer/objects/user/user.py b/pincer/objects/user/user.py index 418c9291..9d2170cc 100644 --- a/pincer/objects/user/user.py +++ b/pincer/objects/user/user.py @@ -110,7 +110,7 @@ class User(APIObject): Whether the email on this account has been verified """ - id: Snowflake + id: Snowflake = None username: APINullable[str] = MISSING discriminator: APINullable[str] = MISSING @@ -183,15 +183,15 @@ async def get_avatar(self, size: int = 512, ext: str = "png") -> Image: avatar = io.BytesIO(await resp.read()) return Image.open(avatar).convert("RGBA") - def __str__(self): - # TODO: fix docs - """ + # def __str__(self): + # # TODO: fix docs + # """ - Returns - ------- + # Returns + # ------- - """ - return self.username + "#" + self.discriminator + # """ + # return self.username + "#" + self.discriminator @classmethod async def from_id(cls, client: Client, user_id: int) -> User: From 2a1e3fc1e399786394a36fac2134e43b355b4b34 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Wed, 24 Nov 2021 16:46:05 -0500 Subject: [PATCH 052/134] :zap: Fixed incorrect user of Interaction data and added mentionable type --- pincer/commands/commands.py | 2 ++ pincer/objects/__init__.py | 3 ++- pincer/objects/app/__init__.py | 3 ++- pincer/objects/app/interactions.py | 40 ++++++++++++++++++++++++------ pincer/objects/app/mentionable.py | 21 ++++++++++++---- pincer/objects/user/user.py | 14 +++++------ 6 files changed, 61 insertions(+), 22 deletions(-) diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index 2f9bbaff..9d4c8514 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -38,6 +38,7 @@ User, Channel, Guild, + Mentionable, MessageContext, ) from ..objects.app import ( @@ -68,6 +69,7 @@ User: AppCommandOptionType.USER, Channel: AppCommandOptionType.CHANNEL, Role: AppCommandOptionType.ROLE, + Mentionable: AppCommandOptionType.MENTIONABLE } if TYPE_CHECKING: diff --git a/pincer/objects/__init__.py b/pincer/objects/__init__.py index 126de518..f48fc706 100644 --- a/pincer/objects/__init__.py +++ b/pincer/objects/__init__.py @@ -15,6 +15,7 @@ from .app.interactions import ( ResolvedData, InteractionData, Interaction ) +from .app.mentionable import Mentionable from .app.select_menu import SelectOption, SelectMenu from .app.session_start_limit import SessionStartLimit from .app.throttle_scope import ThrottleScope @@ -147,5 +148,5 @@ "User", "UserMessage", "VerificationLevel", "VisibilityType", "VoiceChannel", "VoiceRegion", "VoiceServerUpdateEvent", "VoiceState", "Webhook", "WebhookType", "WebhooksUpdateEvent", "WelcomeScreen", - "WelcomeScreenChannel" + "WelcomeScreenChannel", "Mentionable" ) diff --git a/pincer/objects/app/__init__.py b/pincer/objects/app/__init__.py index 3faac163..c57a56fb 100644 --- a/pincer/objects/app/__init__.py +++ b/pincer/objects/app/__init__.py @@ -12,6 +12,7 @@ from .interaction_base import CallbackType, InteractionType, MessageInteraction from .interaction_flags import InteractionFlags from .interactions import ResolvedData, InteractionData, Interaction +from .mentionable import Mentionable from .select_menu import SelectOption, SelectMenu from .session_start_limit import SessionStartLimit from .throttle_scope import ThrottleScope @@ -25,5 +26,5 @@ "DefaultThrottleHandler", "Intents", "Interaction", "InteractionData", "InteractionFlags", "InteractionType", "MessageInteraction", "ResolvedData", "SelectMenu", "SelectOption", "SessionStartLimit", - "ThrottleInterface", "ThrottleScope" + "ThrottleInterface", "ThrottleScope", "Mentionable" ) diff --git a/pincer/objects/app/interactions.py b/pincer/objects/app/interactions.py index 8ba0cf7d..bf7db999 100644 --- a/pincer/objects/app/interactions.py +++ b/pincer/objects/app/interactions.py @@ -4,12 +4,13 @@ from __future__ import annotations from asyncio import sleep, ensure_future +from contextlib import suppress from dataclasses import dataclass -from functools import partial -from typing import Dict, TYPE_CHECKING, Union, Optional, List, T +from typing import Dict, TYPE_CHECKING, Type, Union, Optional, List, T from .command_types import AppCommandOptionType from .interaction_base import InteractionType, CallbackType +from .mentionable import Mentionable from ..app.select_menu import SelectOption from ..guild.member import GuildMember from ..message.context import MessageContext @@ -18,7 +19,7 @@ from ..user import User from ...exceptions import InteractionDoesNotExist, UseFollowup, \ InteractionAlreadyAcknowledged, NotFoundError, InteractionTimedOut -from ...utils import APIObject, convert +from ...utils import APIObject from ...utils.convert_message import convert_message from ...utils.snowflake import Snowflake from ...utils.types import MISSING @@ -156,24 +157,47 @@ def __post_init__(self): elif option.type is AppCommandOptionType.USER: nv = self.return_type(option, self.data.resolved.members) - nv.set_user_data(self.return_type(option, self.data.resolved.users)) + nv.set_user_data(self.return_type( + option, self.data.resolved.users) + ) option.value = nv elif option.type is AppCommandOptionType.CHANNEL: - option.value = self.return_type(option, self.data.resolved.channels) + option.value = self.return_type( + option, self.data.resolved.channels + ) + elif option.type is AppCommandOptionType.ROLE: - option.value = self.return_type(option, self.data.resolved.roles) + option.value = self.return_type( + option, self.data.resolved.roles + ) elif option.type is AppCommandOptionType.MENTIONABLE: - pass + user = self.return_type(option, self.data.resolved.members) + if user is not MISSING: + user.set_user_data(self.return_type( + option, self.data.resolved.users) + ) + + role = self.return_type( + option, self.data.resolved.roles + ) + + option.value = Mentionable( + user, + role + ) def convert_type(t: T, option) -> T: return t(option) def return_type(self, option, t) -> APIObject: - if option.value in t: + + with suppress(TypeError, KeyError): return t[option.value] + return MISSING + def convert_to_message_context(self, command): return MessageContext( self.member or self.user, diff --git a/pincer/objects/app/mentionable.py b/pincer/objects/app/mentionable.py index 4aff2619..15efdb1d 100644 --- a/pincer/objects/app/mentionable.py +++ b/pincer/objects/app/mentionable.py @@ -3,11 +3,22 @@ from dataclasses import dataclass -from ...utils.api_object import APIObject -from ...utils.types import MISSING +from ...objects.guild.role import Role +from ...objects.user.user import User +from ...utils.types import MISSING, APINullable + +# Inspired by Rust (🚀) enums 🚀 @dataclass -class Mentionable(APIObject): - user = MISSING - role = MISSING +class Mentionable: + user: APINullable[User] = MISSING + role: APINullable[Role] = MISSING + + @property + def is_user(self): + return self.user is not MISSING + + @property + def is_role(self): + return self.role is not MISSING diff --git a/pincer/objects/user/user.py b/pincer/objects/user/user.py index 9d2170cc..a69942cd 100644 --- a/pincer/objects/user/user.py +++ b/pincer/objects/user/user.py @@ -183,15 +183,15 @@ async def get_avatar(self, size: int = 512, ext: str = "png") -> Image: avatar = io.BytesIO(await resp.read()) return Image.open(avatar).convert("RGBA") - # def __str__(self): - # # TODO: fix docs - # """ + def __str__(self): + # TODO: fix docs + """ - # Returns - # ------- + Returns + ------- - # """ - # return self.username + "#" + self.discriminator + """ + return self.username + "#" + self.discriminator @classmethod async def from_id(cls, client: Client, user_id: int) -> User: From 881ebed52590bd1cfc24a6d1d8727db2344cd4bd Mon Sep 17 00:00:00 2001 From: Yohann Boniface Date: Wed, 24 Nov 2021 23:47:38 +0100 Subject: [PATCH 053/134] =?UTF-8?q?=F0=9F=93=9D=20Fixing=20pypi.md=20plann?= =?UTF-8?q?ing=20phase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/PYPI.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/PYPI.md b/docs/PYPI.md index 977de25a..075f8960 100644 --- a/docs/PYPI.md +++ b/docs/PYPI.md @@ -18,7 +18,7 @@ An asynchronous Python API wrapper meant to replace discord.py -## The package is currently within the planning phase +## The package is currently within the pre-alpha phase ## 📌 Links From e3d1d21e56f5c522029d843e1b019b34a402f498 Mon Sep 17 00:00:00 2001 From: Yohann Boniface Date: Wed, 24 Nov 2021 23:48:18 +0100 Subject: [PATCH 054/134] =?UTF-8?q?=F0=9F=93=9D=20more=20buttons=20yay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/PYPI.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/PYPI.md b/docs/PYPI.md index 075f8960..582b341d 100644 --- a/docs/PYPI.md +++ b/docs/PYPI.md @@ -1,20 +1,19 @@ # Pincer - - [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Pincer-org/pincer/badges/quality-score.png?b=main)](https://scrutinizer-ci.com/g/Pincer-org/pincer/?branch=main) [![Build Status](https://scrutinizer-ci.com/g/Pincer-org/Pincer/badges/build.png?b=main)](https://scrutinizer-ci.com/g/Pincer-org/Pincer/build-status/main) -![GitHub repo size](https://img.shields.io/github/repo-size/Pincer-org/Pincer) +[![Documentation Status](https://readthedocs.org/projects/pincer/badge/?version=latest)](https://pincer.readthedocs.io/en/latest/?badge=latest) +[![codecov](https://codecov.io/gh/Pincer-org/Pincer/branch/main/graph/badge.svg?token=T15T34KOQW)](https://codecov.io/gh/Pincer-org/Pincer) +![Lines of code](https://img.shields.io/tokei/lines/github/Pincer-org/Pincer) ![GitHub last commit](https://img.shields.io/github/last-commit/Pincer-org/Pincer) ![GitHub commit activity](https://img.shields.io/github/commit-activity/m/Pincer-org/Pincer) ![GitHub](https://img.shields.io/github/license/Pincer-org/Pincer) -![Code Style](https://img.shields.io/badge/code%20style-pep8-green) ![Discord](https://img.shields.io/discord/881531065859190804) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) An asynchronous Python API wrapper meant to replace discord.py From 7ff87ba66d3b9cf2b0161875cfa4c2e0a425f833 Mon Sep 17 00:00:00 2001 From: sigmanificient Date: Wed, 24 Nov 2021 23:58:46 +0100 Subject: [PATCH 055/134] :memo: Better code line count --- docs/PYPI.md | 2 +- docs/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/PYPI.md b/docs/PYPI.md index 582b341d..56206378 100644 --- a/docs/PYPI.md +++ b/docs/PYPI.md @@ -8,7 +8,7 @@ [![Build Status](https://scrutinizer-ci.com/g/Pincer-org/Pincer/badges/build.png?b=main)](https://scrutinizer-ci.com/g/Pincer-org/Pincer/build-status/main) [![Documentation Status](https://readthedocs.org/projects/pincer/badge/?version=latest)](https://pincer.readthedocs.io/en/latest/?badge=latest) [![codecov](https://codecov.io/gh/Pincer-org/Pincer/branch/main/graph/badge.svg?token=T15T34KOQW)](https://codecov.io/gh/Pincer-org/Pincer) -![Lines of code](https://img.shields.io/tokei/lines/github/Pincer-org/Pincer) +![Lines of code](https://tokei.rs/b1/github/pincer-org/pincer?category=code&path=pincer) ![GitHub last commit](https://img.shields.io/github/last-commit/Pincer-org/Pincer) ![GitHub commit activity](https://img.shields.io/github/commit-activity/m/Pincer-org/Pincer) ![GitHub](https://img.shields.io/github/license/Pincer-org/Pincer) diff --git a/docs/README.md b/docs/README.md index cccf24c1..d65aac8d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,7 +8,7 @@ [![Build Status](https://scrutinizer-ci.com/g/Pincer-org/Pincer/badges/build.png?b=main)](https://scrutinizer-ci.com/g/Pincer-org/Pincer/build-status/main) [![Documentation Status](https://readthedocs.org/projects/pincer/badge/?version=latest)](https://pincer.readthedocs.io/en/latest/?badge=latest) [![codecov](https://codecov.io/gh/Pincer-org/Pincer/branch/main/graph/badge.svg?token=T15T34KOQW)](https://codecov.io/gh/Pincer-org/Pincer) -![Lines of code](https://img.shields.io/tokei/lines/github/Pincer-org/Pincer) +![Lines of code](https://tokei.rs/b1/github/pincer-org/pincer?category=code&path=pincer) ![GitHub last commit](https://img.shields.io/github/last-commit/Pincer-org/Pincer) ![GitHub commit activity](https://img.shields.io/github/commit-activity/m/Pincer-org/Pincer) ![GitHub](https://img.shields.io/github/license/Pincer-org/Pincer) From 14f72350e4b534b420923338d2f16156ab6657ba Mon Sep 17 00:00:00 2001 From: sigmanificient Date: Thu, 25 Nov 2021 00:00:09 +0100 Subject: [PATCH 056/134] :sparkles: Adding `.tokeignore` --- .tokeignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .tokeignore diff --git a/.tokeignore b/.tokeignore new file mode 100644 index 00000000..2350d6f7 --- /dev/null +++ b/.tokeignore @@ -0,0 +1,2 @@ +docs/* +tests/* From b49990f8681dbcb88af8c21f604683fe8844a3f4 Mon Sep 17 00:00:00 2001 From: sigmanificient Date: Thu, 25 Nov 2021 01:17:56 +0100 Subject: [PATCH 057/134] :memo: Adding repo & discord link to the doc --- docs/index.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index c1e59241..92df6c3e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -48,4 +48,12 @@ installing quickstart tutorial/index - api \ No newline at end of file + api + +.. toctree:: + :maxdepth: 10 + :caption: Reference + :hidden: + + Github repository + Discord server From aec9301af279f85d190f45ed83ac3180c1bd7917 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Wed, 24 Nov 2021 16:53:33 -0500 Subject: [PATCH 058/134] :art: changed MISSING to False --- pincer/objects/app/command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pincer/objects/app/command.py b/pincer/objects/app/command.py index 5d0a8f06..96037b63 100644 --- a/pincer/objects/app/command.py +++ b/pincer/objects/app/command.py @@ -83,7 +83,7 @@ class AppCommandOption(APIObject): name: str description: str - required: APINullable[bool] = MISSING + required: bool = False autocomplete: APINullable[bool] = MISSING choices: APINullable[List[AppCommandOptionChoice]] = MISSING options: APINullable[List[AppCommandOption]] = MISSING From 5f2aba4b71637b72531340b06d76c3b666c4cf8d Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 25 Nov 2021 03:25:11 +0000 Subject: [PATCH 059/134] :art: Automatic sorting --- pincer/__init__.py | 9 ++++----- pincer/commands/__init__.py | 6 +++--- pincer/utils/__init__.py | 10 +++++----- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/pincer/__init__.py b/pincer/__init__.py index 2a45fd06..93e5df94 100644 --- a/pincer/__init__.py +++ b/pincer/__init__.py @@ -59,11 +59,10 @@ def __repr__(self) -> str: __version__ = repr(version_info) __all__ = ( - "BadRequestError", "Bot", "ChatCommandHandler", - "Client", "CogAlreadyExists", "CogError", "CogNotFound", - "CommandAlreadyRegistered", "CommandCooldownError", - "CommandDescriptionTooLong", "CommandError", "CommandIsNotCoroutine", - "CommandReturnIsEmpty", "DisallowedIntentsError", + "BadRequestError", "Bot", "ChatCommandHandler", "Client", + "CogAlreadyExists", "CogError", "CogNotFound", "CommandAlreadyRegistered", + "CommandCooldownError", "CommandDescriptionTooLong", "CommandError", + "CommandIsNotCoroutine", "CommandReturnIsEmpty", "DisallowedIntentsError", "DispatchError", "EmbedFieldError", "ForbiddenError", "GatewayConfig", "GatewayError", "HTTPError", "HeartbeatError", "Intents", "InvalidArgumentAnnotation", "InvalidCommandGuild", "InvalidCommandName", diff --git a/pincer/commands/__init__.py b/pincer/commands/__init__.py index 4219ae49..462a7a0d 100644 --- a/pincer/commands/__init__.py +++ b/pincer/commands/__init__.py @@ -7,7 +7,7 @@ ) __all__ = ( - "command", "ChatCommandHandler", "CommandArg", "Description", "Choice", - "Choices", "ChannelTypes", "MaxValue", "MinValue", "user_command", - "message_command" + "ChannelTypes", "ChatCommandHandler", "Choice", "Choices", + "CommandArg", "Description", "MaxValue", "MinValue", "command", + "message_command", "user_command" ) diff --git a/pincer/utils/__init__.py b/pincer/utils/__init__.py index 3f03dbeb..1735a1da 100644 --- a/pincer/utils/__init__.py +++ b/pincer/utils/__init__.py @@ -19,9 +19,9 @@ __all__ = ( - "APINullable", "APIObject", "CheckFunction", "Color", - "Coro", "EventMgr", "HTTPMeta", "MISSING", "MissingType", - "Snowflake", "Task", "TaskScheduler", "Timestamp", "chdir", - "choice_value_types", "convert", "get_index", "get_params", - "get_signature_and_params", "should_pass_cls", "should_pass_ctx" + "APINullable", "APIObject", "CheckFunction", "Color", "Coro", + "EventMgr", "HTTPMeta", "MISSING", "MissingType", "Snowflake", "Task", + "TaskScheduler", "Timestamp", "chdir", "choice_value_types", "convert", + "get_index", "get_params", "get_signature_and_params", "should_pass_cls", + "should_pass_ctx" ) From 2a5757af02b185b3cd51b9225bb6b06dc55101c3 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 25 Nov 2021 03:25:40 +0000 Subject: [PATCH 060/134] :hammer: Automatic update of setup.cfg --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 38d37b54..f58d0fde 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,6 +33,7 @@ classifiers = include_package_data = True packages = pincer + pincer.commands pincer.objects pincer.objects.events pincer.objects.app From b3507facefbc6bfb9284517ee116c10ec7fdde51 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Thu, 25 Nov 2021 14:21:49 -0500 Subject: [PATCH 061/134] :memo: updated docs --- pincer/objects/app/interactions.py | 40 +++++++++++++++++------------- pincer/objects/app/mentionable.py | 19 +++++++++----- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/pincer/objects/app/interactions.py b/pincer/objects/app/interactions.py index bf7db999..50397464 100644 --- a/pincer/objects/app/interactions.py +++ b/pincer/objects/app/interactions.py @@ -6,7 +6,7 @@ from asyncio import sleep, ensure_future from contextlib import suppress from dataclasses import dataclass -from typing import Dict, TYPE_CHECKING, Type, Union, Optional, List, T +from typing import Any, Dict, TYPE_CHECKING, Type, Union, Optional, List, T from .command_types import AppCommandOptionType from .interaction_base import InteractionType, CallbackType @@ -156,11 +156,11 @@ def __post_init__(self): option.value = float(option.value) elif option.type is AppCommandOptionType.USER: - nv = self.return_type(option, self.data.resolved.members) - nv.set_user_data(self.return_type( - option, self.data.resolved.users) + user = self.return_type(option, self.data.resolved.members) + user.set_user_data( + self.return_type(option, self.data.resolved.users) ) - option.value = nv + option.value = user elif option.type is AppCommandOptionType.CHANNEL: option.value = self.return_type( @@ -174,29 +174,35 @@ def __post_init__(self): elif option.type is AppCommandOptionType.MENTIONABLE: user = self.return_type(option, self.data.resolved.members) - if user is not MISSING: + if user: user.set_user_data(self.return_type( option, self.data.resolved.users) ) - role = self.return_type( - option, self.data.resolved.roles - ) - option.value = Mentionable( user, - role + self.return_type( + option, self.data.resolved.roles + ) ) - def convert_type(t: T, option) -> T: - return t(option) - - def return_type(self, option, t) -> APIObject: + def return_type( + self, + option: Snowflake, + data: Dict[Snowflake, Any] + ) -> APIObject: + """ + Returns a value from the option or None if it doesn't exist. + option : :class:`~pincer.utils.types.Snowflake` + Snowflake to search ``data`` for. + data : Dict[:class:`~pincer.utils.types.Snowflake`, Any] + Resolved data to search through. + """ with suppress(TypeError, KeyError): - return t[option.value] + return data[option.value] - return MISSING + return None def convert_to_message_context(self, command): return MessageContext( diff --git a/pincer/objects/app/mentionable.py b/pincer/objects/app/mentionable.py index 15efdb1d..01b9e305 100644 --- a/pincer/objects/app/mentionable.py +++ b/pincer/objects/app/mentionable.py @@ -2,23 +2,30 @@ # Full MIT License can be found in `LICENSE` at the project root. from dataclasses import dataclass +from typing import Optional from ...objects.guild.role import Role from ...objects.user.user import User -from ...utils.types import MISSING, APINullable -# Inspired by Rust (🚀) enums 🚀 @dataclass class Mentionable: - user: APINullable[User] = MISSING - role: APINullable[Role] = MISSING + """ + Represents the Mentionable type + + user : Optional[:class:`~pincer.objects.user.user.User`] + User object returned from a discord interaction + role: Optional[:class:`~pincer.objects.guild.role.Role`] + Role object returned from a discord interaction + """ + user: Optional[User] = None + role: Optional[Role] = None @property def is_user(self): - return self.user is not MISSING + return self.user is not None @property def is_role(self): - return self.role is not MISSING + return self.role is not None From d63c55fc3d4742fb4bd2325a713f01db18d015c8 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Thu, 25 Nov 2021 19:57:15 -0500 Subject: [PATCH 062/134] :bug: put back changes that were accidently reverted --- pincer/middleware/interaction_create.py | 43 ++++++++++++++++++------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/pincer/middleware/interaction_create.py b/pincer/middleware/interaction_create.py index 69d3d1da..06e0b5e3 100644 --- a/pincer/middleware/interaction_create.py +++ b/pincer/middleware/interaction_create.py @@ -4,17 +4,18 @@ from __future__ import annotations import logging -from inspect import isasyncgenfunction, getfullargspec +from inspect import isasyncgenfunction, _empty from typing import Dict, Any from typing import TYPE_CHECKING + from ..commands import ChatCommandHandler from ..core.dispatch import GatewayDispatch -from ..objects import Interaction, MessageContext +from ..objects import Interaction, MessageContext, AppCommandType from ..utils import MISSING, should_pass_cls, Coro, should_pass_ctx from ..utils import get_index from ..utils.conversion import construct_client_dict -from ..utils.signature import get_params, get_signature_and_params +from ..utils.signature import get_signature_and_params if TYPE_CHECKING: from typing import List, Tuple @@ -28,6 +29,7 @@ async def interaction_response_handler( command: Coro, context: MessageContext, interaction: Interaction, + args: List[Any], kwargs: Dict[str, Any] ): """|coro| @@ -45,16 +47,15 @@ async def interaction_response_handler( \\*\\*kwargs : The arguments to be passed to the command. """ - if should_pass_cls(command): - cls_keyword = getfullargspec(command).args[0] - kwargs[cls_keyword] = ChatCommandHandler.managers[command.__module__] - sig, params = get_signature_and_params(command) if should_pass_ctx(sig, params): - kwargs[params[0]] = context + args.insert(0, context) + + if should_pass_cls(command): + args.insert(0, ChatCommandHandler.managers[command.__module__]) if isasyncgenfunction(command): - message = command(**kwargs) + message = command(*args, **kwargs) async for msg in message: if interaction.has_replied: @@ -62,7 +63,7 @@ async def interaction_response_handler( else: await interaction.reply(msg) else: - message = await command(**kwargs) + message = await command(*args, **kwargs) if not interaction.has_replied: await interaction.reply(message) @@ -88,7 +89,10 @@ async def interaction_handler( """ self.throttler.handle(context) - defaults = {param: None for param in get_params(command)} + sig, _ = get_signature_and_params(command) + + defaults = {key: value.default for key, + value in sig.items() if value.default is not _empty} params = {} if interaction.data.options is not MISSING: @@ -96,10 +100,25 @@ async def interaction_handler( opt.name: opt.value for opt in interaction.data.options } + args = [] + + if interaction.data.type is AppCommandType.USER: + # Add User and Member args + args.append(next(iter(interaction.data.resolved.users.values()))) + + if members := interaction.data.resolved.members: + args.append(next(iter(members.values()))) + else: + args.append(MISSING) + + elif interaction.data.type is AppCommandType.MESSAGE: + # Add Message to args + args.append(next(iter(interaction.data.resolved.messages.values()))) + kwargs = {**defaults, **params} await interaction_response_handler( - self, command, context, interaction, kwargs + self, command, context, interaction, args, kwargs ) From c4e63048d56524239134efa4e2eadf6472d49ea7 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Thu, 25 Nov 2021 20:49:03 -0500 Subject: [PATCH 063/134] :memo: updated docs --- docs/README.md | 2 +- docs/pincer.objects.app.rst | 7 +++++++ docs/quickstart.rst | 2 +- pincer/commands/commands.py | 2 +- pincer/objects/app/mentionable.py | 2 ++ 5 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/README.md b/docs/README.md index 49ffe4c7..ab1163a1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -149,7 +149,7 @@ class Bot(Client): # pincer.objects.User - User # pincer.objects.Channel - Channel # pincer.objects.Role - Role - # Mentionable is not implemented + # pincer.objects.Mentionable - Mentionable async def say(self, message: str): return message diff --git a/docs/pincer.objects.app.rst b/docs/pincer.objects.app.rst index c7c9efd5..c717d157 100644 --- a/docs/pincer.objects.app.rst +++ b/docs/pincer.objects.app.rst @@ -164,3 +164,10 @@ DefaultThrottleHandler .. attributetable:: DefaultThrottleHandler .. autoclass:: DefaultThrottleHandler() + +Mentionable +~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: Mentionable + +.. autoclass:: Mentionable() diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 6d6619f5..e557e5fe 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -58,7 +58,7 @@ Available types are as follows: - pincer.objects.User - User - pincer.objects.Channel - Channel - pincer.objects.Role - Role -- Mentionable is not implemented +- pincer.objects.Mentionable - Mentionable .. code-block:: python diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index 9d4c8514..26b42ee6 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -97,7 +97,7 @@ def command( pincer.objects.User - User pincer.objects.Channel - Channel pincer.objects.Role - Role - Mentionable is not implemented + pincer.objects.Mentionable - Mentionable .. code-block:: python3 diff --git a/pincer/objects/app/mentionable.py b/pincer/objects/app/mentionable.py index 01b9e305..6a650dc5 100644 --- a/pincer/objects/app/mentionable.py +++ b/pincer/objects/app/mentionable.py @@ -24,8 +24,10 @@ class Mentionable: @property def is_user(self): + """Returns true if the Mentionable object has a User""" return self.user is not None @property def is_role(self): + """Returns true if the Mentionable object has a Role""" return self.role is not None From b9393617dc8c80ead01f55716ca2f7edcc98df8a Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Thu, 25 Nov 2021 22:54:12 -0500 Subject: [PATCH 064/134] :art: codacity changes --- pincer/objects/app/interactions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pincer/objects/app/interactions.py b/pincer/objects/app/interactions.py index 50397464..8ed9aba2 100644 --- a/pincer/objects/app/interactions.py +++ b/pincer/objects/app/interactions.py @@ -6,7 +6,7 @@ from asyncio import sleep, ensure_future from contextlib import suppress from dataclasses import dataclass -from typing import Any, Dict, TYPE_CHECKING, Type, Union, Optional, List, T +from typing import Any, Dict, TYPE_CHECKING, Union, Optional, List from .command_types import AppCommandOptionType from .interaction_base import InteractionType, CallbackType @@ -186,8 +186,8 @@ def __post_init__(self): ) ) + @staticmethod def return_type( - self, option: Snowflake, data: Dict[Snowflake, Any] ) -> APIObject: From b29cff6e0439e93ab7c07cf08e6f4fc32ae9ae8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Nov 2021 20:04:03 -0800 Subject: [PATCH 065/134] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Merge=20pull=20req?= =?UTF-8?q?uest=20#252=20from=20Pincer-org/dependabot/pip/aiohttp-gte-3.7.?= =?UTF-8?q?4post0-and-lt-4.1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the requirements on [aiohttp](https://github.com/aio-libs/aiohttp) to permit the latest version. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.7.4.post0...v4.0.0a1) --- updated-dependencies: - dependency-name: aiohttp dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index f58d0fde..89b094b5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,7 +46,7 @@ packages = pincer.utils install_requires = websockets>=10.0 - aiohttp>=3.7.4post0,<3.8.0 + aiohttp>=3.7.4post0,<4.1.0 python_requires = >=3.8 [options.extras_require] From f43b2f145ad1c68659e6bbbd776945ebe9be5491 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 26 Nov 2021 04:04:40 +0000 Subject: [PATCH 066/134] :hammer: Automatic update of setup.cfg --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 89b094b5..f58d0fde 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,7 +46,7 @@ packages = pincer.utils install_requires = websockets>=10.0 - aiohttp>=3.7.4post0,<4.1.0 + aiohttp>=3.7.4post0,<3.8.0 python_requires = >=3.8 [options.extras_require] From 34c7705be5b87d2818eaf5911c49d3b5656fc73c Mon Sep 17 00:00:00 2001 From: Endercheif <45527309+Endercheif@users.noreply.github.com> Date: Thu, 25 Nov 2021 20:06:40 -0800 Subject: [PATCH 067/134] =?UTF-8?q?=F0=9F=93=8C=20Set=20correct=20version?= =?UTF-8?q?=20to=20dependancies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Pipfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Pipfile b/Pipfile index 09f62e8f..c4344eda 100644 --- a/Pipfile +++ b/Pipfile @@ -4,9 +4,9 @@ verify_ssl = true name = "pypi" [packages] -websockets = "*" -aiohttp = "*" -Pillow = "*" +websockets = ">=10.0" +aiohttp = ">=3.7.4post0,<3.8.0" +Pillow = "==8.4.0" [dev-packages] flake8 = "==4.0.1" From 4822c84437fe20eb6e845917843cf9454b4dc0fd Mon Sep 17 00:00:00 2001 From: Lunarmagpie <65521138+Lunarmagpie@users.noreply.github.com> Date: Thu, 25 Nov 2021 23:18:16 -0500 Subject: [PATCH 068/134] Update pincer/objects/guild/member.py Co-authored-by: Endercheif <45527309+Endercheif@users.noreply.github.com> --- pincer/objects/guild/member.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pincer/objects/guild/member.py b/pincer/objects/guild/member.py index becd17a2..1e67f0cd 100644 --- a/pincer/objects/guild/member.py +++ b/pincer/objects/guild/member.py @@ -36,8 +36,8 @@ class BaseMember(APIObject): hoisted_role: APINullable[:class:`~pincer.utils.snowflake.Snowflake`] The user's top role in the guild. """ - joined_at: Timestamp = None - roles: List[Snowflake] = None + joined_at: APINullable[Timestamp] = MISSING + roles: APINullable[List[Snowflake]] = MISSING deaf: bool = MISSING mute: bool = MISSING From e91ace4cc72f103a3f680e0ebf0c699164d0b3f4 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Thu, 25 Nov 2021 23:18:33 -0500 Subject: [PATCH 069/134] :recycle: simplified return_type() method --- pincer/objects/app/interactions.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pincer/objects/app/interactions.py b/pincer/objects/app/interactions.py index 8ed9aba2..511e07f2 100644 --- a/pincer/objects/app/interactions.py +++ b/pincer/objects/app/interactions.py @@ -4,7 +4,6 @@ from __future__ import annotations from asyncio import sleep, ensure_future -from contextlib import suppress from dataclasses import dataclass from typing import Any, Dict, TYPE_CHECKING, Union, Optional, List @@ -190,7 +189,7 @@ def __post_init__(self): def return_type( option: Snowflake, data: Dict[Snowflake, Any] - ) -> APIObject: + ) -> Optional[APIObject]: """ Returns a value from the option or None if it doesn't exist. @@ -199,7 +198,7 @@ def return_type( data : Dict[:class:`~pincer.utils.types.Snowflake`, Any] Resolved data to search through. """ - with suppress(TypeError, KeyError): + if data: return data[option.value] return None From ac8dc2dd9ffd161104bfadcf63abd9b819bdf7cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Nov 2021 06:26:06 -0800 Subject: [PATCH 070/134] :arrow_up: Update aiohttp requirement (#257) --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index c4344eda..2ead8d82 100644 --- a/Pipfile +++ b/Pipfile @@ -5,7 +5,7 @@ name = "pypi" [packages] websockets = ">=10.0" -aiohttp = ">=3.7.4post0,<3.8.0" +aiohttp = ">=3.7.4post0,<4.1.0" Pillow = "==8.4.0" [dev-packages] From e4cada5617cce06861c614986863a63ba844e2ff Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Sat, 27 Nov 2021 02:39:26 +0000 Subject: [PATCH 071/134] fix: requirements.txt to reduce vulnerabilities The following vulnerabilities are fixed by pinning transitive dependencies: - https://snyk.io/vuln/SNYK-PYTHON-AIOHTTP-1584144 From 8d3298a6bec0d71bd253f2b886a88742c25ecf9e Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Sat, 27 Nov 2021 01:02:32 -0500 Subject: [PATCH 072/134] :building_construction: middleware no longer has list in second tuple index --- pincer/middleware/activity_join.py | 9 +++++---- pincer/middleware/activity_join_request.py | 7 ++++--- pincer/middleware/activity_spectate.py | 10 +++++----- pincer/middleware/channel_create.py | 9 ++++----- pincer/middleware/channel_delete.py | 4 ++-- pincer/middleware/channel_pins_update.py | 7 ++++--- pincer/middleware/channel_update.py | 4 ++-- pincer/middleware/error.py | 9 +++++---- pincer/middleware/guild_ban_add.py | 4 ++-- pincer/middleware/guild_ban_remove.py | 6 +++--- pincer/middleware/guild_create.py | 4 ++-- pincer/middleware/guild_delete.py | 8 +++++--- pincer/middleware/guild_emojis_update.py | 12 +++++------- pincer/middleware/guild_integrations_update.py | 12 +++++------- pincer/middleware/guild_member_add.py | 9 +++++---- pincer/middleware/guild_member_remove.py | 8 +++++--- pincer/middleware/guild_member_update.py | 8 +++++--- pincer/middleware/guild_members_chunk.py | 8 ++++---- pincer/middleware/guild_role_create.py | 8 +++++--- pincer/middleware/guild_role_delete.py | 8 +++++--- pincer/middleware/guild_role_update.py | 8 +++++--- pincer/middleware/guild_status.py | 7 ++++--- pincer/middleware/guild_stickers_update.py | 12 +++++------- pincer/middleware/guild_update.py | 4 ++-- pincer/middleware/integration_create.py | 13 ++++++++----- pincer/middleware/integration_delete.py | 13 ++++++++----- pincer/middleware/integration_update.py | 13 ++++++++----- pincer/middleware/interaction_create.py | 8 ++++---- pincer/middleware/invite_create.py | 9 +++++---- pincer/middleware/invite_delete.py | 7 ++++--- pincer/middleware/message_create.py | 9 +++++---- pincer/middleware/message_delete.py | 9 +++++---- pincer/middleware/message_delete_bulk.py | 11 +++++++---- pincer/middleware/message_reaction_add.py | 9 +++++---- pincer/middleware/message_reaction_remove.py | 9 +++++---- pincer/middleware/message_reaction_remove_all.py | 9 +++++---- pincer/middleware/message_reaction_remove_emoji.py | 13 ++++++++----- pincer/middleware/message_update.py | 11 ++++++----- pincer/middleware/notification_create.py | 13 ++++++++----- pincer/middleware/payload.py | 6 +++--- pincer/middleware/presence_update.py | 9 +++++---- pincer/middleware/speaking_start.py | 7 ++++--- pincer/middleware/speaking_stop.py | 7 ++++--- pincer/middleware/stage_instance_create.py | 7 ++++--- pincer/middleware/stage_instance_delete.py | 7 ++++--- pincer/middleware/stage_instance_update.py | 7 ++++--- pincer/middleware/thread_create.py | 7 ++++--- pincer/middleware/thread_delete.py | 7 ++++--- pincer/middleware/thread_list_sync.py | 9 +++++---- pincer/middleware/thread_member_update.py | 7 ++++--- pincer/middleware/thread_members_update.py | 9 +++++---- pincer/middleware/thread_update.py | 7 ++++--- pincer/middleware/typing_start.py | 9 +++++---- pincer/middleware/user_update.py | 7 ++++--- pincer/middleware/voice_channel_select.py | 13 ++++++++----- pincer/middleware/voice_connection_status.py | 13 ++++++++----- pincer/middleware/voice_server_update.py | 13 ++++++++----- pincer/middleware/voice_settings_update.py | 13 ++++++++----- pincer/middleware/voice_state_create.py | 7 ++++--- pincer/middleware/voice_state_delete.py | 7 ++++--- pincer/middleware/voice_state_update.py | 7 ++++--- pincer/middleware/webhooks_update.py | 9 +++++---- 62 files changed, 301 insertions(+), 234 deletions(-) diff --git a/pincer/middleware/activity_join.py b/pincer/middleware/activity_join.py index 0b54d9b2..6d7e4b6c 100644 --- a/pincer/middleware/activity_join.py +++ b/pincer/middleware/activity_join.py @@ -24,12 +24,13 @@ async def activity_join_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.activity.ActivityJoinEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.activity.ActivityJoinEvent`] ``on_activity_join`` and an ``ActivityJoinEvent`` - """ - return "on_activity_join", [ + """ # noqa: E501 + return ( + "on_activity_join", ActivityJoinEvent.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export() -> Coro: diff --git a/pincer/middleware/activity_join_request.py b/pincer/middleware/activity_join_request.py index aa8440c7..e19a6ea7 100644 --- a/pincer/middleware/activity_join_request.py +++ b/pincer/middleware/activity_join_request.py @@ -21,12 +21,13 @@ async def activity_join_request_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.user.user.User`]] + Tuple[:class:`str`, :class:`~pincer.objects.user.user.User`] ``on_activity_join_request`` and a ``User`` """ - return "on_activity_join_request", [ + return ( + "on_activity_join_request", User.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export() -> Coro: diff --git a/pincer/middleware/activity_spectate.py b/pincer/middleware/activity_spectate.py index 68a58ed6..43de0043 100644 --- a/pincer/middleware/activity_spectate.py +++ b/pincer/middleware/activity_spectate.py @@ -24,12 +24,12 @@ async def activity_spectate_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.event.activity.ActivitySpectateEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.event.activity.ActivitySpectateEvent`] ``on_activity_spectate`` and an ``ActivitySpectateEvent`` - """ - return "on_activity_spectate", [ - ActivitySpectateEvent.from_dict(construct_client_dict(self, payload.data)) - ] + """ # noqa: E501 + return "on_activity_spectate", ActivitySpectateEvent.from_dict( + construct_client_dict(self, payload.data) + ) def export() -> Coro: diff --git a/pincer/middleware/channel_create.py b/pincer/middleware/channel_create.py index b4b2e200..3ea16a67 100644 --- a/pincer/middleware/channel_create.py +++ b/pincer/middleware/channel_create.py @@ -6,20 +6,18 @@ from typing import TYPE_CHECKING -from ..core.dispatch import GatewayDispatch from ..objects.guild.channel import Channel from ..utils.conversion import construct_client_dict if TYPE_CHECKING: from typing import List, Tuple - from ..core.dispatch import GatewayDispatch def channel_create_middleware( self, payload: GatewayDispatch -) -> Tuple[str, List[Channel]]: +) -> Tuple[str, Channel]: """|coro| Middleware for ``on_channel_creation`` event. @@ -34,9 +32,10 @@ def channel_create_middleware( Tuple[:class:`str`, List[:class:`~pincer.objects.guild.channel.Channel`]] ``on_channel_creation`` and a channel. """ - return "on_channel_creation", [ + return ( + "on_channel_creation", Channel.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export(): diff --git a/pincer/middleware/channel_delete.py b/pincer/middleware/channel_delete.py index 2b7c9f12..4a48b5d1 100644 --- a/pincer/middleware/channel_delete.py +++ b/pincer/middleware/channel_delete.py @@ -20,7 +20,7 @@ async def channel_delete_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.guild.channel.Channel`]] + Tuple[:class:`str`, :class:`~pincer.objects.guild.channel.Channel`] ``on_channel_delete`` and a ``Channel`` """ @@ -32,7 +32,7 @@ async def channel_delete_middleware(self, payload: GatewayDispatch): if old: guild.channels.remove(old) - return "on_channel_delete", [channel] + return "on_channel_delete", channel def export(): diff --git a/pincer/middleware/channel_pins_update.py b/pincer/middleware/channel_pins_update.py index fa8ffbea..f07c89ec 100644 --- a/pincer/middleware/channel_pins_update.py +++ b/pincer/middleware/channel_pins_update.py @@ -19,13 +19,14 @@ async def channel_pins_update_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.guild.channel.Channel`]] + Tuple[:class:`str`, :class:`~pincer.objects.guild.channel.Channel`] ``on_channel_pins_update`` and a ``Channel`` """ - return "on_channel_pins_update", [ + return ( + "on_channel_pins_update", ChannelPinsUpdateEvent.from_dict(payload.data) - ] + ) def export(): diff --git a/pincer/middleware/channel_update.py b/pincer/middleware/channel_update.py index 6ea8c94d..4f645088 100644 --- a/pincer/middleware/channel_update.py +++ b/pincer/middleware/channel_update.py @@ -20,7 +20,7 @@ async def channel_update_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.channel.channel.Channel`]] + Tuple[:class:`str`, :class:`~pincer.objects.channel.channel.Channel`] ``on_channel_update`` and a ``Channel`` """ @@ -33,7 +33,7 @@ async def channel_update_middleware(self, payload: GatewayDispatch): guild.channels.remove(old) guild.channels.append(channel) - return "on_channel_update", [channel] + return "on_channel_update", channel def export(): diff --git a/pincer/middleware/error.py b/pincer/middleware/error.py index 0f7dfc4b..063b31ae 100644 --- a/pincer/middleware/error.py +++ b/pincer/middleware/error.py @@ -21,7 +21,7 @@ def error_middleware( self, payload: GatewayDispatch -) -> Tuple[str, List[DiscordError]]: +) -> Tuple[str, DiscordError]: """|coro| Middleware for ``on_error`` event. @@ -33,14 +33,15 @@ def error_middleware( Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.error.DiscordError`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.error.DiscordError`] ``on_error`` and a ``DiscordError`` """ # noqa: E501 - return "on_error", [ + return ( + "on_error", DiscordError.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export() -> Coro: diff --git a/pincer/middleware/guild_ban_add.py b/pincer/middleware/guild_ban_add.py index f400f29a..ca113892 100644 --- a/pincer/middleware/guild_ban_add.py +++ b/pincer/middleware/guild_ban_add.py @@ -21,13 +21,13 @@ async def guild_ban_add_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.guild.GuildBaAddEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.guild.GuildBaAddEvent`] ``on_guild_ban_add_update`` and a ``GuildBanAddEvent`` """ return ( "on_guild_ban_add", - [GuildBanAddEvent.from_dict(construct_client_dict(self, payload.data))], + GuildBanAddEvent.from_dict(construct_client_dict(self, payload.data)), ) diff --git a/pincer/middleware/guild_ban_remove.py b/pincer/middleware/guild_ban_remove.py index c1f75683..7b67362f 100644 --- a/pincer/middleware/guild_ban_remove.py +++ b/pincer/middleware/guild_ban_remove.py @@ -21,13 +21,13 @@ async def guild_ban_remove_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.guild.GuildBanRemoveEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.guild.GuildBanRemoveEvent`] ``on_guild_ban_remove_update`` and a ``GuildBanRemoveEvent`` - """ + """ # noqa: E501 return ( "on_guild_ban_remove", - [GuildBanRemoveEvent.from_dict(construct_client_dict(self, payload.data))], + GuildBanRemoveEvent.from_dict(construct_client_dict(self, payload.data)) ) diff --git a/pincer/middleware/guild_create.py b/pincer/middleware/guild_create.py index 1467041c..0a151231 100644 --- a/pincer/middleware/guild_create.py +++ b/pincer/middleware/guild_create.py @@ -27,13 +27,13 @@ async def guild_create_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.guild.guild.Guild`]] + Tuple[:class:`str`, :class:`~pincer.objects.guild.guild.Guild`] ``on_guild_create`` and a ``Guild`` """ guild = Guild.from_dict(construct_client_dict(self, payload.data)) self.guilds[guild.id] = guild - return "on_guild_create", [guild] + return "on_guild_create", guild def export(): diff --git a/pincer/middleware/guild_delete.py b/pincer/middleware/guild_delete.py index 910b458e..ee533319 100644 --- a/pincer/middleware/guild_delete.py +++ b/pincer/middleware/guild_delete.py @@ -20,18 +20,20 @@ async def guild_delete_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.guild.guild.UnavailableGuild`]] + Tuple[:class:`str`, :class:`~pincer.objects.guild.guild.UnavailableGuild`] ``on_guild_delete`` and an ``UnavailableGuild`` """ # TODO: Fix docs on line 23 (three lines above) # http://docs.pincer.dev/pincer.middleware#pincer.middleware.guild_delete.guild_delete_middleware - guild = UnavailableGuild.from_dict(construct_client_dict(self, payload.data)) + guild = UnavailableGuild.from_dict( + construct_client_dict(self, payload.data) + ) if guild.id in self.guilds.key(): self.guilds.pop(guild.id) - return "on_guild_delete", [guild] + return "on_guild_delete", guild def export(): diff --git a/pincer/middleware/guild_emojis_update.py b/pincer/middleware/guild_emojis_update.py index f3b31d77..73016d0e 100644 --- a/pincer/middleware/guild_emojis_update.py +++ b/pincer/middleware/guild_emojis_update.py @@ -21,17 +21,15 @@ async def guild_emojis_update_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.guild.GuildEmojisUpdateEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.guild.GuildEmojisUpdateEvent`] ``on_guild_emoji_update`` and a ``GuildEmojisUpdateEvent`` - """ + """ # noqa: E501 return ( "on_guild_emojis_update", - [ - GuildEmojisUpdateEvent.from_dict( - construct_client_dict(self, payload.data) - ) - ], + GuildEmojisUpdateEvent.from_dict( + construct_client_dict(self, payload.data) + ) ) diff --git a/pincer/middleware/guild_integrations_update.py b/pincer/middleware/guild_integrations_update.py index eda3bb07..f71f00e6 100644 --- a/pincer/middleware/guild_integrations_update.py +++ b/pincer/middleware/guild_integrations_update.py @@ -21,17 +21,15 @@ async def guild_integrations_update_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.guild.GuildIntegrationsUpdateEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.guild.GuildIntegrationsUpdateEvent`] ``on_guild_integration_update`` and a ``GuildIntegrationsUpdateEvent`` - """ + """ # noqa: E501 return ( "on_guild_integrations_update", - [ - GuildIntegrationsUpdateEvent.from_dict( - construct_client_dict(self, payload.data) - ) - ], + GuildIntegrationsUpdateEvent.from_dict( + construct_client_dict(self, payload.data) + ) ) diff --git a/pincer/middleware/guild_member_add.py b/pincer/middleware/guild_member_add.py index 7ed761d6..c8f21ce7 100644 --- a/pincer/middleware/guild_member_add.py +++ b/pincer/middleware/guild_member_add.py @@ -24,13 +24,14 @@ async def guild_member_add_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.guild.GuildMemberAddEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.guild.GuildMemberAddEvent`] ``on_guild_member_add`` and a ``GuildMemberAddEvent`` - """ + """ # noqa: E501 - return "on_guild_member_add", [ + return ( + "on_guild_member_add", GuildMemberAddEvent.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export() -> Coro: diff --git a/pincer/middleware/guild_member_remove.py b/pincer/middleware/guild_member_remove.py index f4456970..f7d4f1a6 100644 --- a/pincer/middleware/guild_member_remove.py +++ b/pincer/middleware/guild_member_remove.py @@ -23,13 +23,15 @@ async def guild_member_remove_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.guild.GuildMemberRemoveEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.guild.GuildMemberRemoveEvent`] ``on_guild_member_remove`` and a ``GuildMemberRemoveEvent`` - """ + """ # noqa: E501 return ( "on_guild_member_remove", - [GuildMemberRemoveEvent.from_dict(construct_client_dict(self, payload.data))], + GuildMemberRemoveEvent.from_dict( + construct_client_dict(self, payload.data) + ), ) diff --git a/pincer/middleware/guild_member_update.py b/pincer/middleware/guild_member_update.py index 31376a75..8646af84 100644 --- a/pincer/middleware/guild_member_update.py +++ b/pincer/middleware/guild_member_update.py @@ -24,13 +24,15 @@ async def guild_member_update_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.guild.GuildMemberUpdateEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.guild.GuildMemberUpdateEvent`] ``on_guild_member_update`` and a ``GuildMemberUpdateEvent`` - """ + """ # noqa: E501 return ( "on_guild_member_update", - [GuildMemberUpdateEvent.from_dict(construct_client_dict(self, payload.data))], + GuildMemberUpdateEvent.from_dict( + construct_client_dict(self, payload.data) + ), ) diff --git a/pincer/middleware/guild_members_chunk.py b/pincer/middleware/guild_members_chunk.py index 7d91ebcf..1513f128 100644 --- a/pincer/middleware/guild_members_chunk.py +++ b/pincer/middleware/guild_members_chunk.py @@ -24,15 +24,15 @@ async def guild_member_chunk_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.guild.GuildMembersChunkEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.guild.GuildMembersChunkEvent`] ``on_guild_member_chunk`` and a ``GuildMembersChunkEvent`` - """ + """ # noqa: E501 return ( "on_guild_member_chunk", - [GuildMembersChunkEvent.from_dict( + GuildMembersChunkEvent.from_dict( construct_client_dict(self, payload.data) - )] + ) ) diff --git a/pincer/middleware/guild_role_create.py b/pincer/middleware/guild_role_create.py index afacda49..ba3e1714 100644 --- a/pincer/middleware/guild_role_create.py +++ b/pincer/middleware/guild_role_create.py @@ -21,13 +21,15 @@ async def guild_role_create_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.guild.GuildRoleCreateEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.guild.GuildRoleCreateEvent`] ``on_guild_role_create`` and a ``GuildRoleCreateEvent`` - """ + """ # noqa: E501 return ( "on_guild_role_create", - [GuildRoleCreateEvent.from_dict(construct_client_dict(self, payload.data))], + GuildRoleCreateEvent.from_dict( + construct_client_dict(self, payload.data) + ) ) diff --git a/pincer/middleware/guild_role_delete.py b/pincer/middleware/guild_role_delete.py index f3c794c8..0e8f14c9 100644 --- a/pincer/middleware/guild_role_delete.py +++ b/pincer/middleware/guild_role_delete.py @@ -21,13 +21,15 @@ async def guild_role_delete_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.guild.GuildRoleDeleteEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.guild.GuildRoleDeleteEvent`] ``on_guild_role_delete`` and a ``GuildRoleDeleteEvent`` - """ + """ # noqa: E501 return ( "on_guild_role_delete", - [GuildRoleDeleteEvent.from_dict(construct_client_dict(self, payload.data))], + GuildRoleDeleteEvent.from_dict( + construct_client_dict(self, payload.data) + ), ) diff --git a/pincer/middleware/guild_role_update.py b/pincer/middleware/guild_role_update.py index d16b3f33..a32e1cb0 100644 --- a/pincer/middleware/guild_role_update.py +++ b/pincer/middleware/guild_role_update.py @@ -21,13 +21,15 @@ async def guild_role_update_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.guild.GuildRoleUpdateEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.guild.GuildRoleUpdateEvent`] ``on_guild_role_update`` and a ``GuildRoleUpdateEvent`` - """ + """ # noqa: E501 return ( "on_guild_role_update", - [GuildRoleUpdateEvent.from_dict(construct_client_dict(self, payload.data))], + GuildRoleUpdateEvent.from_dict( + construct_client_dict(self, payload.data) + ), ) diff --git a/pincer/middleware/guild_status.py b/pincer/middleware/guild_status.py index b51088fd..c46eeb77 100644 --- a/pincer/middleware/guild_status.py +++ b/pincer/middleware/guild_status.py @@ -21,12 +21,13 @@ async def guild_status_middleware(self, payload: GatewayDispatch): Return ------ - Tuple[:class:`str`, List[:class:`~pincer.objects.events.guild.GuildStatusEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.guild.GuildStatusEvent`] ``on_guild_status`` and a ``GuildStatusEvent`` """ - return "on_guild_status", [ + return ( + "on_guild_status", GuildStatusEvent.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export() -> Coro: diff --git a/pincer/middleware/guild_stickers_update.py b/pincer/middleware/guild_stickers_update.py index dc363821..5b407aae 100644 --- a/pincer/middleware/guild_stickers_update.py +++ b/pincer/middleware/guild_stickers_update.py @@ -21,17 +21,15 @@ async def guild_stickers_update_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.guild.GuildStickersUpdateEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.guild.GuildStickersUpdateEvent`] ``on_guild_sticker_update`` and a ``GuildStickersUpdateEvent`` - """ + """ # noqa: E501 return ( "on_guild_stickers_update", - [ - GuildStickersUpdateEvent.from_dict( - construct_client_dict(self, payload.data) - ) - ], + GuildStickersUpdateEvent.from_dict( + construct_client_dict(self, payload.data) + ) ) diff --git a/pincer/middleware/guild_update.py b/pincer/middleware/guild_update.py index 9c3f41fe..ca29f0b3 100644 --- a/pincer/middleware/guild_update.py +++ b/pincer/middleware/guild_update.py @@ -21,7 +21,7 @@ async def guild_update_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.guild.guild.Guild`]] + Tuple[:class:`str`, :class:`~pincer.objects.guild.guild.Guild`] ``on_guild_Update`` and an ``Guild`` """ @@ -38,7 +38,7 @@ async def guild_update_middleware(self, payload: GatewayDispatch): )) self.guild[guild.id] = guild - return "on_guild_update", [guild] + return "on_guild_update", guild def export(): diff --git a/pincer/middleware/integration_create.py b/pincer/middleware/integration_create.py index 4ef8fed9..b13cd0bd 100644 --- a/pincer/middleware/integration_create.py +++ b/pincer/middleware/integration_create.py @@ -21,12 +21,15 @@ async def integration_create_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.events.integration.IntegrationCreateEvent`]] + Tuple[:class:`str`, :class:`~pincer.events.integration.IntegrationCreateEvent`] ``on_integration_create`` and an ``IntegrationCreateEvent`` - """ - return "on_integration_create", [ - IntegrationCreateEvent.from_dict(construct_client_dict(self, payload.data)) - ] + """ # noqa: E501 + return ( + "on_integration_create", + IntegrationCreateEvent.from_dict( + construct_client_dict(self, payload.data) + ) + ) def export() -> Coro: diff --git a/pincer/middleware/integration_delete.py b/pincer/middleware/integration_delete.py index 1de14880..28925022 100644 --- a/pincer/middleware/integration_delete.py +++ b/pincer/middleware/integration_delete.py @@ -21,12 +21,15 @@ async def integration_delete_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.events.integration.IntegrationDeleteEvent`]] + Tuple[:class:`str`, :class:`~pincer.events.integration.IntegrationDeleteEvent`] ``on_integration_delete`` and an ``IntegrationDeleteEvent`` - """ - return "on_integration_delete", [ - IntegrationDeleteEvent.from_dict(construct_client_dict(self, payload.data)) - ] + """ # noqa: E501 + return ( + "on_integration_delete", + IntegrationDeleteEvent.from_dict( + construct_client_dict(self, payload.data) + ) + ) def export() -> Coro: diff --git a/pincer/middleware/integration_update.py b/pincer/middleware/integration_update.py index bd299175..b3c9da8a 100644 --- a/pincer/middleware/integration_update.py +++ b/pincer/middleware/integration_update.py @@ -21,12 +21,15 @@ async def integration_update_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.events.integration.IntegrationUpdateEvent`]] + Tuple[:class:`str`, :class:`~pincer.events.integration.IntegrationUpdateEvent`] ``on_integration_update`` and an ``IntegrationUpdateEvent`` - """ - return "on_integration_update", [ - IntegrationUpdateEvent.from_dict(construct_client_dict(self, payload.data)) - ] + """ # noqa: E501 + return ( + "on_integration_update", + IntegrationUpdateEvent.from_dict( + construct_client_dict(self, payload.data) + ) + ) def export() -> Coro: diff --git a/pincer/middleware/interaction_create.py b/pincer/middleware/interaction_create.py index 6f1d955f..a2b5f6d8 100644 --- a/pincer/middleware/interaction_create.py +++ b/pincer/middleware/interaction_create.py @@ -125,7 +125,7 @@ async def interaction_handler( async def interaction_create_middleware( self, payload: GatewayDispatch -) -> Tuple[str, List[Interaction]]: +) -> Tuple[str, Interaction]: """Middleware for ``on_interaction``, which handles command execution. @@ -143,9 +143,9 @@ async def interaction_create_middleware( Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.app.interactions.Interaction`]] + Tuple[:class:`str`, :class:`~pincer.objects.app.interactions.Interaction`] ``on_interaction_create`` and an ``Interaction`` - """ # noqa: E501 + """ interaction: Interaction = Interaction.from_dict( construct_client_dict(self, payload.data) ) @@ -177,7 +177,7 @@ async def interaction_create_middleware( else: raise e - return "on_interaction_create", [interaction] + return "on_interaction_create", interaction def export() -> Coro: diff --git a/pincer/middleware/invite_create.py b/pincer/middleware/invite_create.py index 2f931bf9..951fd535 100644 --- a/pincer/middleware/invite_create.py +++ b/pincer/middleware/invite_create.py @@ -21,12 +21,13 @@ async def invite_create_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.invite.InviteCreateEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.invite.InviteCreateEvent`] ``on_invite_create`` and an ``InviteCreateEvent`` - """ - return "on_invite_create", [ + """ # noqa: E501 + return ( + "on_invite_create", InviteCreateEvent.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export() -> Coro: diff --git a/pincer/middleware/invite_delete.py b/pincer/middleware/invite_delete.py index b12cc3c5..e25900cc 100644 --- a/pincer/middleware/invite_delete.py +++ b/pincer/middleware/invite_delete.py @@ -21,12 +21,13 @@ async def invite_delete_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.events.invite.InviteDeleteEvent`]] + Tuple[:class:`str`, :class:`~pincer.events.invite.InviteDeleteEvent`] ``on_invite_delete`` and an ``InviteDeleteEvent`` """ - return "on_invite_delete", [ + return ( + "on_invite_delete", InviteDeleteEvent.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export() -> Coro: diff --git a/pincer/middleware/message_create.py b/pincer/middleware/message_create.py index 98f7e10b..2db9a65c 100644 --- a/pincer/middleware/message_create.py +++ b/pincer/middleware/message_create.py @@ -18,7 +18,7 @@ async def message_create_middleware( self, payload: GatewayDispatch -) -> Tuple[str, List[UserMessage]]: # noqa: E501 +) -> Tuple[str, UserMessage]: # noqa: E501 """|coro| Middleware for ``on_message`` event. @@ -30,12 +30,13 @@ async def message_create_middleware( Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.message.user_message.UserMessage`]] + Tuple[:class:`str`, :class:`~pincer.objects.message.user_message.UserMessage`] ``on_message`` and a ``UserMessage`` """ # noqa: E501 - return "on_message", [ + return ( + "on_message", UserMessage.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export(): diff --git a/pincer/middleware/message_delete.py b/pincer/middleware/message_delete.py index e0d7b804..f79a2e2f 100644 --- a/pincer/middleware/message_delete.py +++ b/pincer/middleware/message_delete.py @@ -18,7 +18,7 @@ async def on_message_delete_middleware( self, payload: GatewayDispatch -) -> Tuple[str, List[MessageDeleteEvent]]: +) -> Tuple[str, MessageDeleteEvent]: """|coro| Middleware for ``on_message_delete`` event. @@ -31,11 +31,12 @@ async def on_message_delete_middleware( ------- Tuple[:class:`str`, :class:`~pincer.objects.events.message.MessageDeleteEvent`] ``on_message_delete`` and a ``MessageDeleteEvent`` - """ + """ # noqa: E501 - return "on_message_delete", [ + return ( + "on_message_delete", MessageDeleteEvent.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export(): diff --git a/pincer/middleware/message_delete_bulk.py b/pincer/middleware/message_delete_bulk.py index 2f5f9e53..1f6f22ea 100644 --- a/pincer/middleware/message_delete_bulk.py +++ b/pincer/middleware/message_delete_bulk.py @@ -21,12 +21,15 @@ async def message_delete_bulk_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.events.message.MessageDeleteBulkEvent`]] + Tuple[:class:`str`, :class:`~pincer.events.message.MessageDeleteBulkEvent`] ``on_message_delete_bulk`` and an ``MessageDeleteBulkEvent`` """ - return "on_message_delete_bulk", [ - MessageDeleteBulkEvent.from_dict(construct_client_dict(self, payload.data)) - ] + return ( + "on_message_delete_bulk", + MessageDeleteBulkEvent.from_dict( + construct_client_dict(self, payload.data) + ) + ) def export() -> Coro: diff --git a/pincer/middleware/message_reaction_add.py b/pincer/middleware/message_reaction_add.py index c8ee0d92..83283942 100644 --- a/pincer/middleware/message_reaction_add.py +++ b/pincer/middleware/message_reaction_add.py @@ -21,11 +21,12 @@ async def message_reaction_add_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.message.MessageReactionAddEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.message.MessageReactionAddEvent`] ``on_message_reaction_add`` and an ``MessageReactionAddEvent`` - """ + """ # noqa: E501 - return "on_message_reaction_add", [ + return ( + "on_message_reaction_add", MessageReactionAddEvent.from_dict( construct_client_dict( self, @@ -39,7 +40,7 @@ async def message_reaction_add_middleware(self, payload: GatewayDispatch): **payload.data } )) - ] + ) def export(): diff --git a/pincer/middleware/message_reaction_remove.py b/pincer/middleware/message_reaction_remove.py index 6b24367d..2eecc90b 100644 --- a/pincer/middleware/message_reaction_remove.py +++ b/pincer/middleware/message_reaction_remove.py @@ -22,11 +22,12 @@ async def message_reaction_remove_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.message.MessageReactionRemoveEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.message.MessageReactionRemoveEvent`] ``on_message_reaction_remove`` and an ``MessageReactionRemoveEvent`` - """ + """ # noqa: E501 - return "on_message_reaction_remove", [ + return ( + "on_message_reaction_remove", MessageReactionRemoveEvent.from_dict( construct_client_dict( self, @@ -37,7 +38,7 @@ async def message_reaction_remove_middleware(self, payload: GatewayDispatch): **payload.data } )) - ] + ) def export(): diff --git a/pincer/middleware/message_reaction_remove_all.py b/pincer/middleware/message_reaction_remove_all.py index 7aa23a11..9f158deb 100644 --- a/pincer/middleware/message_reaction_remove_all.py +++ b/pincer/middleware/message_reaction_remove_all.py @@ -24,15 +24,16 @@ async def message_reaction_remove_all_middleware( Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.message.MessageReactionRemoveAllEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.message.MessageReactionRemoveAllEvent`] ``on_message_reaction_remove_all`` and an ``MessageReactionRemoveAllEvent`` - """ + """ # noqa: E501 - return "on_message_reaction_remove_all", [ + return ( + "on_message_reaction_remove_all", MessageReactionRemoveAllEvent.from_dict( construct_client_dict(self, payload.data) ) - ] + ) def export(): diff --git a/pincer/middleware/message_reaction_remove_emoji.py b/pincer/middleware/message_reaction_remove_emoji.py index 3e5ae647..6548f65c 100644 --- a/pincer/middleware/message_reaction_remove_emoji.py +++ b/pincer/middleware/message_reaction_remove_emoji.py @@ -9,7 +9,9 @@ from ..utils.conversion import construct_client_dict -async def message_reaction_remove_emoji_middleware(self, payload: GatewayDispatch): +async def message_reaction_remove_emoji_middleware( + self, payload: GatewayDispatch +): """|coro| Middleware for ``on_message_reaction_remove_emoji`` event. @@ -22,11 +24,12 @@ async def message_reaction_remove_emoji_middleware(self, payload: GatewayDispatc Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.message.MessageReactionRemoveEmojiEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.message.MessageReactionRemoveEmojiEvent`] ``on_message_reaction_remove_emoji`` and an ``MessageReactionRemoveEmojiEvent`` - """ + """ # noqa: E501 - return "on_message_reaction_remove_emoji", [ + return ( + "on_message_reaction_remove_emoji", MessageReactionRemoveEmojiEvent.from_dict( construct_client_dict( self, @@ -37,7 +40,7 @@ async def message_reaction_remove_emoji_middleware(self, payload: GatewayDispatc **payload.data } )) - ] + ) def export(): diff --git a/pincer/middleware/message_update.py b/pincer/middleware/message_update.py index 2411575f..11763c27 100644 --- a/pincer/middleware/message_update.py +++ b/pincer/middleware/message_update.py @@ -19,7 +19,7 @@ async def message_update_middleware( self, payload: GatewayDispatch -) -> Tuple[str, List[UserMessage]]: +) -> Tuple[str, UserMessage]: """|coro| @@ -33,12 +33,13 @@ async def message_update_middleware( Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.message.user_message.UserMessage`]] + Tuple[:class:`str`, :class:`~pincer.objects.message.user_message.UserMessage`] ``on_message_update`` and a ``UserMessage`` - """ - return "on_message_update", [ + """ # noqa: E501 + return ( + "on_message_update", UserMessage.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export(): diff --git a/pincer/middleware/notification_create.py b/pincer/middleware/notification_create.py index 41789f7c..a3ba412f 100644 --- a/pincer/middleware/notification_create.py +++ b/pincer/middleware/notification_create.py @@ -24,14 +24,17 @@ async def notification_create_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.notification.NotificationCreateEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.notification.NotificationCreateEvent`] ``on_notification_create`` and a ``NotificationCreateEvent`` - """ + """ # noqa: E501 channel_id: int = payload.data.get("channel_id") payload.data["message"]["channel_id"] = channel_id - return "on_notification_create", [ - NotificationCreateEvent.from_dict(construct_client_dict(self, payload.data)) - ] + return ( + "on_notification_create", + NotificationCreateEvent.from_dict( + construct_client_dict(self, payload.data) + ) + ) def export() -> Coro: diff --git a/pincer/middleware/payload.py b/pincer/middleware/payload.py index 2c3358c1..6bbcf17b 100644 --- a/pincer/middleware/payload.py +++ b/pincer/middleware/payload.py @@ -13,7 +13,7 @@ async def payload_middleware( payload: GatewayDispatch -) -> Tuple[str, List[GatewayDispatch]]: +) -> Tuple[str, GatewayDispatch]: """Invoked when basically anything is received from gateway. Parameters @@ -24,10 +24,10 @@ async def payload_middleware( Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.core.dispatch.GatewayDispatch`]] + Tuple[:class:`str`, :class:`~pincer.core.dispatch.GatewayDispatch`] ``on_payload`` and a ``payload`` """ - return "on_payload", [payload] + return "on_payload", payload def export(): diff --git a/pincer/middleware/presence_update.py b/pincer/middleware/presence_update.py index bdf216d1..b1ef9ff8 100644 --- a/pincer/middleware/presence_update.py +++ b/pincer/middleware/presence_update.py @@ -21,12 +21,13 @@ async def presence_update_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.user.voice_state.PresenceUpdateEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.user.voice_state.PresenceUpdateEvent`] ``on_presence_update`` and a ``PresenceUpdateEvent`` - """ - return "on_presence_update", [ + """ # noqa: E501 + return ( + "on_presence_update", PresenceUpdateEvent.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export() -> Coro: diff --git a/pincer/middleware/speaking_start.py b/pincer/middleware/speaking_start.py index 10351c8e..ccecc88f 100644 --- a/pincer/middleware/speaking_start.py +++ b/pincer/middleware/speaking_start.py @@ -21,12 +21,13 @@ async def speaking_start_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`SpeakingStartEvent`]] + Tuple[:class:`str`, :class:`SpeakingStartEvent`] ``on_speaking_start`` and a ``SpeakingStartEvent`` """ - return "on_speaking_start", [ + return ( + "on_speaking_start", SpeakingStartEvent.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export() -> Coro: diff --git a/pincer/middleware/speaking_stop.py b/pincer/middleware/speaking_stop.py index db4f9e81..d97e142d 100644 --- a/pincer/middleware/speaking_stop.py +++ b/pincer/middleware/speaking_stop.py @@ -21,12 +21,13 @@ async def speaking_stop_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`Snowflake`]] + Tuple[:class:`str`, :class:`Snowflake`] ``on_speaking_stop`` and a ``Snowflake`` (user_id) """ - return "on_speaking_stop", [ + return ( + "on_speaking_stop", SpeakingStopEvent.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export() -> Coro: diff --git a/pincer/middleware/stage_instance_create.py b/pincer/middleware/stage_instance_create.py index 5ff66547..2648e104 100644 --- a/pincer/middleware/stage_instance_create.py +++ b/pincer/middleware/stage_instance_create.py @@ -20,13 +20,14 @@ def stage_instance_create_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.guild.stage.StageInstance`]] + Tuple[:class:`str`, :class:`~pincer.objects.guild.stage.StageInstance`] ``on_stage_instance_create`` and a ``StageInstance`` """ - return "on_stage_instance_create", [ + return ( + "on_stage_instance_create", StageInstance.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export() -> Coro: diff --git a/pincer/middleware/stage_instance_delete.py b/pincer/middleware/stage_instance_delete.py index 1f343d65..ce465adc 100644 --- a/pincer/middleware/stage_instance_delete.py +++ b/pincer/middleware/stage_instance_delete.py @@ -20,13 +20,14 @@ def stage_instance_delete_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.guild.stage.StageInstance`]] + Tuple[:class:`str`, :class:`~pincer.objects.guild.stage.StageInstance`] ``on_stage_instance_delete`` and a ``StageInstance`` """ - return "on_stage_instance_delete", [ + return ( + "on_stage_instance_delete", StageInstance.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export() -> Coro: diff --git a/pincer/middleware/stage_instance_update.py b/pincer/middleware/stage_instance_update.py index ebab9d2e..c57f82e9 100644 --- a/pincer/middleware/stage_instance_update.py +++ b/pincer/middleware/stage_instance_update.py @@ -20,13 +20,14 @@ def stage_instance_update_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.guild.stage.StageInstance`]] + Tuple[:class:`str`, :class:`~pincer.objects.guild.stage.StageInstance`] ``on_stage_instance_update`` and a ``StageInstance`` """ - return "on_stage_instance_update", [ + return ( + "on_stage_instance_update", StageInstance.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export() -> Coro: diff --git a/pincer/middleware/thread_create.py b/pincer/middleware/thread_create.py index 0275780d..e10a65b3 100644 --- a/pincer/middleware/thread_create.py +++ b/pincer/middleware/thread_create.py @@ -20,13 +20,14 @@ def thread_create_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.guild.channel.Channel`]] + Tuple[:class:`str`, :class:`~pincer.objects.guild.channel.Channel`] ``on_thread_create`` and an ``Channel`` """ - return "on_thread_create", [ + return ( + "on_thread_create", Channel.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export(): diff --git a/pincer/middleware/thread_delete.py b/pincer/middleware/thread_delete.py index 9f8697b2..230ad57f 100644 --- a/pincer/middleware/thread_delete.py +++ b/pincer/middleware/thread_delete.py @@ -20,13 +20,14 @@ async def thread_delete_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.guild.channel.Channel`]] + Tuple[:class:`str`, :class:`~pincer.objects.guild.channel.Channel`] ``on_thread_delete`` and an ``Channel`` """ - return "on_thread_delete", [ + return ( + "on_thread_delete", Channel.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export(): diff --git a/pincer/middleware/thread_list_sync.py b/pincer/middleware/thread_list_sync.py index a81d26b2..8432f370 100644 --- a/pincer/middleware/thread_list_sync.py +++ b/pincer/middleware/thread_list_sync.py @@ -25,9 +25,9 @@ async def thread_list_sync(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.guild.events.thread.ThreadListSyncEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.guild.events.thread.ThreadListSyncEvent`] ``on_thread_list_sync`` and an ``ThreadListSyncEvent`` - """ + """ # noqa: E501 threads: List[Channel] = [ Channel.from_dict(construct_client_dict(self, thread)) @@ -39,11 +39,12 @@ async def thread_list_sync(self, payload: GatewayDispatch): for member in payload.data.pop("members") ] - return "on_thread_list_sync", [ + return ( + "on_thread_list_sync", ThreadListSyncEvent.from_dict( {"threads": threads, "members": members, **payload.data} ) - ] + ) def export(): diff --git a/pincer/middleware/thread_member_update.py b/pincer/middleware/thread_member_update.py index 077b4052..c15fad58 100644 --- a/pincer/middleware/thread_member_update.py +++ b/pincer/middleware/thread_member_update.py @@ -21,11 +21,12 @@ async def thread_member_update_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.guild.thread.ThreadMember`]] + Tuple[:class:`str`, :class:`~pincer.objects.guild.thread.ThreadMember`] ``on_thread_member_update`` and an ``ThreadMember`` """ - return "on_thread_member_update", [ + return ( + "on_thread_member_update", ThreadMember.from_dict(construct_client_dict( self, { @@ -33,7 +34,7 @@ async def thread_member_update_middleware(self, payload: GatewayDispatch): **payload.data } )) - ] + ) def export(): diff --git a/pincer/middleware/thread_members_update.py b/pincer/middleware/thread_members_update.py index 732b3308..2c9644b8 100644 --- a/pincer/middleware/thread_members_update.py +++ b/pincer/middleware/thread_members_update.py @@ -24,9 +24,9 @@ async def thread_members_update_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.thread.ThreadMembersUpdateEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.thread.ThreadMembersUpdateEvent`] ``on_thread_members_update`` and an ``ThreadMembersUpdateEvent`` - """ + """ # noqa: E501 added_members: List[ThreadMember] = [ ThreadMember.from_dict(construct_client_dict( @@ -39,14 +39,15 @@ async def thread_members_update_middleware(self, payload: GatewayDispatch): for added_member in payload.data.pop("added_members") ] - return "on_thread_members_update", [ + return ( + "on_thread_members_update", ThreadMembersUpdateEvent.from_dict( { "added_members": added_members, **payload.data } ) - ] + ) def export(): diff --git a/pincer/middleware/thread_update.py b/pincer/middleware/thread_update.py index 0b8f2a32..56be14e2 100644 --- a/pincer/middleware/thread_update.py +++ b/pincer/middleware/thread_update.py @@ -21,13 +21,14 @@ async def thread_update_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.guild.channel.Channel`]] + Tuple[:class:`str`, :class:`~pincer.objects.guild.channel.Channel`] ``on_thread_update`` and an ``Channel`` """ - return "on_thread_update", [ + return ( + "on_thread_update", Channel.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export(): diff --git a/pincer/middleware/typing_start.py b/pincer/middleware/typing_start.py index f294123d..dc65c090 100644 --- a/pincer/middleware/typing_start.py +++ b/pincer/middleware/typing_start.py @@ -21,12 +21,13 @@ async def typing_start_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.typing_start.TypingStartEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.typing_start.TypingStartEvent`] ``on_typing_start`` and a ``TypingStartEvent`` - """ - return "on_typing_start", [ + """ # noqa: E501 + return ( + "on_typing_start", TypingStartEvent.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export() -> Coro: diff --git a/pincer/middleware/user_update.py b/pincer/middleware/user_update.py index 3214c404..1731fe67 100644 --- a/pincer/middleware/user_update.py +++ b/pincer/middleware/user_update.py @@ -21,12 +21,13 @@ async def user_update_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.user.user.User`]] + Tuple[:class:`str`, :class:`~pincer.objects.user.user.User`] ``on_user_update`` and a ``User`` """ - return "on_user_update", [ + return ( + "on_user_update", User.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export() -> Coro: diff --git a/pincer/middleware/voice_channel_select.py b/pincer/middleware/voice_channel_select.py index 0cac9e22..dc8883e7 100644 --- a/pincer/middleware/voice_channel_select.py +++ b/pincer/middleware/voice_channel_select.py @@ -21,12 +21,15 @@ async def voice_channel_select_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.voice.VoiceChannelSelectEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.voice.VoiceChannelSelectEvent`] ``on_voice_channel_select`` and a ``VoiceChannelSelectEvent`` - """ - return "on_voice_channel_select", [ - VoiceChannelSelectEvent.from_dict(construct_client_dict(self, payload.data)) - ] + """ # noqa: E501 + return ( + "on_voice_channel_select", + VoiceChannelSelectEvent.from_dict( + construct_client_dict(self, payload.data) + ) + ) def export() -> Coro: diff --git a/pincer/middleware/voice_connection_status.py b/pincer/middleware/voice_connection_status.py index a03f2794..48c9d7cc 100644 --- a/pincer/middleware/voice_connection_status.py +++ b/pincer/middleware/voice_connection_status.py @@ -21,12 +21,15 @@ async def voice_connection_status_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.voice.VoiceConnectionStatusEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.voice.VoiceConnectionStatusEvent`] ``on_voice_connection_status`` and a ``VoiceConnectionStatusEvent`` - """ - return "on_voice_connection_status", [ - VoiceConnectionStatusEvent.from_dict(construct_client_dict(self, payload.data)) - ] + """ # noqa: E501 + return ( + "on_voice_connection_status", + VoiceConnectionStatusEvent.from_dict( + construct_client_dict(self, payload.data) + ) + ) def export() -> Coro: diff --git a/pincer/middleware/voice_server_update.py b/pincer/middleware/voice_server_update.py index 4aa90272..c98c7c31 100644 --- a/pincer/middleware/voice_server_update.py +++ b/pincer/middleware/voice_server_update.py @@ -21,12 +21,15 @@ async def voice_server_update_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.voice.VoiceServerUpdateEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.voice.VoiceServerUpdateEvent`] ``on_voice_server_update`` and a ``VoiceServerUpdateEvent`` - """ - return "on_voice_server_update", [ - VoiceServerUpdateEvent.from_dict(construct_client_dict(self, payload.data)) - ] + """ # noqa: E501 + return ( + "on_voice_server_update", + VoiceServerUpdateEvent.from_dict( + construct_client_dict(self, payload.data) + ) + ) def export() -> Coro: diff --git a/pincer/middleware/voice_settings_update.py b/pincer/middleware/voice_settings_update.py index dac27b09..f0a067b4 100644 --- a/pincer/middleware/voice_settings_update.py +++ b/pincer/middleware/voice_settings_update.py @@ -21,12 +21,15 @@ async def voice_settings_update_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.VoiceSettingsUpdateEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.VoiceSettingsUpdateEvent`] ``on_voice_settings_update`` and a ``VoiceSettingsUpdateEvent`` - """ - return "on_voice_settings_update", [ - VoiceSettingsUpdateEvent.from_dict(construct_client_dict(self, payload.data)) - ] + """ # noqa: E501 + return ( + "on_voice_settings_update", + VoiceSettingsUpdateEvent.from_dict( + construct_client_dict(self, payload.data) + ) + ) def export() -> Coro: diff --git a/pincer/middleware/voice_state_create.py b/pincer/middleware/voice_state_create.py index be8d7c9b..e878aaf2 100644 --- a/pincer/middleware/voice_state_create.py +++ b/pincer/middleware/voice_state_create.py @@ -21,12 +21,13 @@ async def voice_state_create_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.user.voice_state.VoiceState`]] + Tuple[:class:`str`, :class:`~pincer.objects.user.voice_state.VoiceState`] ``on_voice_state_create`` and a ``VoiceState`` """ - return "on_voice_state_create", [ + return ( + "on_voice_state_create", VoiceState.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export() -> Coro: diff --git a/pincer/middleware/voice_state_delete.py b/pincer/middleware/voice_state_delete.py index 6eb3a336..9c4faf18 100644 --- a/pincer/middleware/voice_state_delete.py +++ b/pincer/middleware/voice_state_delete.py @@ -21,12 +21,13 @@ async def voice_state_delete_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.user.voice_state.VoiceState`]] + Tuple[:class:`str`, :class:`~pincer.objects.user.voice_state.VoiceState`] ``on_voice_state_delete`` and a ``VoiceState`` """ - return "on_voice_state_delete", [ + return ( + "on_voice_state_delete", VoiceState.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export() -> Coro: diff --git a/pincer/middleware/voice_state_update.py b/pincer/middleware/voice_state_update.py index abc12a2b..cb05abf0 100644 --- a/pincer/middleware/voice_state_update.py +++ b/pincer/middleware/voice_state_update.py @@ -32,14 +32,15 @@ async def voice_state_update_middleware( Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.user.voice_state.VoiceState`]] + Tuple[:class:`str`, :class:`~pincer.objects.user.voice_state.VoiceState`] ``on_voice_state_update`` and a ``VoiceState`` """ # noqa: E501 - return "on_voice_state_update", [ + return ( + "on_voice_state_update", VoiceState.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export(): diff --git a/pincer/middleware/webhooks_update.py b/pincer/middleware/webhooks_update.py index b26175a4..faaaadcd 100644 --- a/pincer/middleware/webhooks_update.py +++ b/pincer/middleware/webhooks_update.py @@ -23,12 +23,13 @@ async def webhooks_update_middleware(self, payload: GatewayDispatch): Returns ------- - Tuple[:class:`str`, List[:class:`~pincer.objects.events.webhook.WebhooksUpdateEvent`]] + Tuple[:class:`str`, :class:`~pincer.objects.events.webhook.WebhooksUpdateEvent`] ``on_webhooks_update`` and a ``WebhooksUpdateEvent`` - """ - return "on_webhooks_update", [ + """ # noqa: E501 + return ( + "on_webhooks_update", WebhooksUpdateEvent.from_dict(construct_client_dict(self, payload.data)) - ] + ) def export(): From 55f64ec84f965f14a6bf2a0af6d35a4b15d37a1e Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Sat, 27 Nov 2021 01:11:07 -0500 Subject: [PATCH 073/134] :art: updated Client and Event Mgr to work cleanly with middleware changes --- pincer/client.py | 11 +++++------ pincer/utils/event_mgr.py | 34 +++++++++++++++++----------------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/pincer/client.py b/pincer/client.py index 0efc9bdd..919e3af1 100644 --- a/pincer/client.py +++ b/pincer/client.py @@ -488,14 +488,13 @@ async def handle_middleware( ) next_call = get_index(extractable, 0, "") - arguments = get_index(extractable, 1, []) - params = get_index(extractable, 2, {}) + ret_object = get_index(extractable, 1, None) if next_call is None: raise RuntimeError(f"Middleware `{key}` has not been registered.") return ( - (next_call, arguments, params) + (next_call, ret_object) if next_call.startswith("on_") else await self.handle_middleware( payload, next_call, *arguments, **params @@ -546,12 +545,12 @@ async def process_event(self, name: str, payload: GatewayDispatch): what specifically happened. """ try: - key, args, kwargs = await self.handle_middleware(payload, name) + key, ret_object = await self.handle_middleware(payload, name) - self.event_mgr.process_events(key, *args) + self.event_mgr.process_events(key, ret_object) if calls := self.get_event_coro(key): - self.execute_event(calls, *args, **kwargs) + self.execute_event(calls, ret_object) except Exception as e: await self.execute_error(e) diff --git a/pincer/utils/event_mgr.py b/pincer/utils/event_mgr.py index 759c5f88..383e1733 100644 --- a/pincer/utils/event_mgr.py +++ b/pincer/utils/event_mgr.py @@ -18,7 +18,7 @@ class _Processable(ABC): @abstractmethod - def process(self, event_name: str, *args): + def process(self, event_name: str, event_value: Any): """ Method that is ran when an event is received from discord. @@ -26,8 +26,8 @@ def process(self, event_name: str, *args): ---------- event_name : str The name of the event. - *args : Any - Arguments to evaluate check with. + event_value : Any + Object to evaluate check with. Returns ------- @@ -35,20 +35,20 @@ def process(self, event_name: str, *args): Whether the event can be set """ - def matches_event(self, event_name: str, *args): + def matches_event(self, event_name: str, event_value: Any): """ Parameters ---------- event_name : str Name of event. - *args : Any - Arguments to eval check with. + event_value : Any + Object to eval check with. """ if self.event_name != event_name: return False if self.check: - return self.check(*args) + return self.check(event_value) return True @@ -100,7 +100,7 @@ async def wait(self): """Waits until ``self.event`` is set.""" await self.event.wait() - def process(self, event_name: str, *args) -> bool: + def process(self, event_name: str, event_value: Any) -> bool: # TODO: fix docs """ @@ -113,8 +113,8 @@ def process(self, event_name: str, *args) -> bool: ------- """ - if self.matches_event(event_name, *args): - self.return_value = args + if self.matches_event(event_name, event_value): + self.return_value = event_value self.event.set() @@ -151,7 +151,7 @@ def __init__(self, event_name: str, check: CheckFunction) -> None: self.events = deque() self.wait = Event() - def process(self, event_name: str, *args): + def process(self, event_name: str, event_value: Any): # TODO: fix docs """ @@ -167,8 +167,8 @@ def process(self, event_name: str, *args): if not self.can_expand: return - if self.matches_event(event_name, *args): - self.events.append(args) + if self.matches_event(event_name, event_value): + self.events.append(event_value) self.wait.set() async def get_next(self): @@ -196,17 +196,17 @@ class EventMgr: def __init__(self): self.event_list: List[_Processable] = [] - def process_events(self, event_name, *args): + def process_events(self, event_name, event_value): """ Parameters ---------- event_name : str The name of the event to be processed. - *args : Any - The arguments returned from the middleware for this event. + event_value : Any + The object returned from the middleware for this event. """ for event in self.event_list: - event.process(event_name, *args) + event.process(event_name, event_value) async def wait_for( self, From 0ec9de36941b6f045f684434577b83e3a24f8d0a Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Sat, 27 Nov 2021 13:42:46 -0500 Subject: [PATCH 074/134] :art: made id APINullable --- pincer/objects/user/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pincer/objects/user/user.py b/pincer/objects/user/user.py index a69942cd..691f7e4b 100644 --- a/pincer/objects/user/user.py +++ b/pincer/objects/user/user.py @@ -110,7 +110,7 @@ class User(APIObject): Whether the email on this account has been verified """ - id: Snowflake = None + id: APINullable[Snowflake] = MISSING username: APINullable[str] = MISSING discriminator: APINullable[str] = MISSING From 5f53cd3f41a70213ebdf007ea5fc51011bb78b75 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Sat, 27 Nov 2021 15:46:41 -0500 Subject: [PATCH 075/134] :sparkles: Added rate limiter --- pincer/core/http.py | 13 +++++++++ pincer/core/ratelimiter.py | 56 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 pincer/core/ratelimiter.py diff --git a/pincer/core/http.py b/pincer/core/http.py index 3be933c1..3d831439 100644 --- a/pincer/core/http.py +++ b/pincer/core/http.py @@ -10,6 +10,7 @@ from aiohttp import ClientSession, ClientResponse from . import __package__ +from .ratelimiter import RateLimiter from .._config import GatewayConfig from ..exceptions import ( NotFoundError, BadRequestError, NotModifiedError, UnauthorizedError, @@ -74,6 +75,7 @@ def __init__(self, token: str, *, version: int = None, ttl: int = 5): headers: Dict[str, str] = { "Authorization": f"Bot {token}", } + self.__rate_limiter = RateLimiter() self.__session: ClientSession = ClientSession(headers=headers) self.__http_exceptions: Dict[int, HTTPError] = { @@ -148,6 +150,11 @@ async def __send( # TODO: Adjust to work non-json types _log.debug(f"{method.__name__.upper()} {endpoint} | {data}") + await self.__rate_limiter.wait_until_not_ratelimited( + endpoint, + method + ) + url = f"{self.url}/{endpoint}" async with method( url, @@ -196,6 +203,11 @@ async def __handle_response( (Eg set to 1 for 1 max retry) """ _log.debug(f"Received response for {endpoint} | {await res.text()}") + + self.__rate_limiter.save_response_bucket( + endpoint, method, res.headers + ) + if res.ok: if res.status == 204: _log.debug( @@ -218,6 +230,7 @@ async def __handle_response( _log.exception( f"RateLimitError: {res.reason}." + f" The scope is {res.headers.get('X-RateLimit-Scope')}" f" Retrying in {timeout} seconds" ) await sleep(timeout) diff --git a/pincer/core/ratelimiter.py b/pincer/core/ratelimiter.py new file mode 100644 index 00000000..71bc1431 --- /dev/null +++ b/pincer/core/ratelimiter.py @@ -0,0 +1,56 @@ +# Copyright Pincer 2021-Present +# Full MIT License can be found in `LICENSE` at the project root. + +from asyncio import sleep +from dataclasses import dataclass +from time import time +from typing import Dict + + +@dataclass +class Bucket: + limit: int + remaining: int + reset: float + reset_after: float + time_cached: float + + +class RateLimiter: + def __init__(self) -> None: + self.bucket_map: Dict[str, str] = {} + self.buckets: Dict[str, Bucket] = {} + + def save_response_bucket( + self, + endpoint: str, + method, + header: Dict + ): + + if bucket_id := header.get("X-RateLimit-Bucket"): + self.bucket_map[endpoint, method] = bucket_id + + self.buckets[bucket_id] = Bucket( + limit=int(header["X-RateLimit-Limit"]), + remaining=int(header["X-RateLimit-Remaining"]), + reset=float(header["X-RateLimit-Reset"]), + reset_after=float(header["X-RateLimit-Reset-After"]), + time_cached=time() + ) + + async def wait_until_not_ratelimited( + self, + endpoint: str, + method + ): + bucket_id = self.bucket_map.get((endpoint, method), None) + + if bucket_id is None: + return + + bucket = self.buckets[bucket_id] + cur_time = time() + + if bucket.remaining == 0: + await sleep(cur_time - bucket.time_cached + bucket.reset_after) From e11a38f82dbc1ab6dea04a1dcef4f49770e44fa6 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Sat, 27 Nov 2021 16:32:14 -0500 Subject: [PATCH 076/134] :art: fixed some flake8 errors and added typehints --- pincer/core/http.py | 5 +++-- pincer/core/ratelimiter.py | 12 +++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/pincer/core/http.py b/pincer/core/http.py index 3d831439..2888f517 100644 --- a/pincer/core/http.py +++ b/pincer/core/http.py @@ -48,7 +48,8 @@ class HTTPClient: Attributes ---------- url: :class:`str` - ``f"https://discord.com/api/v{version}"`` "Base url for all HTTP requests" + ``f"https://discord.com/api/v{version}"`` + "Base url for all HTTP requests" max_tts: :class:`int` Max amount of attempts after error code 5xx """ @@ -64,7 +65,7 @@ def __init__(self, token: str, *, version: int = None, ttl: int = 5): version: The discord API version. - See ``_. + See `` ttl: Max amount of attempts after error code 5xx """ diff --git a/pincer/core/ratelimiter.py b/pincer/core/ratelimiter.py index 71bc1431..6828a202 100644 --- a/pincer/core/ratelimiter.py +++ b/pincer/core/ratelimiter.py @@ -1,10 +1,16 @@ # Copyright Pincer 2021-Present # Full MIT License can be found in `LICENSE` at the project root. +from __future__ import annotations + from asyncio import sleep from dataclasses import dataclass from time import time -from typing import Dict +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Dict + from .http import HttpCallable @dataclass @@ -24,7 +30,7 @@ def __init__(self) -> None: def save_response_bucket( self, endpoint: str, - method, + method: HttpCallable, header: Dict ): @@ -42,7 +48,7 @@ def save_response_bucket( async def wait_until_not_ratelimited( self, endpoint: str, - method + method: HttpCallable ): bucket_id = self.bucket_map.get((endpoint, method), None) From 4d0abec91d75e6e4888babd5f5d508b291c507fc Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Sat, 27 Nov 2021 17:14:47 -0500 Subject: [PATCH 077/134] :loud_sound: added logging to rate limiter --- pincer/core/ratelimiter.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/pincer/core/ratelimiter.py b/pincer/core/ratelimiter.py index 6828a202..d68aa5e5 100644 --- a/pincer/core/ratelimiter.py +++ b/pincer/core/ratelimiter.py @@ -5,6 +5,7 @@ from asyncio import sleep from dataclasses import dataclass +import logging from time import time from typing import TYPE_CHECKING @@ -12,6 +13,8 @@ from typing import Dict from .http import HttpCallable +_log = logging.getLogger(__name__) + @dataclass class Bucket: @@ -45,6 +48,8 @@ def save_response_bucket( time_cached=time() ) + _log.info("Rate limit bucket detected with ID %s.", bucket_id) + async def wait_until_not_ratelimited( self, endpoint: str, @@ -59,4 +64,17 @@ async def wait_until_not_ratelimited( cur_time = time() if bucket.remaining == 0: - await sleep(cur_time - bucket.time_cached + bucket.reset_after) + sleep_time = cur_time - bucket.time_cached + bucket.reset_after + + _log.info( + "Waiting for %ss until rate limit for bucket %s is over.", + sleep_time, + bucket_id + ) + + await sleep(sleep_time) + + _log.info( + "Message sent. Bucket %s rate limit ended.", + bucket_id + ) From b7b91d972cbc5b2bfc73bd692e2503e26e710c9c Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Sat, 27 Nov 2021 19:50:30 -0500 Subject: [PATCH 078/134] :memo: updated docs --- docs/pincer.core.rst | 14 +++++++ pincer/core/__init__.py | 4 +- pincer/core/ratelimiter.py | 78 ++++++++++++++++++++++++++++++-------- 3 files changed, 80 insertions(+), 16 deletions(-) diff --git a/docs/pincer.core.rst b/docs/pincer.core.rst index 11fdf26a..d42318f6 100644 --- a/docs/pincer.core.rst +++ b/docs/pincer.core.rst @@ -47,3 +47,17 @@ HTTPClient .. autoclass:: HTTPClient() :exclude-members: __send, __handle_response + +Rate Limiting +------------- + +Bucket +~~~~~~ + +.. autoclass:: Bucket() + +RateLimiter +~~~~~~~~~~~ + +.. attributetable:: RateLimiter +.. autoclass:: RateLimiter() diff --git a/pincer/core/__init__.py b/pincer/core/__init__.py index 0534dca5..9379611e 100644 --- a/pincer/core/__init__.py +++ b/pincer/core/__init__.py @@ -5,8 +5,10 @@ from .gateway import Dispatcher from .heartbeat import Heartbeat from .http import HTTPClient +from .ratelimiter import RateLimiter, Bucket __all__ = ( - "Dispatcher", "GatewayDispatch", "HTTPClient", "Heartbeat" + "Dispatcher", "GatewayDispatch", "HTTPClient", "Heartbeat", "RateLimiter", + "Bucket" ) diff --git a/pincer/core/ratelimiter.py b/pincer/core/ratelimiter.py index d68aa5e5..5d6a520b 100644 --- a/pincer/core/ratelimiter.py +++ b/pincer/core/ratelimiter.py @@ -7,7 +7,7 @@ from dataclasses import dataclass import logging from time import time -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Tuple if TYPE_CHECKING: from typing import Dict @@ -18,6 +18,21 @@ @dataclass class Bucket: + """Represents a rate limit bucket + + Attributes + ---------- + limit : int + The number of requests that can be made. + remaining : int + The number of remaining requests that can be made. + reset : float + Epoch time at which rate limit resets. + reset_after : float + Total time in seconds until rate limit resets. + time_cached : float + Time since epoch when this bucket was last saved. + """ limit: int remaining: int reset: float @@ -26,8 +41,17 @@ class Bucket: class RateLimiter: + """Prevents ``user`` rate limits + Attributes + ---------- + bucket_map : Tuple[str, :class:`~pincer.core.http.HttpCallable`] + Maps endpoints and methods to a rate limit bucket + buckets : Dict[str, :class:`~pincer.core.ratelimiter.Bucket`] + Dictionary of buckets + """ + def __init__(self) -> None: - self.bucket_map: Dict[str, str] = {} + self.bucket_map: Dict[Tuple[str, HttpCallable], str] = {} self.buckets: Dict[str, Bucket] = {} def save_response_bucket( @@ -36,28 +60,52 @@ def save_response_bucket( method: HttpCallable, header: Dict ): + """ + Parameters + ---------- + endpoint : str + The endpoint + method : :class:`~pincer.core.http.HttpCallable` + The method used on the endpoint (Eg. ``Get``, ``Post``, ``Patch``) + header : :class:`aiohttp.typedefs.CIMultiDictProxy` + The headers from the response + """ + bucket_id = header.get("X-RateLimit-Bucket") + + if not bucket_id: + return - if bucket_id := header.get("X-RateLimit-Bucket"): - self.bucket_map[endpoint, method] = bucket_id + self.bucket_map[endpoint, method] = bucket_id - self.buckets[bucket_id] = Bucket( - limit=int(header["X-RateLimit-Limit"]), - remaining=int(header["X-RateLimit-Remaining"]), - reset=float(header["X-RateLimit-Reset"]), - reset_after=float(header["X-RateLimit-Reset-After"]), - time_cached=time() - ) + self.buckets[bucket_id] = Bucket( + limit=int(header["X-RateLimit-Limit"]), + remaining=int(header["X-RateLimit-Remaining"]), + reset=float(header["X-RateLimit-Reset"]), + reset_after=float(header["X-RateLimit-Reset-After"]), + time_cached=time() + ) - _log.info("Rate limit bucket detected with ID %s.", bucket_id) + _log.info("Rate limit bucket detected with ID %s.", bucket_id) async def wait_until_not_ratelimited( self, endpoint: str, method: HttpCallable ): - bucket_id = self.bucket_map.get((endpoint, method), None) - - if bucket_id is None: + """ + Waits until the response no longer needs to be blocked to prevent a + 429 response because of ``user`` rate limits. + + Parameters + ---------- + endpoint : str + The endpoint + method : :class:`~pincer.core.http.HttpCallable` + The method used on the endpoint (Eg. ``Get``, ``Post``, ``Patch``) + """ + bucket_id = self.bucket_map.get((endpoint, method)) + + if not bucket_id: return bucket = self.buckets[bucket_id] From 30de21654eb175fbe6d13a2bba1a2f530ebfd806 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Sat, 27 Nov 2021 20:11:22 -0500 Subject: [PATCH 079/134] :art: fix typo --- pincer/core/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pincer/core/http.py b/pincer/core/http.py index 2888f517..043ce800 100644 --- a/pincer/core/http.py +++ b/pincer/core/http.py @@ -231,7 +231,7 @@ async def __handle_response( _log.exception( f"RateLimitError: {res.reason}." - f" The scope is {res.headers.get('X-RateLimit-Scope')}" + f" The scope is {res.headers.get('X-RateLimit-Scope')}." f" Retrying in {timeout} seconds" ) await sleep(timeout) From 72b4048eec8436143e9694584c9b274ef1feed04 Mon Sep 17 00:00:00 2001 From: Yohann Boniface Date: Sun, 28 Nov 2021 11:49:14 +0100 Subject: [PATCH 080/134] =?UTF-8?q?=F0=9F=94=96=200.13.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pincer/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pincer/__init__.py b/pincer/__init__.py index 93e5df94..23885a24 100644 --- a/pincer/__init__.py +++ b/pincer/__init__.py @@ -55,7 +55,7 @@ def __repr__(self) -> str: ) -version_info = VersionInfo(0, 12, 1) +version_info = VersionInfo(0, 13, 0) __version__ = repr(version_info) __all__ = ( From c013d1a3dfdf10ad8831244ba0f2d570dedbe55b Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Sun, 28 Nov 2021 10:50:01 +0000 Subject: [PATCH 081/134] :hammer: Automatic update of setup.cfg --- VERSION | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index aac2daca..51de3305 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.12.1 \ No newline at end of file +0.13.0 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index f58d0fde..19d5d2b8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pincer -version = 0.12.1 +version = 0.13.0 description = Discord API wrapper rebuild from scratch. long_description = file: docs/PYPI.md long_description_content_type = text/markdown From 75be6bd615c593969c6d2faf392252399cdf87cb Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 28 Nov 2021 12:30:33 +0100 Subject: [PATCH 082/134] :bug: changed to font used --- .gitignore | 8 +++++++- examples/tweet_generator/Segoe_UI.ttf | Bin 34161 -> 0 bytes examples/tweet_generator/Segoe_UI_Bold.ttf | Bin 36052 -> 0 bytes examples/tweet_generator/tweet_generator.py | 18 ++++++++++++++---- 4 files changed, 21 insertions(+), 5 deletions(-) delete mode 100644 examples/tweet_generator/Segoe_UI.ttf delete mode 100644 examples/tweet_generator/Segoe_UI_Bold.ttf diff --git a/.gitignore b/.gitignore index bdb3cdb2..3ce326d8 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,10 @@ MANIFEST .coverage # IDE Ext's -.history/ \ No newline at end of file +.history/ + +# tweet_generator example +examples/tweet_generator/NotoSans-Bold.ttf +examples/tweet_generator/NotoSans-BoldItalic.ttf +examples/tweet_generator/NotoSans-Italic.ttf +examples/tweet_generator/NotoSans-Regular.ttf \ No newline at end of file diff --git a/examples/tweet_generator/Segoe_UI.ttf b/examples/tweet_generator/Segoe_UI.ttf deleted file mode 100644 index 01b1ca8db3988502a9da31e5d508bfcd85c79586..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34161 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 diff --git a/examples/tweet_generator/Segoe_UI_Bold.ttf b/examples/tweet_generator/Segoe_UI_Bold.ttf deleted file mode 100644 index 49fdf2773d926c85fd00ab44e2cd3748d08d5b69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36052 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 diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index b68552c4..8455d5c0 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -1,11 +1,20 @@ import re import textwrap +import os +import sys from datetime import datetime from PIL import Image, ImageFont, ImageDraw, ImageOps from pincer import command, Client, Descripted from pincer.objects import Message, Embed, MessageContext +# you need to manually download the font files and put them into the folder +# ./examples/tweet_generator/ to make the script works using this link: +# https://fonts.google.com/share?selection.family=Noto%20Sans:wght@400;700 +if not all(font in os.listdir() for font in ["NotoSans-Regular.ttf", "NotoSans-Bold.ttf"]): + sys.exit() + + class Bot(Client): @Client.event async def on_ready(self): @@ -17,6 +26,7 @@ async def on_ready(self): @command( name="twitter", description="to create fake tweets", + guild=690604075775164437 ) async def twitter( self, ctx: MessageContext, content: Descripted[str, "..."] @@ -56,9 +66,9 @@ async def twitter( ) # add the fonts - font = ImageFont.truetype("Segoe_UI.ttf", 40) - font_small = ImageFont.truetype("Segoe_UI.ttf", 30) - font_bold = ImageFont.truetype("Segoe_UI_Bold.ttf", 40) + font = ImageFont.truetype("NotoSans-Regular.ttf", 40) + font_small = ImageFont.truetype("NotoSans-Regular.ttf", 30) + font_bold = ImageFont.truetype("NotoSans-Bold.ttf", 40) # write the name and username on the Image draw = ImageDraw.Draw(tweet) @@ -150,4 +160,4 @@ def trans_paste(fg_img, bg_img, alpha=1.0, box=(0, 0)): # Of course we have to run our client, you can replace the # XXXYOURBOTTOKENHEREXXX with your token, or dynamically get it # through a dotenv/env. - Bot("XXXYOURBOTTOKENHEREXXX").run() + Bot("OTA1MzczMDYwMjcyNjMxODI4.YYJIXg.Jvr4NseqVed1lPliN1kyJiNqnO0").run() From 1110ff6306ff7a403ea993e6d6797449b49cd64e Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 28 Nov 2021 12:32:55 +0100 Subject: [PATCH 083/134] :ambulance: hide my token --- examples/tweet_generator/tweet_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index 8455d5c0..3fabb4ba 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -160,4 +160,4 @@ def trans_paste(fg_img, bg_img, alpha=1.0, box=(0, 0)): # Of course we have to run our client, you can replace the # XXXYOURBOTTOKENHEREXXX with your token, or dynamically get it # through a dotenv/env. - Bot("OTA1MzczMDYwMjcyNjMxODI4.YYJIXg.Jvr4NseqVed1lPliN1kyJiNqnO0").run() + Bot("XXXYOURBOTTOKENHEREXXX").run() From 5b00ca23efc68a24a4fc6b1e0acbf8f349e9afff Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 28 Nov 2021 12:35:29 +0100 Subject: [PATCH 084/134] :fire: delete `name=twitter` line requested by Endercheif --- examples/tweet_generator/tweet_generator.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index 3fabb4ba..1ce53aa5 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -24,9 +24,7 @@ async def on_ready(self): ) @command( - name="twitter", description="to create fake tweets", - guild=690604075775164437 ) async def twitter( self, ctx: MessageContext, content: Descripted[str, "..."] From 20e36962e1e9d462262cf6778ac6f7c613823d23 Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 28 Nov 2021 12:38:16 +0100 Subject: [PATCH 085/134] :art: using f-string --- examples/tweet_generator/tweet_generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index 1ce53aa5..671e733a 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -20,7 +20,7 @@ class Bot(Client): async def on_ready(self): print( f"Started client on {self.bot}\n" - "Registered commands: " + ", ".join(self.chat_commands) + f"Registered commands: {', '.join(self.chat_commands)}" ) @command( @@ -36,7 +36,7 @@ async def twitter( re.compile(r"(<@!(\d+)>)"), message ): message = message.replace( - text_match, "@%s" % await self.get_user(user_id) + text_match, f"@{await self.get_user(user_id)}" ) if len(message) > 280: From 441bc40a2654ad35cec3a28efe8ac3260fed8a84 Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 28 Nov 2021 13:38:01 +0100 Subject: [PATCH 086/134] :bug: using composite commands --- examples/tweet_generator/tweet_generator.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index 671e733a..78feed16 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -4,7 +4,8 @@ import sys from datetime import datetime from PIL import Image, ImageFont, ImageDraw, ImageOps -from pincer import command, Client, Descripted +from pincer import command, Client +from pincer.commands import CommandArg, Description from pincer.objects import Message, Embed, MessageContext @@ -25,9 +26,10 @@ async def on_ready(self): @command( description="to create fake tweets", + guild=690604075775164437 ) async def twitter( - self, ctx: MessageContext, content: Descripted[str, "..."] + self, ctx: MessageContext, content: CommandArg[str, Description["The content of the message"]] ): await ctx.interaction.ack() @@ -158,4 +160,4 @@ def trans_paste(fg_img, bg_img, alpha=1.0, box=(0, 0)): # Of course we have to run our client, you can replace the # XXXYOURBOTTOKENHEREXXX with your token, or dynamically get it # through a dotenv/env. - Bot("XXXYOURBOTTOKENHEREXXX").run() + Bot("OTA1MzczMDYwMjcyNjMxODI4.YYJIXg.oegS9NPilP449bk_BCKIpYWjq5Y").run() From 9b8e8ce972e6d7b1f53e6b995ba1b07ff1c8fba9 Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 28 Nov 2021 13:39:27 +0100 Subject: [PATCH 087/134] :fire: re-hide my token... --- examples/tweet_generator/tweet_generator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index 78feed16..778f5e8f 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -26,7 +26,6 @@ async def on_ready(self): @command( description="to create fake tweets", - guild=690604075775164437 ) async def twitter( self, ctx: MessageContext, content: CommandArg[str, Description["The content of the message"]] @@ -160,4 +159,4 @@ def trans_paste(fg_img, bg_img, alpha=1.0, box=(0, 0)): # Of course we have to run our client, you can replace the # XXXYOURBOTTOKENHEREXXX with your token, or dynamically get it # through a dotenv/env. - Bot("OTA1MzczMDYwMjcyNjMxODI4.YYJIXg.oegS9NPilP449bk_BCKIpYWjq5Y").run() + Bot("XXXYOURBOTTOKENHEREXXX").run() From ca90b4f3e365b941935aad628723cfcd97b851e0 Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 28 Nov 2021 14:21:10 +0100 Subject: [PATCH 088/134] :art: requested changes in `trans_paste` requested by Lunarmagpie --- examples/tweet_generator/tweet_generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index 778f5e8f..35e2b698 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -145,12 +145,12 @@ async def twitter( # https://stackoverflow.com/a/53663233/15485584 -def trans_paste(fg_img, bg_img, alpha=1.0, box=(0, 0)): +def trans_paste(fg_img, bg_img, box=(0, 0)): """ paste an image into one another """ fg_img_trans = Image.new("RGBA", fg_img.size) - fg_img_trans = Image.blend(fg_img_trans, fg_img, alpha) + fg_img_trans = Image.blend(fg_img_trans, fg_img, 1.0) bg_img.paste(fg_img_trans, box, fg_img_trans) return bg_img From a17b8c67c0450d0ffd5b0c2fd2cd3594f0534284 Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 28 Nov 2021 14:30:51 +0100 Subject: [PATCH 089/134] :see_no_evil: improve syntax --- .gitignore | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 3ce326d8..607c5c67 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,4 @@ MANIFEST .history/ # tweet_generator example -examples/tweet_generator/NotoSans-Bold.ttf -examples/tweet_generator/NotoSans-BoldItalic.ttf -examples/tweet_generator/NotoSans-Italic.ttf -examples/tweet_generator/NotoSans-Regular.ttf \ No newline at end of file +examples/tweet_generator/*.ttf \ No newline at end of file From 345d01820e4882c017aaf6ab5c33296be959918b Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 28 Nov 2021 14:55:04 +0100 Subject: [PATCH 090/134] :art: extract the full tweet creation to another method to shorter the command one as requested by Sigmaficient https://github.com/Pincer-org/Pincer/pull/241#discussion_r753722642 --- examples/tweet_generator/tweet_generator.py | 142 +++++++++++--------- 1 file changed, 80 insertions(+), 62 deletions(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index 35e2b698..98f8968a 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -32,97 +32,48 @@ async def twitter( ): await ctx.interaction.ack() - message = content for text_match, user_id in re.findall( - re.compile(r"(<@!(\d+)>)"), message + re.compile(r"(<@!(\d+)>)"), content ): - message = message.replace( + content = content.replace( text_match, f"@{await self.get_user(user_id)}" ) - if len(message) > 280: + if len(content) > 280: return "A tweet can be at maximum 280 characters long" - # wrap the message to be multi-line - message = textwrap.wrap(message, 38) - # download the profile picture and convert it into Image object avatar = (await ctx.author.user.get_avatar()).resize((128, 128)) - - # modify profile picture to be circular - mask = Image.new("L", (128, 128), 0) - draw = ImageDraw.Draw(mask) - draw.ellipse((0, 0, 128, 128), fill=255) - avatar = ImageOps.fit(avatar, mask.size, centering=(0.5, 0.5)) - avatar.putalpha(mask) + avatar = circular_avatar(avatar) # create the tweet by pasting the profile picture into a white image tweet = trans_paste( avatar, - # background - Image.new("RGBA", (800, 250 + 50 * len(message)), (255, 255, 255)), + Image.new("RGBA", (800, 250 + 50 * len(textwrap.wrap(content, 38))), (255, 255, 255)), box=(15, 15), ) # add the fonts - font = ImageFont.truetype("NotoSans-Regular.ttf", 40) + font_normal = ImageFont.truetype("NotoSans-Regular.ttf", 40) font_small = ImageFont.truetype("NotoSans-Regular.ttf", 30) font_bold = ImageFont.truetype("NotoSans-Bold.ttf", 40) # write the name and username on the Image draw = ImageDraw.Draw(tweet) draw.text( - (180, 20), str(ctx.author.user), fill=(0, 0, 0), font=font_bold + (180, 20), str(ctx.author.user), + fill=(0, 0, 0), font=font_bold ) draw.text( (180, 70), - "@" + ctx.author.user.username, - fill=(120, 120, 120), - font=font, + f"@{ctx.author.user.username}", + fill=(120, 120, 120), font=font_normal, ) - # write the content of the tweet on the Image - message = "\n".join(message).split(" ") - result = [] - - # generate a dict to set were the text need to be in different color. - # for example, if a word starts with '@' it will be write in blue. - # example: - # [ - # {'color': (0, 0, 0), 'text': 'hello world '}, - # {'color': (0, 154, 234), 'text': '@drawbu'} - # ] - for word in message: - for index, text in enumerate(word.splitlines()): - - text += "\n" if index != len(word.split("\n")) - 1 else " " - - if not result: - result.append({"color": (0, 0, 0), "text": text}) - continue - - if not text.startswith("@"): - if result[-1:][0]["color"] == (0, 0, 0): - result[-1:][0]["text"] += text - continue - - result.append({"color": (0, 0, 0), "text": text}) - continue - - result.append({"color": (0, 154, 234), "text": text}) + content = add_color_to_mentions(content) # write the text - draw = ImageDraw.Draw(tweet) - x = 30 - y = 170 - for text in result: - y -= font.getsize(" ")[1] - for l_index, line in enumerate(text["text"].splitlines()): - if l_index: - x = 30 - y += font.getsize(" ")[1] - draw.text((x, y), line, fill=text["color"], font=font) - x += font.getsize(line)[0] + tweet = draw_multicolored_text(tweet, content, font_normal) # write the footer draw.text( @@ -144,9 +95,9 @@ async def twitter( ) -# https://stackoverflow.com/a/53663233/15485584 def trans_paste(fg_img, bg_img, box=(0, 0)): """ + https://stackoverflow.com/a/53663233/15485584 paste an image into one another """ fg_img_trans = Image.new("RGBA", fg_img.size) @@ -155,6 +106,73 @@ def trans_paste(fg_img, bg_img, box=(0, 0)): return bg_img +def circular_avatar(avatar): + mask = Image.new("L", (128, 128), 0) + draw = ImageDraw.Draw(mask) + draw.ellipse((0, 0, 128, 128), fill=255) + avatar = ImageOps.fit(avatar, mask.size, centering=(0.5, 0.5)) + avatar.putalpha(mask) + return avatar + + +def add_color_to_mentions(message): + """ + generate a dict to set were the text need to be in different colors. + if a word starts with '@' it will be write in blue. + + Parameters + ---------- + message: the text + + Returns + ------- + a list with all colors selected + example: + [ + {'color': (0, 0, 0), 'text': 'hello world '}, + {'color': (0, 154, 234), 'text': '@drawbu'} + ] + + """ + message = textwrap.wrap(message, 38) + message = "\n".join(message).split(" ") + result = [] + for word in message: + for index, text in enumerate(word.splitlines()): + + text += "\n" if index != len(word.split("\n")) - 1 else " " + + if not result: + result.append({"color": (0, 0, 0), "text": text}) + continue + + if not text.startswith("@"): + if result[-1:][0]["color"] == (0, 0, 0): + result[-1:][0]["text"] += text + continue + + result.append({"color": (0, 0, 0), "text": text}) + continue + + result.append({"color": (0, 154, 234), "text": text}) + return result + + +def draw_multicolored_text(image, message, font): + draw = ImageDraw.Draw(image) + x = 30 + y = 170 + for text in message: + y -= font.getsize(" ")[1] + for l_index, line in enumerate(text["text"].splitlines()): + if l_index: + x = 30 + y += font.getsize(" ")[1] + draw.text((x, y), line, fill=text["color"], font=font) + x += font.getsize(line)[0] + return image + + if __name__ == "__main__": # Of course we have to run our client, you can replace the # XXXYOURBOTTOKENHEREXXX with your token, or dynamically get it From efb92593f500a607618f8b5eedfa0bcb79c23d99 Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 28 Nov 2021 16:50:45 +0100 Subject: [PATCH 091/134] :art: each parameter on a separate line, the line's too long https://github.com/Pincer-org/Pincer/pull/241#discussion_r757901061 --- examples/tweet_generator/tweet_generator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index 98f8968a..fb5c58be 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -28,7 +28,9 @@ async def on_ready(self): description="to create fake tweets", ) async def twitter( - self, ctx: MessageContext, content: CommandArg[str, Description["The content of the message"]] + self, + ctx: MessageContext, + content: CommandArg[str, Description["The content of the message"]] ): await ctx.interaction.ack() From c23b4d5fec44aa1c6e2f9682d7ca1461e36695ba Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 28 Nov 2021 16:53:39 +0100 Subject: [PATCH 092/134] :art: merge into 1 line https://github.com/Pincer-org/Pincer/pull/241#discussion_r757901150 --- examples/tweet_generator/tweet_generator.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index fb5c58be..566a5433 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -24,9 +24,7 @@ async def on_ready(self): f"Registered commands: {', '.join(self.chat_commands)}" ) - @command( - description="to create fake tweets", - ) + @command(description="to create fake tweets") async def twitter( self, ctx: MessageContext, From b6dc77c0fa932b89aa4ec4cc5fceac3dfdf8470b Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 28 Nov 2021 16:55:10 +0100 Subject: [PATCH 093/134] :art: simplified list's square brackets usage Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> --- examples/tweet_generator/tweet_generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index 566a5433..a36fb658 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -147,8 +147,8 @@ def add_color_to_mentions(message): continue if not text.startswith("@"): - if result[-1:][0]["color"] == (0, 0, 0): - result[-1:][0]["text"] += text + if result[-1]["color"] == (0, 0, 0): + result[-1]["text"] += text continue result.append({"color": (0, 0, 0), "text": text}) From f009ec5f6231e77527b2712b98f6e5aee29594c1 Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 28 Nov 2021 17:00:17 +0100 Subject: [PATCH 094/134] Update examples/tweet_generator/tweet_generator.py Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> --- examples/tweet_generator/tweet_generator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index a36fb658..a852b20b 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -138,9 +138,10 @@ def add_color_to_mentions(message): message = "\n".join(message).split(" ") result = [] for word in message: - for index, text in enumerate(word.splitlines()): + wordlines = word.splitlines() + for index, text in enumerate(wordlines): - text += "\n" if index != len(word.split("\n")) - 1 else " " + text += "\n" if index != len(wordlines) - 1 else " " if not result: result.append({"color": (0, 0, 0), "text": text}) From 46c49793761e0426b68ff0a2e182913449492e89 Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 28 Nov 2021 17:01:42 +0100 Subject: [PATCH 095/134] Update examples/tweet_generator/tweet_generator.py Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> --- examples/tweet_generator/tweet_generator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index a852b20b..f9290a47 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -163,12 +163,13 @@ def draw_multicolored_text(image, message, font): draw = ImageDraw.Draw(image) x = 30 y = 170 + y_fontsize = font.getsize(" ")[1] for text in message: - y -= font.getsize(" ")[1] + y -= y_fontsize for l_index, line in enumerate(text["text"].splitlines()): if l_index: x = 30 - y += font.getsize(" ")[1] + y += y_fontsize draw.text((x, y), line, fill=text["color"], font=font) x += font.getsize(line)[0] return image From 3bb5ca8d4466d27c22e214d9c546597af6a63abd Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 28 Nov 2021 17:01:52 +0100 Subject: [PATCH 096/134] Update examples/tweet_generator/tweet_generator.py Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> --- examples/tweet_generator/tweet_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index f9290a47..291cb3c2 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -87,7 +87,7 @@ async def twitter( return Message( embeds=[ - Embed(title="Twitter for Discord", description="").set_image( + Embed(title="Twitter for Discord").set_image( url="attachment://image0.png" ) ], From 837941488d0c6ba8c0bc67842539e3f28b4f8b34 Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 28 Nov 2021 17:03:14 +0100 Subject: [PATCH 097/134] :art: split into multiple lines, the line's too long https://github.com/Pincer-org/Pincer/pull/241#discussion_r757908479 --- examples/tweet_generator/tweet_generator.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index 291cb3c2..629e975b 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -49,7 +49,11 @@ async def twitter( # create the tweet by pasting the profile picture into a white image tweet = trans_paste( avatar, - Image.new("RGBA", (800, 250 + 50 * len(textwrap.wrap(content, 38))), (255, 255, 255)), + Image.new( + "RGBA", + (800, 250 + 50 * len(textwrap.wrap(content, 38))), + (255, 255, 255) + ), box=(15, 15), ) From 112ed8dbb3596f8ac2278c72cee2033a0402c7b2 Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 28 Nov 2021 17:04:30 +0100 Subject: [PATCH 098/134] :art: split into multiple lines https://github.com/Pincer-org/Pincer/pull/241#discussion_r757911576 --- examples/tweet_generator/tweet_generator.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index 629e975b..a58fca96 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -12,7 +12,13 @@ # you need to manually download the font files and put them into the folder # ./examples/tweet_generator/ to make the script works using this link: # https://fonts.google.com/share?selection.family=Noto%20Sans:wght@400;700 -if not all(font in os.listdir() for font in ["NotoSans-Regular.ttf", "NotoSans-Bold.ttf"]): +if not all( + font in os.listdir() + for font in [ + "NotoSans-Regular.ttf", + "NotoSans-Bold.ttf" + ] +): sys.exit() From 9756eab38c16c85c19e221c8ffdb252207d11f63 Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 28 Nov 2021 17:05:24 +0100 Subject: [PATCH 099/134] Update examples/tweet_generator/tweet_generator.py Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> --- examples/tweet_generator/tweet_generator.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index a58fca96..fc43ebc2 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -71,13 +71,16 @@ async def twitter( # write the name and username on the Image draw = ImageDraw.Draw(tweet) draw.text( - (180, 20), str(ctx.author.user), - fill=(0, 0, 0), font=font_bold + (180, 20), + str(ctx.author.user), + fill=(0, 0, 0), + font=font_bold ) draw.text( (180, 70), f"@{ctx.author.user.username}", - fill=(120, 120, 120), font=font_normal, + fill=(120, 120, 120), + font=font_normal ) content = add_color_to_mentions(content) From b20bffd73c84c98ada1061baf588535e02afcf0b Mon Sep 17 00:00:00 2001 From: trag1c Date: Sun, 28 Nov 2021 17:45:02 +0100 Subject: [PATCH 100/134] Added Guild.get_audit_log --- pincer/objects/guild/guild.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/pincer/objects/guild/guild.py b/pincer/objects/guild/guild.py index b397588a..46d33d28 100644 --- a/pincer/objects/guild/guild.py +++ b/pincer/objects/guild/guild.py @@ -15,15 +15,16 @@ if TYPE_CHECKING: from typing import Any, Dict, List, Optional, Union + from .audit_log import AuditLog from .ban import Ban from .channel import Channel from .invite import Invite from .member import GuildMember - from .widget import GuildWidget from .features import GuildFeature from .role import Role from .stage import StageInstance from .welcome_screen import WelcomeScreen, WelcomeScreenChannel + from .widget import GuildWidget from ..user.user import User from ..user.integration import Integration from ..voice.region import VoiceRegion @@ -1236,6 +1237,23 @@ async def modify_user_voice_state( } ) + async def get_audit_log(self) -> AuditLog: + """|coro| + Returns an audit log object for the guild. + Requires the ``VIEW_AUDIT_LOG`` permission. + + Returns + ------- + :class:`~pincer.objects.guild.audit_log.AuditLog` + The audit log object for the guild. + """ + return AuditLog.from_dict( + construct_client_dict( + self._client, + await self._http.get(f"guilds/{self.id}/audit-logs") + ) + ) + @classmethod def from_dict(cls, data) -> Guild: """ From 3b102c2b23c05080113c80f3008af398ca81a060 Mon Sep 17 00:00:00 2001 From: trag1c Date: Sun, 28 Nov 2021 17:50:00 +0100 Subject: [PATCH 101/134] Added Guild.get_emojis --- pincer/objects/guild/guild.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pincer/objects/guild/guild.py b/pincer/objects/guild/guild.py index 46d33d28..b0af2397 100644 --- a/pincer/objects/guild/guild.py +++ b/pincer/objects/guild/guild.py @@ -1254,6 +1254,21 @@ async def get_audit_log(self) -> AuditLog: ) ) + async def get_emojis(self) -> AsyncGenerator[Emoji, None]: + """|coro| + Returns an async generator of tl emojis in the guild. + + Yields + ------ + :class:`~pincer.objects.guild.emoji.Emoji` + The emoji object. + """ + data = await self._http.get(f"guilds/{self.id}/emojis") + for emoji_data in data: + yield Emoji.from_dict( + construct_client_dict(self._client, emoji_data) + ) + @classmethod def from_dict(cls, data) -> Guild: """ From 1e5fa5a063f9e730f93728483f4b26e937d30e8a Mon Sep 17 00:00:00 2001 From: trag1c Date: Sun, 28 Nov 2021 17:51:24 +0100 Subject: [PATCH 102/134] Changed Returns to Yields in generator docstrings --- pincer/objects/guild/guild.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pincer/objects/guild/guild.py b/pincer/objects/guild/guild.py index b0af2397..208e69e5 100644 --- a/pincer/objects/guild/guild.py +++ b/pincer/objects/guild/guild.py @@ -538,7 +538,7 @@ async def get_roles(self) -> AsyncGenerator[Role, None]: """|coro| Fetches all the roles in the guild. - Returns + Yields ------- AsyncGenerator[:class:`~pincer.objects.guild.role.Role`, :data:`None`] An async generator of Role objects. @@ -629,7 +629,7 @@ async def edit_role_position( position : Optional[:class:`int`] Sorting position of the role |default| :data:`None` - Returns + Yields ------- AsyncGenerator[:class:`~pincer.objects.guild.role.Role`, :data:`None`] An async generator of all of the guild's role objects. @@ -730,7 +730,7 @@ async def get_bans(self) -> AsyncGenerator[Ban, None]: """|coro| Fetches all the bans in the guild. - Returns + Yields ------- AsyncGenerator[:class:`~pincer.objects.guild.ban.Ban`, :data:`None`] An async generator of Ban objects. @@ -952,7 +952,7 @@ async def get_voice_regions(self) -> AsyncGenerator[VoiceRegion, None]: """|coro| Returns an async generator of voice regions. - Returns + Yields ------- AsyncGenerator[:class:`~pincer.objects.voice.VoiceRegion`, :data:`None`] An async generator of voice regions. @@ -966,7 +966,7 @@ async def get_invites(self) -> AsyncGenerator[Invite, None]: Returns an async generator of invites for the guild. Requires the ``MANAGE_GUILD`` permission. - Returns + Yields ------- AsyncGenerator[:class:`~pincer.objects.invite.Invite`, :data:`None`] An async generator of invites. @@ -980,7 +980,7 @@ async def get_integrations(self) -> AsyncGenerator[Integration, None]: Returns an async generator of integrations for the guild. Requires the ``MANAGE_GUILD`` permission. - Returns + Yields ------- AsyncGenerator[:class:`~pincer.objects.integration.Integration`, :data:`None`] An async generator of integrations. From 3e8c4f2975250f8a20ca51fb798014e12749ffe2 Mon Sep 17 00:00:00 2001 From: trag1c Date: Sun, 28 Nov 2021 18:00:20 +0100 Subject: [PATCH 103/134] Added Guild.get_emoji --- pincer/objects/guild/guild.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pincer/objects/guild/guild.py b/pincer/objects/guild/guild.py index 208e69e5..02b95d27 100644 --- a/pincer/objects/guild/guild.py +++ b/pincer/objects/guild/guild.py @@ -1269,6 +1269,27 @@ async def get_emojis(self) -> AsyncGenerator[Emoji, None]: construct_client_dict(self._client, emoji_data) ) + async def get_emoji(self, id: Snowflake) -> Emoji: + """|coro| + Returns an emoji object for the given ID. + + Parameters + ---------- + id : :class:`~pincer.utils.snowflake.Snowflake` + The ID of the emoji + + Returns + ------- + :class:`~pincer.objects.guild.emoji.Emoji` + The emoji object. + """ + return Emoji.from_dict( + construct_client_dict( + self._client, + await self._http.get(f"guilds/{self.id}/emojis/{id}") + ) + ) + @classmethod def from_dict(cls, data) -> Guild: """ From 078d74972124dc9722539266ba7acc32e0aaa1f8 Mon Sep 17 00:00:00 2001 From: trag1c Date: Sun, 28 Nov 2021 18:05:45 +0100 Subject: [PATCH 104/134] Added Guild.create_emoji --- pincer/objects/guild/guild.py | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/pincer/objects/guild/guild.py b/pincer/objects/guild/guild.py index 02b95d27..1d5faba1 100644 --- a/pincer/objects/guild/guild.py +++ b/pincer/objects/guild/guild.py @@ -1290,6 +1290,50 @@ async def get_emoji(self, id: Snowflake) -> Emoji: ) ) + async def create_emoji( + self, + *, + name: str, + image: str, + roles: List[Snowflake], + reason: Optional[str] = None + ) -> Emoji: + """|coro| + Creates a new emoji for the guild. + Requires the ``MANAGE_EMOJIS_AND_STICKERS`` permission. + + Emojis and animated emojis have a maximum file size of 256kb. + Attempting to upload an emoji larger than this limit will fail. + + Parameters + ---------- + name : :class:`str` + Name of the emoji + image : :class:`str` + The 128x128 emoji image data + roles : List[:class:`~pincer.utils.snowflake.Snowflake`] + Roles allowed to use this emoji + reason : Optional[:class:`str`] + The reason for creating the emoji |default| :data:`None` + + Returns + ------- + :class:`~pincer.objects.guild.emoji.Emoji` + The newly created emoji object. + """ + data = await self._http.post( + f"guilds/{self.id}/emojis", + data={ + "name": name, + "image": image, + "roles": roles + }, + headers=remove_none({"X-Audit-Log-Reason": reason}) + ) + return Emoji.from_dict( + construct_client_dict(self._client, data) + ) + @classmethod def from_dict(cls, data) -> Guild: """ From 9350a92d9b0421cded53de53e0ef16e6114f9d21 Mon Sep 17 00:00:00 2001 From: drawbu <69208565+drawbu@users.noreply.github.com> Date: Sun, 28 Nov 2021 18:07:29 +0100 Subject: [PATCH 105/134] :wheelchair: add a print statement with the instructions if the user don't have the font files https://github.com/Pincer-org/Pincer/pull/241#discussion_r757923555 --- examples/tweet_generator/tweet_generator.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/tweet_generator/tweet_generator.py b/examples/tweet_generator/tweet_generator.py index fc43ebc2..1ffdeb23 100644 --- a/examples/tweet_generator/tweet_generator.py +++ b/examples/tweet_generator/tweet_generator.py @@ -19,6 +19,12 @@ "NotoSans-Bold.ttf" ] ): + print( + "You don't have the font files installed! you need to manually " + "download the font files and put them into the folder " + "./examples/tweet_generator/ to make the script works using this link: " + "https://fonts.google.com/share?selection.family=Noto%20Sans:wght@400;700" + ) sys.exit() From 5eead4f80cc18b69990257836677e4cd6dcba976 Mon Sep 17 00:00:00 2001 From: trag1c Date: Sun, 28 Nov 2021 18:11:00 +0100 Subject: [PATCH 106/134] Added Guild.edit_emoji --- pincer/objects/guild/guild.py | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pincer/objects/guild/guild.py b/pincer/objects/guild/guild.py index 1d5faba1..f3194a29 100644 --- a/pincer/objects/guild/guild.py +++ b/pincer/objects/guild/guild.py @@ -1334,6 +1334,46 @@ async def create_emoji( construct_client_dict(self._client, data) ) + async def edit_emoji( + self, + id: Snowflake, + *, + name: Optional[str] = None, + roles: Optional[List[Snowflake]] = None, + reason: Optional[str] = None + ) -> Emoji: + """|coro| + Modifies the given emoji. + Requires the ``MANAGE_EMOJIS_AND_STICKERS`` permission. + + Parameters + ---------- + id : :class:`~pincer.utils.snowflake.Snowflake` + The ID of the emoji + name : Optional[:class:`str`] + Name of the emoji |default| :data:`None` + roles : Optional[List[:class:`~pincer.utils.snowflake.Snowflake`]] + Roles allowed to use this emoji |default| :data:`None` + reason : Optional[:class:`str`] + The reason for editing the emoji |default| :data:`None` + + Returns + ------- + :class:`~pincer.objects.guild.emoji.Emoji` + The modified emoji object. + """ + data = await self._http.patch( + f"guilds/{self.id}/emojis/{id}", + data={ + "name": name, + "roles": roles + }, + headers=remove_none({"X-Audit-Log-Reason": reason}) + ) + return Emoji.from_dict( + construct_client_dict(self._client, data) + ) + @classmethod def from_dict(cls, data) -> Guild: """ From 1b1d8e21b247b5d7bde80311d533d1ef2a7fb28c Mon Sep 17 00:00:00 2001 From: trag1c Date: Sun, 28 Nov 2021 18:22:07 +0100 Subject: [PATCH 107/134] Added Guild.delete_emoji --- pincer/objects/guild/guild.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pincer/objects/guild/guild.py b/pincer/objects/guild/guild.py index f3194a29..ad67ccc4 100644 --- a/pincer/objects/guild/guild.py +++ b/pincer/objects/guild/guild.py @@ -1374,6 +1374,28 @@ async def edit_emoji( construct_client_dict(self._client, data) ) + async def delete_emoji( + self, + id: Snowflake, + *, + reason: Optional[str] = None + ): + """|coro| + Deletes the given emoji. + Requires the ``MANAGE_EMOJIS_AND_STICKERS`` permission. + + Parameters + ---------- + id : :class:`~pincer.utils.snowflake.Snowflake` + The ID of the emoji + reason : Optional[:class:`str`] + The reason for deleting the emoji |default| :data:`None` + """ + await self._http.delete( + f"guilds/{self.id}/emojis/{id}", + headers=remove_none({"X-Audit-Log-Reason": reason}) + ) + @classmethod def from_dict(cls, data) -> Guild: """ From 4938521cb8849f00a8166cfd4016b75808f9fd12 Mon Sep 17 00:00:00 2001 From: trag1c Date: Sun, 28 Nov 2021 18:37:51 +0100 Subject: [PATCH 108/134] Changed from single to double backslashes --- pincer/commands/arg_types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pincer/commands/arg_types.py b/pincer/commands/arg_types.py index 363038a2..6822c59d 100644 --- a/pincer/commands/arg_types.py +++ b/pincer/commands/arg_types.py @@ -35,7 +35,7 @@ class CommandArg(metaclass=_CommandTypeMeta): ---------- command_type : T The type of the command - \*args : :class:`~pincer.commands.arg_types.Modifier` + \\*args : :class:`~pincer.commands.arg_types.Modifier` """ @@ -124,7 +124,7 @@ class Choices(Modifier): Parameters ---------- - \*choices : Union[:class:`~pincer.commands.arg_types.Choice`, str, int, float] + \\*choices : Union[:class:`~pincer.commands.arg_types.Choice`, str, int, float] A choice. If the type is not :class:`~pincer.commands.arg_types.Choice`, the same value will be used for the choice name and value. """ @@ -165,7 +165,7 @@ class ChannelTypes(Modifier): Parameters ---------- - \*types : :class:`~pincer.objects.guild.channel.ChannelType` + \\*types : :class:`~pincer.objects.guild.channel.ChannelType` A list of channel types that the user can pick from. """ From 1689c09b423229e9d450a66271ac6944c5ff586b Mon Sep 17 00:00:00 2001 From: trag1c Date: Sun, 28 Nov 2021 18:56:50 +0100 Subject: [PATCH 109/134] Fixed typo --- pincer/objects/guild/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pincer/objects/guild/guild.py b/pincer/objects/guild/guild.py index ad67ccc4..0b2cb7b3 100644 --- a/pincer/objects/guild/guild.py +++ b/pincer/objects/guild/guild.py @@ -1256,7 +1256,7 @@ async def get_audit_log(self) -> AuditLog: async def get_emojis(self) -> AsyncGenerator[Emoji, None]: """|coro| - Returns an async generator of tl emojis in the guild. + Returns an async generator of the emojis in the guild. Yields ------ From 4b1aedb74a14a25eaeba08e459a83051bb8ead62 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Sun, 28 Nov 2021 19:54:19 +0000 Subject: [PATCH 110/134] :art: Automatic sorting --- pincer/objects/__init__.py | 10 +++++----- pincer/objects/app/__init__.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pincer/objects/__init__.py b/pincer/objects/__init__.py index f48fc706..89d23871 100644 --- a/pincer/objects/__init__.py +++ b/pincer/objects/__init__.py @@ -130,10 +130,10 @@ "IntegrationExpireBehavior", "Intents", "Interaction", "InteractionData", "InteractionFlags", "InteractionType", "Invite", "InviteCreateEvent", "InviteDeleteEvent", "InviteMetadata", "InviteStageInstance", - "InviteTargetType", "MFALevel", "Message", "MessageActivity", - "MessageActivityType", "MessageComponent", "MessageContext", - "MessageDeleteBulkEvent", "MessageDeleteEvent", "MessageFlags", - "MessageInteraction", "MessageReactionAddEvent", + "InviteTargetType", "MFALevel", "Mentionable", "Message", + "MessageActivity", "MessageActivityType", "MessageComponent", + "MessageContext", "MessageDeleteBulkEvent", "MessageDeleteEvent", + "MessageFlags", "MessageInteraction", "MessageReactionAddEvent", "MessageReactionRemoveAllEvent", "MessageReactionRemoveEmojiEvent", "MessageReactionRemoveEvent", "MessageReference", "MessageType", "NewsChannel", "Overwrite", "PartialGuildMember", "PremiumTier", @@ -148,5 +148,5 @@ "User", "UserMessage", "VerificationLevel", "VisibilityType", "VoiceChannel", "VoiceRegion", "VoiceServerUpdateEvent", "VoiceState", "Webhook", "WebhookType", "WebhooksUpdateEvent", "WelcomeScreen", - "WelcomeScreenChannel", "Mentionable" + "WelcomeScreenChannel" ) diff --git a/pincer/objects/app/__init__.py b/pincer/objects/app/__init__.py index c57a56fb..e3fae1bc 100644 --- a/pincer/objects/app/__init__.py +++ b/pincer/objects/app/__init__.py @@ -24,7 +24,7 @@ "AppCommandOption", "AppCommandOptionChoice", "AppCommandOptionType", "AppCommandType", "Application", "CallbackType", "ClientCommandStructure", "DefaultThrottleHandler", "Intents", "Interaction", "InteractionData", - "InteractionFlags", "InteractionType", "MessageInteraction", + "InteractionFlags", "InteractionType", "Mentionable", "MessageInteraction", "ResolvedData", "SelectMenu", "SelectOption", "SessionStartLimit", - "ThrottleInterface", "ThrottleScope", "Mentionable" + "ThrottleInterface", "ThrottleScope" ) From 8ccc054e9e7d01472b3f5390a12ce7a3e4f360b3 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Sun, 28 Nov 2021 15:31:30 -0500 Subject: [PATCH 111/134] :loud_sound: improved logging --- pincer/core/ratelimiter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pincer/core/ratelimiter.py b/pincer/core/ratelimiter.py index 5d6a520b..df823aaf 100644 --- a/pincer/core/ratelimiter.py +++ b/pincer/core/ratelimiter.py @@ -85,7 +85,11 @@ def save_response_bucket( time_cached=time() ) - _log.info("Rate limit bucket detected with ID %s.", bucket_id) + _log.info( + "Rate limit bucket detected: %s - %r.", + bucket_id, + self.buckets[bucket_id] + ) async def wait_until_not_ratelimited( self, From 072bf68247a1fc4ee96b4290efdf6bc6b540b574 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Sun, 28 Nov 2021 15:38:05 -0500 Subject: [PATCH 112/134] :ambulance: fix exception thrown if options is MISSING --- pincer/objects/app/interactions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pincer/objects/app/interactions.py b/pincer/objects/app/interactions.py index 511e07f2..5dcf9433 100644 --- a/pincer/objects/app/interactions.py +++ b/pincer/objects/app/interactions.py @@ -143,8 +143,10 @@ class Interaction(APIObject): def __post_init__(self): super().__post_init__() - for option in self.data.options: + if not self.data.options: + return + for option in self.data.options: if option.type is AppCommandOptionType.STRING: option.value = str(option.value) elif option.type is AppCommandOptionType.INTEGER: From cf6996683a93d06dcc775a7a0f855e7399cf67fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Nov 2021 13:33:16 +0000 Subject: [PATCH 113/134] :arrow_up: Bump coverage from 6.1.2 to 6.2 Bumps [coverage](https://github.com/nedbat/coveragepy) from 6.1.2 to 6.2. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/6.1.2...6.2) --- updated-dependencies: - dependency-name: coverage dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- packages/dev.txt | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dev.txt b/packages/dev.txt index 2ca92243..0c9db3c9 100644 --- a/packages/dev.txt +++ b/packages/dev.txt @@ -1,4 +1,4 @@ -coverage==6.1.2 +coverage==6.2 flake8==4.0.1 tox==3.24.4 pre-commit==2.15.0 diff --git a/setup.cfg b/setup.cfg index 19d5d2b8..fa32a9b7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,7 +51,7 @@ python_requires = >=3.8 [options.extras_require] testing = - coverage==6.1.2 + coverage==6.2 flake8==4.0.1 tox==3.24.4 pre-commit==2.15.0 From b3216f08141edfcaa5ec622632de5aeed576f4c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Nov 2021 13:34:23 +0000 Subject: [PATCH 114/134] :arrow_up: Update aiohttp requirement Updates the requirements on [aiohttp](https://github.com/aio-libs/aiohttp) to permit the latest version. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.7.4.post0...v4.0.0a1) --- updated-dependencies: - dependency-name: aiohttp dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 19d5d2b8..a464aa82 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,7 +46,7 @@ packages = pincer.utils install_requires = websockets>=10.0 - aiohttp>=3.7.4post0,<3.8.0 + aiohttp>=3.7.4post0,<4.1.0 python_requires = >=3.8 [options.extras_require] From 4296cc65e481b6d1e0ed1571817f1e2e60c67023 Mon Sep 17 00:00:00 2001 From: Lunarmagpie <65521138+Lunarmagpie@users.noreply.github.com> Date: Mon, 29 Nov 2021 14:02:37 -0500 Subject: [PATCH 115/134] :memo: fix docs --- pincer/core/ratelimiter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pincer/core/ratelimiter.py b/pincer/core/ratelimiter.py index df823aaf..970a2236 100644 --- a/pincer/core/ratelimiter.py +++ b/pincer/core/ratelimiter.py @@ -44,7 +44,7 @@ class RateLimiter: """Prevents ``user`` rate limits Attributes ---------- - bucket_map : Tuple[str, :class:`~pincer.core.http.HttpCallable`] + bucket_map : Dict[Tuple[str, :class:`~pincer.core.http.HttpCallable`], str] Maps endpoints and methods to a rate limit bucket buckets : Dict[str, :class:`~pincer.core.ratelimiter.Bucket`] Dictionary of buckets From c248a13b24f6649069705e085882bf1da7887794 Mon Sep 17 00:00:00 2001 From: trag1c <77130613+trag1c@users.noreply.github.com> Date: Mon, 29 Nov 2021 20:44:01 +0100 Subject: [PATCH 116/134] Fixed typo --- pincer/objects/guild/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pincer/objects/guild/guild.py b/pincer/objects/guild/guild.py index 0b2cb7b3..83dbc18f 100644 --- a/pincer/objects/guild/guild.py +++ b/pincer/objects/guild/guild.py @@ -1163,7 +1163,7 @@ async def modify_welcome_screen( ) return WelcomeScreen.from_dict(construct_client_dict(self._client, data)) - async def modify_curent_user_voice_state( + async def modify_current_user_voice_state( self, channel_id: Snowflake, suppress: Optional[bool] = None, From 53e6969c769a55d4d36d8fc06bd1a07ac6167aa5 Mon Sep 17 00:00:00 2001 From: trag1c Date: Mon, 29 Nov 2021 21:11:54 +0100 Subject: [PATCH 117/134] Added Client.get_guild_template --- pincer/client.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/pincer/client.py b/pincer/client.py index 919e3af1..894ed8fc 100644 --- a/pincer/client.py +++ b/pincer/client.py @@ -21,8 +21,10 @@ ) from .middleware import middleware from .objects import ( - Role, Channel, DefaultThrottleHandler, User, Guild, Intents + Role, Channel, DefaultThrottleHandler, User, Guild, Intents, + GuildTemplate ) +from .utils.conversion import construct_client_dict from .utils.event_mgr import EventMgr from .utils.extraction import get_index from .utils.insertion import should_pass_cls @@ -647,6 +649,27 @@ async def create_guild(self, name: str, **kwargs) -> Guild: g = await self.http.post("guilds", data={"name": name, **kwargs}) return await self.get_guild(g['id']) + async def get_guild_template(self, code: str) -> GuildTemplate: + """|coro| + Retrieves a guild template by its code. + + Parameters + ---------- + code : :class:`str` + The code of the guild template + + Returns + ------- + :class:`~pincer.objects.guild.template.GuildTemplate` + The guild template + """ + return GuildTemplate.from_dict( + construct_client_dict( + self, + await self.http.get(f"guilds/templates/{code}") + ) + ) + async def wait_for( self, event_name: str, From fe8ddc4f0653e8796a9c2cb274163ef569786a38 Mon Sep 17 00:00:00 2001 From: trag1c Date: Mon, 29 Nov 2021 21:19:05 +0100 Subject: [PATCH 118/134] Added Client.create_guild_from_template --- pincer/client.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/pincer/client.py b/pincer/client.py index 894ed8fc..e36aa41c 100644 --- a/pincer/client.py +++ b/pincer/client.py @@ -670,6 +670,39 @@ async def get_guild_template(self, code: str) -> GuildTemplate: ) ) + async def create_guild_from_template( + self, + template: GuildTemplate, + name: str, + icon: Optional[str] = None + ) -> Guild: + """|coro| + Creates a guild from a template. + + Parameters + ---------- + template : :class:`~pincer.objects.guild.template.GuildTemplate` + The guild template + name : :class:`str` + Name of the guild (2-100 characters) + icon : Optional[:class:`str`] + base64 128x128 image for the guild icon |default| :data:`None` + + Returns + ------- + :class:`~pincer.objects.guild.guild.Guild` + The created guild + """ + return Guild.from_dict( + construct_client_dict( + self, + await self.http.post( + f"guilds/templates/{template.code}", + data={"name": name, "icon": icon} + ) + ) + ) + async def wait_for( self, event_name: str, From 24bafa749d950054b34ffc076bf4098bb8decc16 Mon Sep 17 00:00:00 2001 From: trag1c Date: Mon, 29 Nov 2021 21:22:30 +0100 Subject: [PATCH 119/134] Added Guild.get_templates --- pincer/objects/guild/guild.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pincer/objects/guild/guild.py b/pincer/objects/guild/guild.py index 83dbc18f..8d1f943e 100644 --- a/pincer/objects/guild/guild.py +++ b/pincer/objects/guild/guild.py @@ -23,6 +23,7 @@ from .features import GuildFeature from .role import Role from .stage import StageInstance + from .template import GuildTemplate from .welcome_screen import WelcomeScreen, WelcomeScreenChannel from .widget import GuildWidget from ..user.user import User @@ -1396,6 +1397,21 @@ async def delete_emoji( headers=remove_none({"X-Audit-Log-Reason": reason}) ) + async def get_templates(self) -> AsyncGenerator[GuildTemplate, None]: + """|coro| + Returns an async generator of the guild templates. + + Yields + ------- + AsyncGenerator[:class:`~pincer.objects.guild.template.GuildTemplate`, :data:`None`] + The guild template object. + """ + data = await self._http.get(f"guilds/{self.id}/templates") + for template_data in data: + yield GuildTemplate.from_dict( + construct_client_dict(self._client, template_data) + ) + @classmethod def from_dict(cls, data) -> Guild: """ From 68dacebcfa7d04483385268f391935f14a85ac1a Mon Sep 17 00:00:00 2001 From: trag1c Date: Mon, 29 Nov 2021 21:25:18 +0100 Subject: [PATCH 120/134] Added Guild.create_template --- pincer/objects/guild/guild.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pincer/objects/guild/guild.py b/pincer/objects/guild/guild.py index 8d1f943e..a01d5a7a 100644 --- a/pincer/objects/guild/guild.py +++ b/pincer/objects/guild/guild.py @@ -1412,6 +1412,38 @@ async def get_templates(self) -> AsyncGenerator[GuildTemplate, None]: construct_client_dict(self._client, template_data) ) + async def create_template( + self, + name: str, + description: Optional[str] = None + ) -> GuildTemplate: + """|coro| + Creates a new template for the guild. + Requires the ``MANAGE_GUILD`` permission. + + Parameters + ---------- + name : :class:`str` + Name of the template (1-100 characters) + description : Optional[:class:`str`] + Description of the template + (0-120 characters) |default| :data:`None` + Returns + ------- + :class:`~pincer.objects.guild.template.GuildTemplate` + The newly created template object. + """ + data = await self._http.post( + f"guilds/{self.id}/templates", + data={ + "name": name, + "description": description + } + ) + return GuildTemplate.from_dict( + construct_client_dict(self._client, data) + ) + @classmethod def from_dict(cls, data) -> Guild: """ From 6fe8468631921a25d13dd7613bc657962762b878 Mon Sep 17 00:00:00 2001 From: trag1c Date: Mon, 29 Nov 2021 21:27:01 +0100 Subject: [PATCH 121/134] Added Guild.sync_template --- pincer/objects/guild/guild.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pincer/objects/guild/guild.py b/pincer/objects/guild/guild.py index a01d5a7a..75657f84 100644 --- a/pincer/objects/guild/guild.py +++ b/pincer/objects/guild/guild.py @@ -1444,6 +1444,31 @@ async def create_template( construct_client_dict(self._client, data) ) + async def sync_template( + self, + template: GuildTemplate + ) -> GuildTemplate: + """|coro| + Syncs the given template. + Requires the ``MANAGE_GUILD`` permission. + + Parameters + ---------- + template : :class:`~pincer.objects.guild.template.GuildTemplate` + The template to sync + + Returns + ------- + :class:`~pincer.objects.guild.template.GuildTemplate` + The synced template object. + """ + data = await self._http.put( + f"guilds/{self.id}/templates/{template.code}" + ) + return GuildTemplate.from_dict( + construct_client_dict(self._client, data) + ) + @classmethod def from_dict(cls, data) -> Guild: """ From 3bd9ae004374233aa62e1e8ff582dfdf869ff3c1 Mon Sep 17 00:00:00 2001 From: trag1c Date: Mon, 29 Nov 2021 21:30:05 +0100 Subject: [PATCH 122/134] Added Guild.edit_template --- pincer/objects/guild/guild.py | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/pincer/objects/guild/guild.py b/pincer/objects/guild/guild.py index 75657f84..742716a0 100644 --- a/pincer/objects/guild/guild.py +++ b/pincer/objects/guild/guild.py @@ -1469,6 +1469,44 @@ async def sync_template( construct_client_dict(self._client, data) ) + async def edit_template( + self, + template: GuildTemplate, + *, + name: Optional[str] = None, + description: Optional[str] = None + ) -> GuildTemplate: + """|coro| + Modifies the template's metadata. + Requires the ``MANAGE_GUILD`` permission. + + Parameters + ---------- + template : :class:`~pincer.objects.guild.template.GuildTemplate` + The template to edit + name : Optional[:class:`str`] + Name of the template (1-100 characters) + |default| :data:`None` + description : Optional[:class:`str`] + Description of the template (0-120 characters) + |default| :data:`None` + + Returns + ------- + :class:`~pincer.objects.guild.template.GuildTemplate` + The edited template object. + """ + data = await self._http.patch( + f"guilds/{self.id}/templates/{template.code}", + data={ + "name": name, + "description": description + } + ) + return GuildTemplate.from_dict( + construct_client_dict(self._client, data) + ) + @classmethod def from_dict(cls, data) -> Guild: """ From 84153339b563fbfc859be1f9edd11cef8ed5b9ea Mon Sep 17 00:00:00 2001 From: trag1c Date: Mon, 29 Nov 2021 21:32:11 +0100 Subject: [PATCH 123/134] Added Guild.delete_template --- pincer/objects/guild/guild.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pincer/objects/guild/guild.py b/pincer/objects/guild/guild.py index 742716a0..5c70b983 100644 --- a/pincer/objects/guild/guild.py +++ b/pincer/objects/guild/guild.py @@ -1507,6 +1507,31 @@ async def edit_template( construct_client_dict(self._client, data) ) + async def delete_template( + self, + template: GuildTemplate + ) -> GuildTemplate: + """|coro| + Deletes the given template. + Requires the ``MANAGE_GUILD`` permission. + + Parameters + ---------- + template : :class:`~pincer.objects.guild.template.GuildTemplate` + The template to delete + + Returns + ------- + :class:`~pincer.objects.guild.template.GuildTemplate` + The deleted template object. + """ + data = await self._http.delete( + f"guilds/{self.id}/templates/{template.code}" + ) + return GuildTemplate.from_dict( + construct_client_dict(self._client, data) + ) + @classmethod def from_dict(cls, data) -> Guild: """ From 8814b0c5a0156f0f3cc0aaa33638ecf12ebb2655 Mon Sep 17 00:00:00 2001 From: Lunarmagpie <65521138+Lunarmagpie@users.noreply.github.com> Date: Mon, 29 Nov 2021 15:51:31 -0500 Subject: [PATCH 124/134] Update pincer/core/ratelimiter.py Co-authored-by: trag1c <77130613+trag1c@users.noreply.github.com> --- pincer/core/ratelimiter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pincer/core/ratelimiter.py b/pincer/core/ratelimiter.py index 970a2236..9435fc99 100644 --- a/pincer/core/ratelimiter.py +++ b/pincer/core/ratelimiter.py @@ -7,10 +7,10 @@ from dataclasses import dataclass import logging from time import time -from typing import TYPE_CHECKING, Tuple +from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Dict + from typing import Dict, Tuple from .http import HttpCallable _log = logging.getLogger(__name__) From 4442ddd594784bc0b440a9362d1f8d2bc98f67fc Mon Sep 17 00:00:00 2001 From: sigmanificient Date: Mon, 29 Nov 2021 23:02:02 +0100 Subject: [PATCH 125/134] :recycle: simplifying __str__ method --- pincer/utils/api_object.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pincer/utils/api_object.py b/pincer/utils/api_object.py index 803dfcf2..aca6b618 100644 --- a/pincer/utils/api_object.py +++ b/pincer/utils/api_object.py @@ -233,10 +233,7 @@ def __str__(self): ------- """ - if self.__dict__.get('name'): - return self.name - - return super().__str__() + return getattr(self, 'id', None) or super().__str__() @classmethod def from_dict( From 38accb29dfba1fd8eea7b29781c516ee5c8fe1e7 Mon Sep 17 00:00:00 2001 From: sigmanificient Date: Mon, 29 Nov 2021 23:11:00 +0100 Subject: [PATCH 126/134] :recycle: small fixes --- pincer/client.py | 2 +- pincer/objects/user/user.py | 4 ++-- pincer/utils/conversion.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pincer/client.py b/pincer/client.py index e36aa41c..5fefda1f 100644 --- a/pincer/client.py +++ b/pincer/client.py @@ -490,7 +490,7 @@ async def handle_middleware( ) next_call = get_index(extractable, 0, "") - ret_object = get_index(extractable, 1, None) + ret_object = get_index(extractable, 1) if next_call is None: raise RuntimeError(f"Middleware `{key}` has not been registered.") diff --git a/pincer/objects/user/user.py b/pincer/objects/user/user.py index f6fd7642..9c252340 100644 --- a/pincer/objects/user/user.py +++ b/pincer/objects/user/user.py @@ -238,5 +238,5 @@ async def send(self, message: MessageConvertable) -> UserMessage: message : :class:`~pincer.utils.convert_message.MessageConvertable` Message to be sent to the user. """ - channel = await self.get_dm_channel() - return await channel.send(message) + _channel = await self.get_dm_channel() + return await _channel.send(message) diff --git a/pincer/utils/conversion.py b/pincer/utils/conversion.py index 7332c50a..741d20b9 100644 --- a/pincer/utils/conversion.py +++ b/pincer/utils/conversion.py @@ -81,4 +81,4 @@ def remove_none(obj: Union[List, Dict, Set]) -> Union[List, Dict, Set]: elif isinstance(obj, set): return obj - {None} elif isinstance(obj, dict): - return {k: v for k, v in obj.items() if None not in {k, v}} \ No newline at end of file + return {k: v for k, v in obj.items() if None not in {k, v}} From 898c18d344e03a84e2a2111fc66226a76db1967c Mon Sep 17 00:00:00 2001 From: sigmanificient Date: Mon, 29 Nov 2021 23:11:50 +0100 Subject: [PATCH 127/134] :pencil2: correcting var name --- pincer/commands/commands.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pincer/commands/commands.py b/pincer/commands/commands.py index 26b42ee6..5bb3be47 100644 --- a/pincer/commands/commands.py +++ b/pincer/commands/commands.py @@ -259,9 +259,9 @@ async def test_command( "Choice value must match the command type" ) - cannel_types = annotation.get_arg(ChannelTypes) + channel_types = annotation.get_arg(ChannelTypes) if ( - cannel_types is not MISSING + channel_types is not MISSING and annotation.command_type is not Channel ): raise InvalidArgumentAnnotation( @@ -289,7 +289,7 @@ async def test_command( description=argument_description, required=required, choices=choices, - channel_types=cannel_types, + channel_types=channel_types, max_value=max_value, min_value=min_value, ) From 328ca96f4ccdb6635b829f3e54aeb9f93b124db8 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Mon, 29 Nov 2021 19:48:21 -0500 Subject: [PATCH 128/134] :memo: hyperlink and init docs didn't work --- pincer/core/http.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/pincer/core/http.py b/pincer/core/http.py index 043ce800..47dde97e 100644 --- a/pincer/core/http.py +++ b/pincer/core/http.py @@ -45,6 +45,21 @@ def __call__( class HTTPClient: """Interacts with Discord API through HTTP protocol + Parameters + ---------- + Instantiate a new HttpApi object. + + token: + Discord API token + + Keyword Arguments: + + version: + The discord API version. + See ``_. + ttl: + Max amount of attempts after error code 5xx + Attributes ---------- url: :class:`str` @@ -55,20 +70,6 @@ class HTTPClient: """ def __init__(self, token: str, *, version: int = None, ttl: int = 5): - """ - Instantiate a new HttpApi object. - - token: - Discord API token - - Keyword Arguments: - - version: - The discord API version. - See `` - ttl: - Max amount of attempts after error code 5xx - """ version = version or GatewayConfig.version self.url: str = f"https://discord.com/api/v{version}" self.max_ttl: int = ttl From 224c4cb6e3e6734cc24b00d631dfc32052353bc4 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Mon, 29 Nov 2021 22:00:05 -0500 Subject: [PATCH 129/134] :memo: marked async func as |coro| i keep on forgetting :( --- pincer/core/ratelimiter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pincer/core/ratelimiter.py b/pincer/core/ratelimiter.py index 9435fc99..59568d11 100644 --- a/pincer/core/ratelimiter.py +++ b/pincer/core/ratelimiter.py @@ -96,7 +96,7 @@ async def wait_until_not_ratelimited( endpoint: str, method: HttpCallable ): - """ + """|coro| Waits until the response no longer needs to be blocked to prevent a 429 response because of ``user`` rate limits. From 16dc1bcb46a77c4a4a4f5360123507ce8ab18165 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 30 Nov 2021 19:21:27 +0000 Subject: [PATCH 130/134] :art: Automatic sorting --- pincer/core/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pincer/core/__init__.py b/pincer/core/__init__.py index 9379611e..ad11074c 100644 --- a/pincer/core/__init__.py +++ b/pincer/core/__init__.py @@ -9,6 +9,6 @@ __all__ = ( - "Dispatcher", "GatewayDispatch", "HTTPClient", "Heartbeat", "RateLimiter", - "Bucket" + "Bucket", "Dispatcher", "GatewayDispatch", "HTTPClient", + "Heartbeat", "RateLimiter" ) From 2e768dc270fd6f6aab72d89132874ef9f7575e91 Mon Sep 17 00:00:00 2001 From: Yohann Boniface Date: Tue, 30 Nov 2021 21:14:45 +0100 Subject: [PATCH 131/134] Update setup.cfg Co-authored-by: RPS --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index a464aa82..dc2a07c0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,7 +46,7 @@ packages = pincer.utils install_requires = websockets>=10.0 - aiohttp>=3.7.4post0,<4.1.0 + aiohttp>=3.7.4post0,<3.9.0 python_requires = >=3.8 [options.extras_require] From 98b7d71aa33f29ec1f7f40ac3549bb49ccc74eb5 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 30 Nov 2021 20:15:45 +0000 Subject: [PATCH 132/134] :hammer: Automatic update of setup.cfg --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 5839355d..fa32a9b7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,7 +46,7 @@ packages = pincer.utils install_requires = websockets>=10.0 - aiohttp>=3.7.4post0,<3.9.0 + aiohttp>=3.7.4post0,<3.8.0 python_requires = >=3.8 [options.extras_require] From 4d1d118520af0a342ac7ca0cdcdc742c6b4d02f6 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Tue, 30 Nov 2021 15:36:26 -0500 Subject: [PATCH 133/134] :bug: fix on_ready middleware --- pincer/client.py | 22 ++++++++++------------ pincer/utils/event_mgr.py | 6 +++++- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/pincer/client.py b/pincer/client.py index 5fefda1f..22429375 100644 --- a/pincer/client.py +++ b/pincer/client.py @@ -432,7 +432,7 @@ def execute_event(calls: List[Coro], *args, **kwargs): if should_pass_cls(call): call_args = ( ChatCommandHandler.managers[call.__module__], - *args + *(arg for arg in args if arg is not None) ) ensure_future(call(*call_args, **kwargs)) @@ -495,12 +495,11 @@ async def handle_middleware( if next_call is None: raise RuntimeError(f"Middleware `{key}` has not been registered.") - return ( - (next_call, ret_object) - if next_call.startswith("on_") - else await self.handle_middleware( - payload, next_call, *arguments, **params - ) + if next_call.startswith("on_"): + return (next_call, ret_object) + + return await self.handle_middleware( + payload, next_call, *arguments, **params ) async def execute_error( @@ -547,12 +546,11 @@ async def process_event(self, name: str, payload: GatewayDispatch): what specifically happened. """ try: - key, ret_object = await self.handle_middleware(payload, name) - - self.event_mgr.process_events(key, ret_object) + key, args = await self.handle_middleware(payload, name) + self.event_mgr.process_events(key, args) if calls := self.get_event_coro(key): - self.execute_event(calls, ret_object) + self.execute_event(calls, args) except Exception as e: await self.execute_error(e) @@ -665,7 +663,7 @@ async def get_guild_template(self, code: str) -> GuildTemplate: """ return GuildTemplate.from_dict( construct_client_dict( - self, + self, await self.http.get(f"guilds/templates/{code}") ) ) diff --git a/pincer/utils/event_mgr.py b/pincer/utils/event_mgr.py index 383e1733..2defb81a 100644 --- a/pincer/utils/event_mgr.py +++ b/pincer/utils/event_mgr.py @@ -48,7 +48,11 @@ def matches_event(self, event_name: str, event_value: Any): return False if self.check: - return self.check(event_value) + if event_value is not None: + return self.check(event_value) + else: + # Certain middleware do not have an event_value + return self.check() return True From 558a1a3b1e1a202318df350bad3471d7309135e1 Mon Sep 17 00:00:00 2001 From: Lunarmagpie Date: Tue, 30 Nov 2021 15:41:50 -0500 Subject: [PATCH 134/134] :bug: fixed Channel undefined when constructing Guild from_id --- pincer/objects/guild/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pincer/objects/guild/guild.py b/pincer/objects/guild/guild.py index 5c70b983..c22362b8 100644 --- a/pincer/objects/guild/guild.py +++ b/pincer/objects/guild/guild.py @@ -7,6 +7,7 @@ from enum import IntEnum from typing import AsyncGenerator, overload, TYPE_CHECKING +from .channel import Channel from ...exceptions import UnavailableGuildError from ...utils.api_object import APIObject from ...utils.conversion import construct_client_dict, remove_none @@ -17,7 +18,6 @@ from .audit_log import AuditLog from .ban import Ban - from .channel import Channel from .invite import Invite from .member import GuildMember from .features import GuildFeature