diff --git a/.flake8 b/.flake8 new file mode 100644 index 000000000..e61de53c0 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +ignore = E203 E501 W503 W504 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9a882c3d7..bb0fcae48 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,6 +8,7 @@ What changes were made? ## Checklist +- [ ] I've run the `pre_push.py` script to format and lint code. - [ ] I've checked this pull request runs on `Python 3.6.X`. - [ ] This fixes something in [Issues](https://github.com/eunwoo1104/discord-py-slash-command/issues). - Issue: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..a5c70a06d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +jobs: + lint-multi-os: + name: Lint ${{ matrix.os }} + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: 3.x + - uses: actions/cache@v1 + with: + key: v0-${{ runner.os }}-pip-lint-${{ hashFiles('setup.py') }} + path: ~/.cache/pip + restore-keys: | + v0-${{ runner.os }}-pip-lint- + v0-${{ runner.os }}-pip- + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install .[lint] + - name: Run black + run: black --check --verbose . + - name: Run flake8 + run: flake8 --exclude docs --statistics + - name: Run isort + run: isort -cv . + - name: Run sphinx + run: sphinx-build -W --keep-going docs/ /tmp/foo + strategy: + matrix: + os: [macOS-latest, ubuntu-latest, windows-latest] +name: CI +on: [pull_request, push] diff --git a/.gitignore b/.gitignore index 839b32575..9fa8b2b65 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,8 @@ -.idea -__pycache__ -test.py -test2.py -test3.py -docs/_build -slash.log -test -__*.py -soontm.png - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg *.egg -MANIFEST +*.egg-info/ +*.eggs/ +*.pyc +.cache/ +_build/ +build/ +dist/ \ No newline at end of file diff --git a/README.md b/README.md index 2640437ed..73a0cf552 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Documentation ⦿ Discord Server

- + ## About Discord Slash Commands are a new implementation for the Bot API that utilize the forward-slash "/" symbol. Released on 15 December 2020, many bot developers are still learning to learn how to implement this into @@ -79,8 +79,8 @@ def setup(bot): ``` -------- -This library is based on gateway event. If you are looking for webserver based, have a look at this: -[dispike](https://github.com/ms7m/dispike) -[discord-interactions-python](https://github.com/discord/discord-interactions-python) -Or for other languages: -[discord-api-docs Community Resources: Interactions](https://discord.com/developers/docs/topics/community-resources#interactions) \ No newline at end of file +- This library is based on gateway event. If you are looking for webserver based, have a look at this: + - [dispike](https://github.com/ms7m/dispike) + - [discord-interactions-python](https://github.com/discord/discord-interactions-python) +- Or for other languages: + - [discord-api-docs Community Resources: Interactions](https://discord.com/developers/docs/topics/community-resources#interactions) \ No newline at end of file diff --git a/discord_slash/__init__.py b/discord_slash/__init__.py index 662495e71..7fa481236 100644 --- a/discord_slash/__init__.py +++ b/discord_slash/__init__.py @@ -8,12 +8,11 @@ :license: MIT """ -from .client import SlashCommand -from .model import SlashCommandOptionType -from .context import SlashContext -from .context import ComponentContext -from .dpy_overrides import ComponentMessage -from .utils import manage_commands -from .utils import manage_components - -__version__ = "1.2.2" +from .client import SlashCommand # noqa: F401 +from .const import __version__ # noqa: F401 +from .context import ComponentContext # noqa: F401 +from .context import SlashContext # noqa: F401 +from .dpy_overrides import ComponentMessage # noqa: F401 +from .model import SlashCommandOptionType # noqa: F401 +from .utils import manage_commands # noqa: F401 +from .utils import manage_components # noqa: F401 diff --git a/discord_slash/client.py b/discord_slash/client.py index 82bc521f8..840565647 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -1,15 +1,13 @@ import copy import logging import typing -import discord -from inspect import iscoroutinefunction, getdoc from contextlib import suppress +from inspect import getdoc, iscoroutinefunction + +import discord from discord.ext import commands -from . import http -from . import model -from . import error -from . import context -from . import dpy_overrides + +from . import context, error, http, model from .utils import manage_commands @@ -43,13 +41,15 @@ class SlashCommand: :ivar has_listener: Whether discord client has listener add function. """ - def __init__(self, - client: typing.Union[discord.Client, commands.Bot], - sync_commands: bool = False, - delete_from_unused_guilds: bool = False, - sync_on_cog_reload: bool = False, - override_type: bool = False, - application_id: typing.Optional[int] = None): + def __init__( + self, + client: typing.Union[discord.Client, commands.Bot], + sync_commands: bool = False, + delete_from_unused_guilds: bool = False, + sync_on_cog_reload: bool = False, + override_type: bool = False, + application_id: typing.Optional[int] = None, + ): self._discord = client self.commands = {} self.subcommands = {} @@ -61,12 +61,18 @@ def __init__(self, if self.sync_commands: self._discord.loop.create_task(self.sync_all_commands(delete_from_unused_guilds)) - if not isinstance(client, commands.Bot) and not isinstance(client, commands.AutoShardedBot) and not override_type: - self.logger.warning("Detected discord.Client! It is highly recommended to use `commands.Bot`. Do not add any `on_socket_response` event.") + if ( + not isinstance(client, commands.Bot) + and not isinstance(client, commands.AutoShardedBot) + and not override_type + ): + self.logger.warning( + "Detected discord.Client! It is highly recommended to use `commands.Bot`. Do not add any `on_socket_response` event." + ) self._discord.on_socket_response = self.on_socket_response self.has_listener = False else: - if not hasattr(self._discord, 'slash'): + if not hasattr(self._discord, "slash"): self._discord.slash = self else: raise error.DuplicateSlashClient("You can't have duplicate SlashCommand instances!") @@ -96,7 +102,9 @@ def override_remove_cog(name: str): def override_reload_extension(*args): orig_reload(*args) - self._discord.loop.create_task(self.sync_all_commands(delete_from_unused_guilds)) + self._discord.loop.create_task( + self.sync_all_commands(delete_from_unused_guilds) + ) self._discord.reload_extension = override_reload_extension @@ -110,12 +118,18 @@ def get_cog_commands(self, cog: commands.Cog): :param cog: Cog that has slash commands. :type cog: discord.ext.commands.Cog """ - if hasattr(cog, '_slash_registered'): # Temporary warning - return self.logger.warning("Calling get_cog_commands is no longer required " - "to add cog slash commands. Make sure to remove all calls to this function.") + if hasattr(cog, "_slash_registered"): # Temporary warning + return self.logger.warning( + "Calling get_cog_commands is no longer required " + "to add cog slash commands. Make sure to remove all calls to this function." + ) cog._slash_registered = True # Assuming all went well func_list = [getattr(cog, x) for x in dir(cog)] - res = [x for x in func_list if isinstance(x, (model.CogBaseCommandObject, model.CogSubcommandObject))] + res = [ + x + for x in func_list + if isinstance(x, (model.CogBaseCommandObject, model.CogSubcommandObject)) + ] for x in res: x.cog = cog if isinstance(x, model.CogBaseCommandObject): @@ -134,7 +148,9 @@ def get_cog_commands(self, cog: commands.Cog): for applicable_guild in base_permissions: if applicable_guild not in base_command.permissions: base_command.permissions[applicable_guild] = [] - base_command.permissions[applicable_guild].extend(base_permissions[applicable_guild]) + base_command.permissions[applicable_guild].extend( + base_permissions[applicable_guild] + ) self.commands[x.base].has_subcommands = True @@ -163,11 +179,14 @@ def remove_cog_commands(self, cog): :param cog: Cog that has slash commands. :type cog: discord.ext.commands.Cog """ - if hasattr(cog, '_slash_registered'): + if hasattr(cog, "_slash_registered"): del cog._slash_registered func_list = [getattr(cog, x) for x in dir(cog)] - res = [x for x in func_list if - isinstance(x, (model.CogBaseCommandObject, model.CogSubcommandObject))] + res = [ + x + for x in func_list + if isinstance(x, (model.CogBaseCommandObject, model.CogSubcommandObject)) + ] for x in res: if isinstance(x, model.CogBaseCommandObject): if x.name not in self.commands: @@ -215,10 +234,7 @@ async def to_dict(self): for i in self.commands[x].allowed_guild_ids: if i not in all_guild_ids: all_guild_ids.append(i) - cmds = { - "global": [], - "guild": {x: [] for x in all_guild_ids} - } + cmds = {"global": [], "guild": {x: [] for x in all_guild_ids}} wait = {} # Before merging to return dict, let's first put commands to temporary dict. for x in self.commands: selected = self.commands[x] @@ -231,7 +247,7 @@ async def to_dict(self): "description": selected.description or "No Description.", "options": selected.options or [], "default_permission": selected.default_permission, - "permissions": {} + "permissions": {}, } if y in selected.permissions: command_dict["permissions"][y] = selected.permissions[y] @@ -244,7 +260,7 @@ async def to_dict(self): "description": selected.description or "No Description.", "options": selected.options or [], "default_permission": selected.default_permission, - "permissions": selected.permissions or {} + "permissions": selected.permissions or {}, } wait["global"][x] = copy.deepcopy(command_dict) @@ -263,7 +279,7 @@ async def to_dict(self): "name": sub.name, "description": sub.description or "No Description.", "type": model.SlashCommandOptionType.SUB_COMMAND, - "options": sub.options or [] + "options": sub.options or [], } if sub.allowed_guild_ids: for z in sub.allowed_guild_ids: @@ -276,7 +292,7 @@ async def to_dict(self): "name": y, "description": "No Description.", "type": model.SlashCommandOptionType.SUB_COMMAND_GROUP, - "options": [] + "options": [], } for z in sub: sub_sub = sub[z] @@ -284,7 +300,7 @@ async def to_dict(self): "name": sub_sub.name, "description": sub_sub.description or "No Description.", "type": model.SlashCommandOptionType.SUB_COMMAND, - "options": sub_sub.options or [] + "options": sub_sub.options or [], } if sub_sub.allowed_guild_ids: for i in sub_sub.allowed_guild_ids: @@ -322,21 +338,21 @@ async def sync_all_commands( permissions_map = {} cmds = await self.to_dict() self.logger.info("Syncing commands...") - cmds_formatted = {None: cmds['global']} - for guild in cmds['guild']: - cmds_formatted[guild] = cmds['guild'][guild] + cmds_formatted = {None: cmds["global"]} + for guild in cmds["guild"]: + cmds_formatted[guild] = cmds["guild"][guild] for scope in cmds_formatted: permissions = {} new_cmds = cmds_formatted[scope] - existing_cmds = await self.req.get_all_commands(guild_id = scope) + existing_cmds = await self.req.get_all_commands(guild_id=scope) existing_by_name = {} - to_send=[] + to_send = [] changed = False for cmd in existing_cmds: existing_by_name[cmd["name"]] = model.CommandData(**cmd) - if len(new_cmds) != len(existing_cmds): + if len(new_cmds) != len(existing_cmds): changed = True for command in new_cmds: @@ -346,22 +362,27 @@ async def sync_all_commands( cmd_data = model.CommandData(**command) existing_cmd = existing_by_name[cmd_name] if cmd_data != existing_cmd: - changed=True + changed = True to_send.append(command) else: command_with_id = command command_with_id["id"] = existing_cmd.id to_send.append(command_with_id) else: - changed=True + changed = True to_send.append(command) - if changed: - self.logger.debug(f"Detected changes on {scope if scope is not None else 'global'}, updating them") - existing_cmds = await self.req.put_slash_commands(slash_commands=to_send, guild_id=scope) + self.logger.debug( + f"Detected changes on {scope if scope is not None else 'global'}, updating them" + ) + existing_cmds = await self.req.put_slash_commands( + slash_commands=to_send, guild_id=scope + ) else: - self.logger.debug(f"Detected no changes on {scope if scope is not None else 'global'}, skipping") + self.logger.debug( + f"Detected no changes on {scope if scope is not None else 'global'}, skipping" + ) id_name_map = {} for cmd in existing_cmds: @@ -376,11 +397,10 @@ async def sync_all_commands( permission = { "id": cmd_id, "guild_id": applicable_guild, - "permissions": cmd_permissions[applicable_guild] + "permissions": cmd_permissions[applicable_guild], } permissions_map[applicable_guild].append(permission) - self.logger.info("Syncing permissions...") self.logger.debug(f"Commands permission data are {permissions_map}") for scope in permissions_map: @@ -393,39 +413,45 @@ async def sync_all_commands( else: existing_perms_model = {} for existing_perm in existing_perms: - existing_perms_model[existing_perm["id"]] = model.GuildPermissionsData(**existing_perm) + existing_perms_model[existing_perm["id"]] = model.GuildPermissionsData( + **existing_perm + ) for new_perm in new_perms: if new_perm["id"] not in existing_perms_model: changed = True break - if existing_perms_model[new_perm["id"]] != model.GuildPermissionsData(**new_perm): + if existing_perms_model[new_perm["id"]] != model.GuildPermissionsData( + **new_perm + ): changed = True break - + if changed: self.logger.debug(f"Detected permissions changes on {scope}, updating them") await self.req.update_guild_commands_permissions(scope, new_perms) else: self.logger.debug(f"Detected no permissions changes on {scope}, skipping") - if delete_from_unused_guilds: self.logger.info("Deleting unused guild commands...") - other_guilds = [guild.id for guild in self._discord.guilds if guild.id not in cmds["guild"]] + other_guilds = [ + guild.id for guild in self._discord.guilds if guild.id not in cmds["guild"] + ] # This is an extremly bad way to do this, because slash cmds can be in guilds the bot isn't in # But it's the only way until discord makes an endpoint to request all the guild with cmds registered. - + for guild in other_guilds: with suppress(discord.Forbidden): - existing = await self.req.get_all_commands(guild_id = guild) + existing = await self.req.get_all_commands(guild_id=guild) if len(existing) != 0: self.logger.debug(f"Deleting commands from {guild}") await self.req.put_slash_commands(slash_commands=[], guild_id=guild) - if delete_perms_from_unused_guilds: self.logger.info("Deleting unused guild permissions...") - other_guilds = [guild.id for guild in self._discord.guilds if guild.id not in permissions_map.keys()] + other_guilds = [ + guild.id for guild in self._discord.guilds if guild.id not in permissions_map.keys() + ] for guild in other_guilds: with suppress(discord.Forbidden): self.logger.debug(f"Deleting permissions from {guild}") @@ -435,16 +461,18 @@ async def sync_all_commands( self.logger.info("Completed syncing all commands!") - def add_slash_command(self, - cmd, - name: str = None, - description: str = None, - guild_ids: typing.List[int] = None, - options: list = None, - default_permission: bool = True, - permissions: typing.Dict[int, list] = None, - connector: dict = None, - has_subcommands: bool = False): + def add_slash_command( + self, + cmd, + name: str = None, + description: str = None, + guild_ids: typing.List[int] = None, + options: list = None, + default_permission: bool = True, + permissions: typing.Dict[int, list] = None, + connector: dict = None, + has_subcommands: bool = False, + ): """ Registers slash command to SlashCommand. @@ -496,26 +524,28 @@ def add_slash_command(self, "default_permission": default_permission, "api_permissions": permissions, "connector": connector or {}, - "has_subcommands": has_subcommands + "has_subcommands": has_subcommands, } obj = model.BaseCommandObject(name, _cmd) self.commands[name] = obj self.logger.debug(f"Added command `{name}`") return obj - def add_subcommand(self, - cmd, - base, - subcommand_group=None, - name=None, - description: str = None, - base_description: str = None, - base_default_permission: bool = True, - base_permissions: typing.Dict[int, list] = None, - subcommand_group_description: str = None, - guild_ids: typing.List[int] = None, - options: list = None, - connector: dict = None): + def add_subcommand( + self, + cmd, + base, + subcommand_group=None, + name=None, + description: str = None, + base_description: str = None, + base_default_permission: bool = True, + base_permissions: typing.Dict[int, list] = None, + subcommand_group_description: str = None, + guild_ids: typing.List[int] = None, + options: list = None, + connector: dict = None, + ): """ Registers subcommand to SlashCommand. @@ -567,7 +597,7 @@ def add_subcommand(self, "default_permission": base_default_permission, "api_permissions": base_permissions, "connector": {}, - "has_subcommands": True + "has_subcommands": True, } _sub = { "func": cmd, @@ -577,7 +607,7 @@ def add_subcommand(self, "sub_group_desc": subcommand_group_description, "guild_ids": guild_ids, "api_options": options, - "connector": connector or {} + "connector": connector or {}, } if base not in self.commands: self.commands[base] = model.BaseCommandObject(base, _cmd) @@ -588,7 +618,9 @@ def add_subcommand(self, for applicable_guild in base_permissions: if applicable_guild not in base_command.permissions: base_command.permissions[applicable_guild] = [] - base_command.permissions[applicable_guild].extend(base_permissions[applicable_guild]) + base_command.permissions[applicable_guild].extend( + base_permissions[applicable_guild] + ) if base_command.description: _cmd["description"] = base_command.description if base not in self.subcommands: @@ -605,18 +637,22 @@ def add_subcommand(self, raise error.DuplicateCommand(f"{base} {name}") obj = model.SubcommandObject(_sub, base, name) self.subcommands[base][name] = obj - self.logger.debug(f"Added subcommand `{base} {subcommand_group or ''} {name or cmd.__name__}`") + self.logger.debug( + f"Added subcommand `{base} {subcommand_group or ''} {name or cmd.__name__}`" + ) return obj - def slash(self, - *, - name: str = None, - description: str = None, - guild_ids: typing.List[int] = None, - options: typing.List[dict] = None, - default_permission: bool = True, - permissions: dict = None, - connector: dict = None): + def slash( + self, + *, + name: str = None, + description: str = None, + guild_ids: typing.List[int] = None, + options: typing.List[dict] = None, + default_permission: bool = True, + permissions: dict = None, + connector: dict = None, + ): """ Decorator that registers coroutine as a slash command.\n All decorator args must be passed as keyword-only args.\n @@ -681,26 +717,37 @@ def wrapper(cmd): if decorator_permissions: permissions.update(decorator_permissions) - obj = self.add_slash_command(cmd, name, description, guild_ids, options, default_permission, permissions, connector) + obj = self.add_slash_command( + cmd, + name, + description, + guild_ids, + options, + default_permission, + permissions, + connector, + ) return obj return wrapper - def subcommand(self, - *, - base, - subcommand_group=None, - name=None, - description: str = None, - base_description: str = None, - base_desc: str = None, - base_default_permission: bool = True, - base_permissions: dict = None, - subcommand_group_description: str = None, - sub_group_desc: str = None, - guild_ids: typing.List[int] = None, - options: typing.List[dict] = None, - connector: dict = None): + def subcommand( + self, + *, + base, + subcommand_group=None, + name=None, + description: str = None, + base_description: str = None, + base_desc: str = None, + base_default_permission: bool = True, + base_permissions: dict = None, + subcommand_group_description: str = None, + sub_group_desc: str = None, + guild_ids: typing.List[int] = None, + options: typing.List[dict] = None, + connector: dict = None, + ): """ Decorator that registers subcommand.\n Unlike discord.py, you don't need base command.\n @@ -764,7 +811,20 @@ def wrapper(cmd): if decorator_permissions: base_permissions.update(decorator_permissions) - obj = self.add_subcommand(cmd, base, subcommand_group, name, description, base_description, base_default_permission, base_permissions, subcommand_group_description, guild_ids, options, connector) + obj = self.add_subcommand( + cmd, + base, + subcommand_group, + name, + description, + base_description, + base_default_permission, + base_permissions, + subcommand_group_description, + guild_ids, + options, + connector, + ) return obj return wrapper @@ -772,12 +832,13 @@ def wrapper(cmd): def permission(self, guild_id: int, permissions: list): """ Decorator that add permissions. This will set the permissions for a single guild, you can use it more than once for each command. - :param guild_id: ID of the guild for the permissions. + :param guild_id: ID of the guild for the permissions. :type guild_id: int :param permissions: Permission requirements of the slash command. Default ``None``. :type permissions: dict - + """ + def wrapper(cmd): if not getattr(cmd, "__permissions__", None): cmd.__permissions__ = {} @@ -786,8 +847,13 @@ def wrapper(cmd): return wrapper - async def process_options(self, guild: discord.Guild, options: list, connector: dict, - temporary_auto_convert: dict = None) -> dict: + async def process_options( + self, + guild: discord.Guild, + options: list, + connector: dict, + temporary_auto_convert: dict = None, + ) -> dict: """ Processes Role, User, and Channel option types to discord.py's models. @@ -809,7 +875,7 @@ async def process_options(self, guild: discord.Guild, options: list, connector: # and 2nd as a actual fetching method. [guild.get_member, guild.fetch_member], guild.get_channel, - guild.get_role + guild.get_role, ] types = { @@ -827,7 +893,7 @@ async def process_options(self, guild: discord.Guild, options: list, connector: "ROLE": 2, model.SlashCommandOptionType.ROLE: 2, 8: 2, - "8": 2 + "8": 2, } to_return = {} @@ -852,10 +918,16 @@ async def process_options(self, guild: discord.Guild, options: list, connector: loaded_converter = loaded_converter[1] if not processed: try: - processed = await loaded_converter(int(x["value"])) \ - if iscoroutinefunction(loaded_converter) else \ - loaded_converter(int(x["value"])) - except (discord.Forbidden, discord.HTTPException, discord.NotFound): # Just in case. + processed = ( + await loaded_converter(int(x["value"])) + if iscoroutinefunction(loaded_converter) + else loaded_converter(int(x["value"])) + ) + except ( + discord.Forbidden, + discord.HTTPException, + discord.NotFound, + ): # Just in case. self.logger.warning("Failed fetching discord object! Passing ID instead.") processed = int(x["value"]) to_return[connector.get(x["name"]) or x["name"]] = processed @@ -923,7 +995,10 @@ async def _on_slash(self, to_use): selected_cmd = self.commands[to_use["data"]["name"]] - if selected_cmd.allowed_guild_ids and ctx.guild_id not in selected_cmd.allowed_guild_ids: + if ( + selected_cmd.allowed_guild_ids + and ctx.guild_id not in selected_cmd.allowed_guild_ids + ): return if selected_cmd.has_subcommands and not selected_cmd.func: @@ -940,8 +1015,16 @@ async def _on_slash(self, to_use): for x in selected_cmd.options: temporary_auto_convert[x["name"].lower()] = x["type"] - args = await self.process_options(ctx.guild, to_use["data"]["options"], selected_cmd.connector, temporary_auto_convert) \ - if "options" in to_use["data"] else {} + args = ( + await self.process_options( + ctx.guild, + to_use["data"]["options"], + selected_cmd.connector, + temporary_auto_convert, + ) + if "options" in to_use["data"] + else {} + ) self._discord.dispatch("slash_command", ctx) @@ -980,8 +1063,13 @@ async def handle_subcommand(self, ctx: context.SlashContext, data: dict): for n in selected.options: temporary_auto_convert[n["name"].lower()] = n["type"] - args = await self.process_options(ctx.guild, x["options"], selected.connector, temporary_auto_convert) \ - if "options" in x else {} + args = ( + await self.process_options( + ctx.guild, x["options"], selected.connector, temporary_auto_convert + ) + if "options" in x + else {} + ) self._discord.dispatch("slash_command", ctx) await self.invoke_command(selected, ctx, args) return @@ -993,8 +1081,13 @@ async def handle_subcommand(self, ctx: context.SlashContext, data: dict): for n in selected.options: temporary_auto_convert[n["name"].lower()] = n["type"] - args = await self.process_options(ctx.guild, sub_opts, selected.connector, temporary_auto_convert) \ - if "options" in sub else {} + args = ( + await self.process_options( + ctx.guild, sub_opts, selected.connector, temporary_auto_convert + ) + if "options" in sub + else {} + ) self._discord.dispatch("slash_command", ctx) await self.invoke_command(selected, ctx, args) @@ -1025,7 +1118,7 @@ async def on_slash_command_error(ctx, ex): :return: """ if self.has_listener: - if self._discord.extra_events.get('on_slash_command_error'): + if self._discord.extra_events.get("on_slash_command_error"): self._discord.dispatch("slash_command_error", ctx, ex) return if hasattr(self._discord, "on_slash_command_error"): diff --git a/discord_slash/cog_ext.py b/discord_slash/cog_ext.py index 1d5589d5c..54e699f1f 100644 --- a/discord_slash/cog_ext.py +++ b/discord_slash/cog_ext.py @@ -1,17 +1,20 @@ -import typing import inspect +import typing + from .model import CogBaseCommandObject, CogSubcommandObject from .utils import manage_commands -def cog_slash(*, - name: str = None, - description: str = None, - guild_ids: typing.List[int] = None, - options: typing.List[dict] = None, - default_permission: bool = True, - permissions: typing.Dict[int, list] = None, - connector: dict = None): +def cog_slash( + *, + name: str = None, + description: str = None, + guild_ids: typing.List[int] = None, + options: typing.List[dict] = None, + default_permission: bool = True, + permissions: typing.Dict[int, list] = None, + connector: dict = None +): """ Decorator for Cog to add slash command.\n Almost same as :func:`.client.SlashCommand.slash`. @@ -43,6 +46,7 @@ async def ping(self, ctx: SlashContext): :param connector: Kwargs connector for the command. Default ``None``. :type connector: dict """ + def wrapper(cmd): desc = description or inspect.getdoc(cmd) if options is None: @@ -58,26 +62,29 @@ def wrapper(cmd): "default_permission": default_permission, "api_permissions": permissions, "connector": connector, - "has_subcommands": False + "has_subcommands": False, } return CogBaseCommandObject(name or cmd.__name__, _cmd) + return wrapper -def cog_subcommand(*, - base, - subcommand_group=None, - name=None, - description: str = None, - base_description: str = None, - base_desc: str = None, - base_default_permission: bool = True, - base_permissions: typing.Dict[int, list] = None, - subcommand_group_description: str = None, - sub_group_desc: str = None, - guild_ids: typing.List[int] = None, - options: typing.List[dict] = None, - connector: dict = None): +def cog_subcommand( + *, + base, + subcommand_group=None, + name=None, + description: str = None, + base_description: str = None, + base_desc: str = None, + base_default_permission: bool = True, + base_permissions: typing.Dict[int, list] = None, + subcommand_group_description: str = None, + sub_group_desc: str = None, + guild_ids: typing.List[int] = None, + options: typing.List[dict] = None, + connector: dict = None +): """ Decorator for Cog to add subcommand.\n Almost same as :func:`.client.SlashCommand.subcommand`. @@ -138,7 +145,7 @@ def wrapper(cmd): "default_permission": base_default_permission, "api_permissions": base_permissions, "connector": {}, - "has_subcommands": True + "has_subcommands": True, } _sub = { @@ -149,7 +156,8 @@ def wrapper(cmd): "sub_group_desc": subcommand_group_description, "guild_ids": guild_ids, "api_options": opts, - "connector": connector + "connector": connector, } return CogSubcommandObject(base, _cmd, subcommand_group, name or cmd.__name__, _sub) + return wrapper diff --git a/discord_slash/const.py b/discord_slash/const.py new file mode 100644 index 000000000..5b64473ba --- /dev/null +++ b/discord_slash/const.py @@ -0,0 +1,5 @@ +"""Discord Slash Constants""" + +__version__ = "1.2.2" + +BASE_API = "https://discord.com/api/v8" diff --git a/discord_slash/context.py b/discord_slash/context.py index fa56a10cb..5972ccad5 100644 --- a/discord_slash/context.py +++ b/discord_slash/context.py @@ -1,17 +1,13 @@ import datetime import typing -import asyncio from warnings import warn import discord -from contextlib import suppress from discord.ext import commands from discord.utils import snowflake_time -from . import http -from . import error -from . import model -from . dpy_overrides import ComponentMessage +from . import error, http, model +from .dpy_overrides import ComponentMessage class InteractionContext: @@ -36,11 +32,13 @@ class InteractionContext: :ivar author: User or Member instance of the command invoke. """ - def __init__(self, - _http: http.SlashCommandRequest, - _json: dict, - _discord: typing.Union[discord.Client, commands.Bot], - logger): + def __init__( + self, + _http: http.SlashCommandRequest, + _json: dict, + _discord: typing.Union[discord.Client, commands.Bot], + logger, + ): self._token = _json["token"] self.message = None # Should be set later. self.interaction_id = _json["id"] @@ -51,10 +49,14 @@ def __init__(self, self.responded = False self._deferred_hidden = False # To check if the patch to the deferred response matches self.guild_id = int(_json["guild_id"]) if "guild_id" in _json.keys() else None - self.author_id = int(_json["member"]["user"]["id"] if "member" in _json.keys() else _json["user"]["id"]) + self.author_id = int( + _json["member"]["user"]["id"] if "member" in _json.keys() else _json["user"]["id"] + ) self.channel_id = int(_json["channel_id"]) if self.guild: - self.author = discord.Member(data=_json["member"], state=self.bot._connection, guild=self.guild) + self.author = discord.Member( + data=_json["member"], state=self.bot._connection, guild=self.guild + ) elif self.guild_id: self.author = discord.User(data=_json["member"]["user"], state=self.bot._connection) else: @@ -63,12 +65,20 @@ def __init__(self, @property def _deffered_hidden(self): - warn("`_deffered_hidden` as been renamed to `_deferred_hidden`.", DeprecationWarning, stacklevel=2) + warn( + "`_deffered_hidden` as been renamed to `_deferred_hidden`.", + DeprecationWarning, + stacklevel=2, + ) return self._deferred_hidden @_deffered_hidden.setter def _deffered_hidden(self, value): - warn("`_deffered_hidden` as been renamed to `_deferred_hidden`.", DeprecationWarning, stacklevel=2) + warn( + "`_deffered_hidden` as been renamed to `_deferred_hidden`.", + DeprecationWarning, + stacklevel=2, + ) self._deferred_hidden = value @property @@ -114,18 +124,20 @@ async def defer(self, hidden: bool = False): await self._http.post_initial_response(base, self.interaction_id, self._token) self.deferred = True - async def send(self, - content: str = "", *, - embed: discord.Embed = None, - embeds: typing.List[discord.Embed] = None, - tts: bool = False, - file: discord.File = None, - files: typing.List[discord.File] = None, - allowed_mentions: discord.AllowedMentions = None, - hidden: bool = False, - delete_after: float = None, - components: typing.List[dict] = None, - ) -> model.SlashMessage: + async def send( + self, + content: str = "", + *, + embed: discord.Embed = None, + embeds: typing.List[discord.Embed] = None, + tts: bool = False, + file: discord.File = None, + files: typing.List[discord.File] = None, + allowed_mentions: discord.AllowedMentions = None, + hidden: bool = False, + delete_after: float = None, + components: typing.List[dict] = None, + ) -> model.SlashMessage: """ Sends response of the slash command. @@ -172,14 +184,19 @@ async def send(self, if delete_after and hidden: raise error.IncorrectFormat("You can't delete a hidden message!") if components and not all(comp.get("type") == 1 for comp in components): - raise error.IncorrectFormat("The top level of the components list must be made of ActionRows!") + raise error.IncorrectFormat( + "The top level of the components list must be made of ActionRows!" + ) base = { "content": content, "tts": tts, "embeds": [x.to_dict() for x in embeds] if embeds else [], - "allowed_mentions": allowed_mentions.to_dict() if allowed_mentions - else self.bot.allowed_mentions.to_dict() if self.bot.allowed_mentions else {}, + "allowed_mentions": allowed_mentions.to_dict() + if allowed_mentions + else self.bot.allowed_mentions.to_dict() + if self.bot.allowed_mentions + else {}, "components": components or [], } if hidden: @@ -199,10 +216,7 @@ async def send(self, resp = await self._http.edit(base, self._token, files=files) self.deferred = False else: - json_data = { - "type": 4, - "data": base - } + json_data = {"type": 4, "data": base} await self._http.post_initial_response(json_data, self.interaction_id, self._token) if not hidden: resp = await self._http.edit({}, self._token) @@ -215,11 +229,13 @@ async def send(self, for file in files: file.close() if not hidden: - smsg = model.SlashMessage(state=self.bot._connection, - data=resp, - channel=self.channel or discord.Object(id=self.channel_id), - _http=self._http, - interaction_token=self._token) + smsg = model.SlashMessage( + state=self.bot._connection, + data=resp, + channel=self.channel or discord.Object(id=self.channel_id), + _http=self._http, + interaction_token=self._token, + ) if delete_after: self.bot.loop.create_task(smsg.delete(delay=delete_after)) if initial_message: @@ -240,11 +256,14 @@ class SlashContext(InteractionContext): :ivar subcommand_group: Subcommand group of the command. :ivar command_id: ID of the command. """ - def __init__(self, - _http: http.SlashCommandRequest, - _json: dict, - _discord: typing.Union[discord.Client, commands.Bot], - logger): + + def __init__( + self, + _http: http.SlashCommandRequest, + _json: dict, + _discord: typing.Union[discord.Client, commands.Bot], + logger, + ): self.name = self.command = self.invoked_with = _json["data"]["name"] self.args = [] self.kwargs = {} @@ -264,11 +283,14 @@ class ComponentContext(InteractionContext): :ivar origin_message: The origin message of the component. Not available if the origin message was ephemeral. :ivar origin_message_id: The ID of the origin message. """ - def __init__(self, - _http: http.SlashCommandRequest, - _json: dict, - _discord: typing.Union[discord.Client, commands.Bot], - logger): + + def __init__( + self, + _http: http.SlashCommandRequest, + _json: dict, + _discord: typing.Union[discord.Client, commands.Bot], + logger, + ): self.custom_id = self.component_id = _json["data"]["custom_id"] self.component_type = _json["data"]["component_type"] super().__init__(_http=_http, _json=_json, _discord=_discord, logger=logger) @@ -276,8 +298,9 @@ def __init__(self, self.origin_message_id = int(_json["message"]["id"]) if "message" in _json.keys() else None if self.origin_message_id and (_json["message"]["flags"] & 64) != 64: - self.origin_message = ComponentMessage(state=self.bot._connection, channel=self.channel, - data=_json["message"]) + self.origin_message = ComponentMessage( + state=self.bot._connection, channel=self.channel, data=_json["message"] + ) async def defer(self, hidden: bool = False, edit_origin: bool = False): """ @@ -331,8 +354,13 @@ async def edit_origin(self, **fields): _resp["embeds"] = [x.to_dict() for x in embeds] allowed_mentions = fields.get("allowed_mentions") - _resp["allowed_mentions"] = allowed_mentions.to_dict() if allowed_mentions else \ - self.bot.allowed_mentions.to_dict() if self.bot.allowed_mentions else {} + _resp["allowed_mentions"] = ( + allowed_mentions.to_dict() + if allowed_mentions + else self.bot.allowed_mentions.to_dict() + if self.bot.allowed_mentions + else {} + ) if not self.responded: if files and not self.deferred: @@ -340,12 +368,11 @@ async def edit_origin(self, **fields): if self.deferred: _json = await self._http.edit(_resp, self._token, files=files) self.deferred = False - else: - json_data = { - "type": 7, - "data": _resp - } - _json = await self._http.post_initial_response(json_data, self.interaction_id, self._token) + else: # noqa: F841 + json_data = {"type": 7, "data": _resp} + _json = await self._http.post_initial_response( # noqa: F841 + json_data, self.interaction_id, self._token + ) self.responded = True else: raise error.IncorrectFormat("Already responded") diff --git a/discord_slash/dpy_overrides.py b/discord_slash/dpy_overrides.py index 58d63c732..f943f29b4 100644 --- a/discord_slash/dpy_overrides.py +++ b/discord_slash/dpy_overrides.py @@ -1,10 +1,7 @@ import discord +from discord import AllowedMentions, File, InvalidArgument, abc, http, utils from discord.ext import commands -from discord import AllowedMentions, InvalidArgument, File from discord.http import Route -from discord import http -from discord import abc -from discord import utils class ComponentMessage(discord.Message): @@ -12,7 +9,7 @@ class ComponentMessage(discord.Message): def __init__(self, *, state, channel, data): super().__init__(state=state, channel=channel, data=data) - self.components = data['components'] + self.components = data["components"] def new_override(cls, *args, **kwargs): @@ -25,71 +22,96 @@ def new_override(cls, *args, **kwargs): discord.message.Message.__new__ = new_override -def send_files(self, channel_id, *, files, content=None, tts=False, embed=None, components=None, - nonce=None, allowed_mentions=None, message_reference=None): - r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) +def send_files( + self, + channel_id, + *, + files, + content=None, + tts=False, + embed=None, + components=None, + nonce=None, + allowed_mentions=None, + message_reference=None +): + r = Route("POST", "/channels/{channel_id}/messages", channel_id=channel_id) form = [] - payload = {'tts': tts} + payload = {"tts": tts} if content: - payload['content'] = content + payload["content"] = content if embed: - payload['embed'] = embed + payload["embed"] = embed if components: - payload['components'] = components + payload["components"] = components if nonce: - payload['nonce'] = nonce + payload["nonce"] = nonce if allowed_mentions: - payload['allowed_mentions'] = allowed_mentions + payload["allowed_mentions"] = allowed_mentions if message_reference: - payload['message_reference'] = message_reference + payload["message_reference"] = message_reference - form.append({'name': 'payload_json', 'value': utils.to_json(payload)}) + form.append({"name": "payload_json", "value": utils.to_json(payload)}) if len(files) == 1: file = files[0] - form.append({ - 'name': 'file', - 'value': file.fp, - 'filename': file.filename, - 'content_type': 'application/octet-stream' - }) + form.append( + { + "name": "file", + "value": file.fp, + "filename": file.filename, + "content_type": "application/octet-stream", + } + ) else: for index, file in enumerate(files): - form.append({ - 'name': 'file%s' % index, - 'value': file.fp, - 'filename': file.filename, - 'content_type': 'application/octet-stream' - }) + form.append( + { + "name": "file%s" % index, + "value": file.fp, + "filename": file.filename, + "content_type": "application/octet-stream", + } + ) return self.request(r, form=form, files=files) -def send_message(self, channel_id, content, *, tts=False, embed=None, components=None, - nonce=None, allowed_mentions=None, message_reference=None): - r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) +def send_message( + self, + channel_id, + content, + *, + tts=False, + embed=None, + components=None, + nonce=None, + allowed_mentions=None, + message_reference=None +): + r = Route("POST", "/channels/{channel_id}/messages", channel_id=channel_id) payload = {} if content: - payload['content'] = content + payload["content"] = content if tts: - payload['tts'] = True + payload["tts"] = True if embed: - payload['embed'] = embed + payload["embed"] = embed if components: - payload['components'] = components + payload["components"] = components if nonce: - payload['nonce'] = nonce + payload["nonce"] = nonce if allowed_mentions: - payload['allowed_mentions'] = allowed_mentions + payload["allowed_mentions"] = allowed_mentions if message_reference: - payload['message_reference'] = message_reference + payload["message_reference"] = message_reference return self.request(r, json=payload) @@ -98,10 +120,21 @@ def send_message(self, channel_id, content, *, tts=False, embed=None, components http.HTTPClient.send_message = send_message -async def send(self, content=None, *, tts=False, embed=None, file=None, components=None, - files=None, delete_after=None, nonce=None, - allowed_mentions=None, reference=None, - mention_author=None): +async def send( + self, + content=None, + *, + tts=False, + embed=None, + file=None, + components=None, + files=None, + delete_after=None, + nonce=None, + allowed_mentions=None, + reference=None, + mention_author=None +): """|coro| Sends a message to the destination with the content given. @@ -195,47 +228,70 @@ async def send(self, content=None, *, tts=False, embed=None, file=None, componen if mention_author is not None: allowed_mentions = allowed_mentions or AllowedMentions().to_dict() - allowed_mentions['replied_user'] = bool(mention_author) + allowed_mentions["replied_user"] = bool(mention_author) if reference is not None: try: reference = reference.to_message_reference_dict() except AttributeError: - raise InvalidArgument('reference parameter must be Message or MessageReference') from None + raise InvalidArgument( + "reference parameter must be Message or MessageReference" + ) from None if file is not None and files is not None: - raise InvalidArgument('cannot pass both file and files parameter to send()') + raise InvalidArgument("cannot pass both file and files parameter to send()") if file is not None: if not isinstance(file, File): - raise InvalidArgument('file parameter must be File') + raise InvalidArgument("file parameter must be File") try: - data = await state.http.send_files(channel.id, files=[file], allowed_mentions=allowed_mentions, - content=content, tts=tts, embed=embed, nonce=nonce, - components=components, - message_reference=reference) + data = await state.http.send_files( + channel.id, + files=[file], + allowed_mentions=allowed_mentions, + content=content, + tts=tts, + embed=embed, + nonce=nonce, + components=components, + message_reference=reference, + ) finally: file.close() elif files is not None: if len(files) > 10: - raise InvalidArgument('files parameter must be a list of up to 10 elements') + raise InvalidArgument("files parameter must be a list of up to 10 elements") elif not all(isinstance(file, File) for file in files): - raise InvalidArgument('files parameter must be a list of File') + raise InvalidArgument("files parameter must be a list of File") try: - data = await state.http.send_files(channel.id, files=files, content=content, tts=tts, - embed=embed, nonce=nonce, allowed_mentions=allowed_mentions, - components=components, - message_reference=reference) + data = await state.http.send_files( + channel.id, + files=files, + content=content, + tts=tts, + embed=embed, + nonce=nonce, + allowed_mentions=allowed_mentions, + components=components, + message_reference=reference, + ) finally: for f in files: f.close() else: - data = await state.http.send_message(channel.id, content, tts=tts, embed=embed, components=components, - nonce=nonce, allowed_mentions=allowed_mentions, - message_reference=reference) + data = await state.http.send_message( + channel.id, + content, + tts=tts, + embed=embed, + components=components, + nonce=nonce, + allowed_mentions=allowed_mentions, + message_reference=reference, + ) ret = state.create_message(channel=channel, data=data) if delete_after is not None: @@ -251,4 +307,5 @@ async def send_override(context_or_channel, *args, **kwargs): return await send(channel, *args, **kwargs) + abc.Messageable.send = send_override diff --git a/discord_slash/error.py b/discord_slash/error.py index 8df1b92de..897576281 100644 --- a/discord_slash/error.py +++ b/discord_slash/error.py @@ -18,6 +18,7 @@ class RequestFailure(SlashCommandError): :ivar status: Status code of failed response. :ivar msg: Message of failed response. """ + def __init__(self, status: int, msg: str): self.status = status self.msg = msg @@ -34,6 +35,7 @@ class DuplicateCommand(SlashCommandError): """ There is a duplicate command name. """ + def __init__(self, name: str): super().__init__(f"Duplicate command name detected: {name}") @@ -60,7 +62,7 @@ class IncorrectCommandData(SlashCommandError): """ Incorrect data was passed to a slash command data object """ - + class AlreadyResponded(SlashCommandError): """ diff --git a/discord_slash/http.py b/discord_slash/http.py index 6becfc078..ee5ebb056 100644 --- a/discord_slash/http.py +++ b/discord_slash/http.py @@ -1,14 +1,18 @@ import json import typing + import aiohttp import discord from discord.http import Route + from . import error +from .const import BASE_API class CustomRoute(Route): """discord.py's Route but changed ``BASE`` to use at slash command.""" - BASE = "https://discord.com/api/v8" + + BASE = BASE_API class SlashCommandRequest: @@ -29,9 +33,7 @@ def put_slash_commands(self, slash_commands: list, guild_id): :param slash_commands: List of all the slash commands to make a put request to discord with. :param guild_id: ID of the guild to set the commands on. Pass `None` for the global scope. """ - return self.command_request( - method="PUT", guild_id = guild_id, json = slash_commands - ) + return self.command_request(method="PUT", guild_id=guild_id, json=slash_commands) def remove_slash_command(self, guild_id, cmd_id): """ @@ -41,9 +43,7 @@ def remove_slash_command(self, guild_id, cmd_id): :param cmd_id: ID of the command. :return: Response code of the request. """ - return self.command_request( - method="DELETE", guild_id=guild_id, url_ending=f"/{cmd_id}" - ) + return self.command_request(method="DELETE", guild_id=guild_id, url_ending=f"/{cmd_id}") def get_all_commands(self, guild_id=None): """ @@ -70,11 +70,11 @@ def update_guild_commands_permissions(self, guild_id, perms_dict): :param guild_id: ID of the target guild to register command permissions. :return: JSON Response of the request. """ - return self.command_request(method="PUT", guild_id=guild_id, json=perms_dict, url_ending="/permissions") + return self.command_request( + method="PUT", guild_id=guild_id, json=perms_dict, url_ending="/permissions" + ) - def add_slash_command( - self, guild_id, cmd_name: str, description: str, options: list = None - ): + def add_slash_command(self, guild_id, cmd_name: str, description: str, options: list = None): """ Sends a slash command add request to Discord API. @@ -85,7 +85,7 @@ def add_slash_command( :return: JSON Response of the request. """ base = {"name": cmd_name, "description": description, "options": options or []} - return self.command_request(json=base, method="POST", guild_id = guild_id) + return self.command_request(json=base, method="POST", guild_id=guild_id) def command_request(self, method, guild_id, url_ending="", **kwargs): r""" @@ -120,7 +120,7 @@ def post_followup(self, _resp, token, files: typing.List[discord.File] = None): def post_initial_response(self, _resp, interaction_id, token): """ Sends an initial "POST" response to the Discord API. - + :param _resp: Command response. :type _resp: dict :param interaction_id: Interaction ID. @@ -129,8 +129,10 @@ def post_initial_response(self, _resp, interaction_id, token): """ return self.command_response(token, False, "POST", interaction_id, json=_resp) - def command_response(self, token, use_webhook, method, interaction_id= None, url_ending = "", **kwargs): - """ + def command_response( + self, token, use_webhook, method, interaction_id=None, url_ending="", **kwargs + ): + r""" Sends a command response to discord (POST, PATCH, DELETE) :param token: Interaction token @@ -142,21 +144,33 @@ def command_response(self, token, use_webhook, method, interaction_id= None, url :return: Coroutine """ if not use_webhook and not interaction_id: - raise error.IncorrectFormat("Internal Error! interaction_id must be set if use_webhook is False") - req_url = f"/webhooks/{self.application_id}/{token}" if use_webhook else f"/interactions/{interaction_id}/{token}/callback" + raise error.IncorrectFormat( + "Internal Error! interaction_id must be set if use_webhook is False" + ) + req_url = ( + f"/webhooks/{self.application_id}/{token}" + if use_webhook + else f"/interactions/{interaction_id}/{token}/callback" + ) req_url += url_ending route = CustomRoute(method, req_url) return self._discord.http.request(route, **kwargs) - def request_with_files(self, _resp, files: typing.List[discord.File], token, method, url_ending = ""): + def request_with_files( + self, _resp, files: typing.List[discord.File], token, method, url_ending="" + ): form = aiohttp.FormData() form.add_field("payload_json", json.dumps(_resp)) for x in range(len(files)): name = f"file{x if len(files) > 1 else ''}" sel = files[x] - form.add_field(name, sel.fp, filename=sel.filename, content_type="application/octet-stream") - return self.command_response(token, True, method, data=form, files=files, url_ending=url_ending) + form.add_field( + name, sel.fp, filename=sel.filename, content_type="application/octet-stream" + ) + return self.command_response( + token, True, method, data=form, files=files, url_ending=url_ending + ) def edit(self, _resp, token, message_id="@original", files: typing.List[discord.File] = None): """ @@ -172,8 +186,8 @@ def edit(self, _resp, token, message_id="@original", files: typing.List[discord. """ req_url = f"/messages/{message_id}" if files: - return self.request_with_files(_resp, files, token, "PATCH", url_ending = req_url) - return self.command_response(token, True, "PATCH", url_ending = req_url, json=_resp) + return self.request_with_files(_resp, files, token, "PATCH", url_ending=req_url) + return self.command_response(token, True, "PATCH", url_ending=req_url, json=_resp) def delete(self, token, message_id="@original"): """ @@ -184,4 +198,4 @@ def delete(self, token, message_id="@original"): :return: Coroutine """ req_url = f"/messages/{message_id}" - return self.command_response(token, True, "DELETE", url_ending = req_url) + return self.command_response(token, True, "DELETE", url_ending=req_url) diff --git a/discord_slash/model.py b/discord_slash/model.py index df9f7021b..c8b4f2a34 100644 --- a/discord_slash/model.py +++ b/discord_slash/model.py @@ -1,16 +1,14 @@ import asyncio import datetime - -import discord -from enum import IntEnum from contextlib import suppress +from enum import IntEnum from inspect import iscoroutinefunction -from discord.ext.commands import CooldownMapping, CommandOnCooldown +import discord +from discord.ext.commands import CommandOnCooldown, CooldownMapping -from . import http -from . import error -from . dpy_overrides import ComponentMessage +from . import error, http +from .dpy_overrides import ComponentMessage class ChoiceData: @@ -40,9 +38,7 @@ class OptionData: :ivar options: List of :class:`OptionData`, this will be present if it's a subcommand group """ - def __init__( - self, name, description, required=False, choices=None, options=None, **kwargs - ): + def __init__(self, name, description, required=False, choices=None, options=None, **kwargs): self.name = name self.description = description self.type = kwargs.get("type") @@ -83,7 +79,15 @@ class CommandData: """ def __init__( - self, name, description, options=None, default_permission=True, id=None, application_id=None, version=None, **kwargs + self, + name, + description, + options=None, + default_permission=True, + id=None, + application_id=None, + version=None, + **kwargs ): self.name = name self.description = description @@ -101,10 +105,10 @@ def __init__( def __eq__(self, other): if isinstance(other, CommandData): return ( - self.name == other.name - and self.description == other.description - and self.options == other.options - and self.default_permission == other.default_permission + self.name == other.name + and self.description == other.description + and self.options == other.options + and self.default_permission == other.default_permission ) else: return False @@ -137,7 +141,7 @@ def __init__(self, name, cmd): # Let's reuse old command formatting. # Since this isn't inherited from `discord.ext.commands.Command`, discord.py's check decorator will # add checks at this var. self.__commands_checks__ = [] - if hasattr(self.func, '__commands_checks__'): + if hasattr(self.func, "__commands_checks__"): self.__commands_checks__ = self.func.__commands_checks__ cooldown = None @@ -177,7 +181,7 @@ async def _concurrency_checks(self, ctx): try: # cooldown checks self._prepare_cooldowns(ctx) - except: + except Exception: if self._max_concurrency is not None: await self._max_concurrency.release(ctx) raise @@ -282,7 +286,10 @@ async def can_run(self, ctx) -> bool: :type ctx: .context.SlashContext :return: bool """ - res = [bool(x(ctx)) if not iscoroutinefunction(x) else bool(await x(ctx)) for x in self.__commands_checks__] + res = [ + bool(x(ctx)) if not iscoroutinefunction(x) else bool(await x(ctx)) + for x in self.__commands_checks__ + ] return False not in res @@ -307,6 +314,7 @@ def __init__(self, name, cmd): # Let's reuse old command formatting. self.default_permission = cmd["default_permission"] self.permissions = cmd["api_permissions"] or {} + class SubcommandObject(CommandObject): """ Subcommand object of this extension. @@ -362,6 +370,7 @@ class SlashCommandOptionType(IntEnum): """ Equivalent of `ApplicationCommandOptionType `_ in the Discord API. """ + SUB_COMMAND = 1 SUB_COMMAND_GROUP = 2 STRING = 3 @@ -379,13 +388,19 @@ def from_type(cls, t: type): :param t: The type or object to get a SlashCommandOptionType for. :return: :class:`.model.SlashCommandOptionType` or ``None`` """ - if issubclass(t, str): return cls.STRING - if issubclass(t, bool): return cls.BOOLEAN + if issubclass(t, str): + return cls.STRING + if issubclass(t, bool): + return cls.BOOLEAN # The check for bool MUST be above the check for integers as booleans subclass integers - if issubclass(t, int): return cls.INTEGER - if issubclass(t, discord.abc.User): return cls.USER - if issubclass(t, discord.abc.GuildChannel): return cls.CHANNEL - if issubclass(t, discord.abc.Role): return cls.ROLE + if issubclass(t, int): + return cls.INTEGER + if issubclass(t, discord.abc.User): + return cls.USER + if issubclass(t, discord.abc.GuildChannel): + return cls.CHANNEL + if issubclass(t, discord.abc.Role): + return cls.ROLE class SlashMessage(ComponentMessage): @@ -432,8 +447,13 @@ async def _slash_edit(self, **fields): _resp["embeds"] = [x.to_dict() for x in embeds] allowed_mentions = fields.get("allowed_mentions") - _resp["allowed_mentions"] = allowed_mentions.to_dict() if allowed_mentions else \ - self._state.allowed_mentions.to_dict() if self._state.allowed_mentions else {} + _resp["allowed_mentions"] = ( + allowed_mentions.to_dict() + if allowed_mentions + else self._state.allowed_mentions.to_dict() + if self._state.allowed_mentions + else {} + ) await self._http.edit(_resp, self.__interaction_token, self.id, files=files) @@ -477,6 +497,7 @@ class PermissionData: :ivar type: The ``SlashCommandPermissionsType`` type of this permission. :ivar permission: State of permission. ``True`` to allow, ``False`` to disallow. """ + def __init__(self, id, type, permission, **kwargs): self.id = id self.type = type @@ -498,9 +519,10 @@ class GuildPermissionsData: Slash permissions data for a command in a guild. :ivar id: Command id, provided by discord. - :ivar guild_id: Guild id that the permissions are in. + :ivar guild_id: Guild id that the permissions are in. :ivar permissions: List of permissions dict. """ + def __init__(self, id, guild_id, permissions, **kwargs): self.id = id self.guild_id = guild_id @@ -524,10 +546,13 @@ class SlashCommandPermissionType(IntEnum): """ Equivalent of `ApplicationCommandPermissionType `_ in the Discord API. """ + ROLE = 1 USER = 2 @classmethod def from_type(cls, t: type): - if issubclass(t, discord.abc.Role): return cls.ROLE - if issubclass(t, discord.abc.User): return cls.USER + if issubclass(t, discord.abc.Role): + return cls.ROLE + if issubclass(t, discord.abc.User): + return cls.USER diff --git a/discord_slash/utils/manage_commands.py b/discord_slash/utils/manage_commands.py index 37d4dc849..8544b9ba0 100644 --- a/discord_slash/utils/manage_commands.py +++ b/discord_slash/utils/manage_commands.py @@ -1,19 +1,18 @@ -import typing -import inspect import asyncio -import aiohttp -from ..error import RequestFailure, IncorrectType -from ..model import SlashCommandOptionType, SlashCommandPermissionType +import inspect +import typing from collections.abc import Callable from typing import Union +import aiohttp -async def add_slash_command(bot_id, - bot_token: str, - guild_id, - cmd_name: str, - description: str, - options: list = None): +from ..error import IncorrectType, RequestFailure +from ..model import SlashCommandOptionType, SlashCommandPermissionType + + +async def add_slash_command( + bot_id, bot_token: str, guild_id, cmd_name: str, description: str, options: list = None +): """ A coroutine that sends a slash command add request to Discord API. @@ -28,27 +27,24 @@ async def add_slash_command(bot_id, """ url = f"https://discord.com/api/v8/applications/{bot_id}" url += "/commands" if not guild_id else f"/guilds/{guild_id}/commands" - base = { - "name": cmd_name, - "description": description, - "options": options or [] - } + base = {"name": cmd_name, "description": description, "options": options or []} async with aiohttp.ClientSession() as session: - async with session.post(url, headers={"Authorization": f"Bot {bot_token}"}, json=base) as resp: + async with session.post( + url, headers={"Authorization": f"Bot {bot_token}"}, json=base + ) as resp: if resp.status == 429: _json = await resp.json() await asyncio.sleep(_json["retry_after"]) - return await add_slash_command(bot_id, bot_token, guild_id, cmd_name, description, options) + return await add_slash_command( + bot_id, bot_token, guild_id, cmd_name, description, options + ) if not 200 <= resp.status < 300: raise RequestFailure(resp.status, await resp.text()) return await resp.json() -async def remove_slash_command(bot_id, - bot_token, - guild_id, - cmd_id): +async def remove_slash_command(bot_id, bot_token, guild_id, cmd_id): """ A coroutine that sends a slash command remove request to Discord API. @@ -73,9 +69,7 @@ async def remove_slash_command(bot_id, return resp.status -async def get_all_commands(bot_id, - bot_token, - guild_id=None): +async def get_all_commands(bot_id, bot_token, guild_id=None): """ A coroutine that sends a slash command get request to Discord API. @@ -98,9 +92,7 @@ async def get_all_commands(bot_id, return await resp.json() -async def remove_all_commands(bot_id, - bot_token, - guild_ids: typing.List[int] = None): +async def remove_all_commands(bot_id, bot_token, guild_ids: typing.List[int] = None): """ Remove all slash commands. @@ -118,9 +110,7 @@ async def remove_all_commands(bot_id, pass -async def remove_all_commands_in(bot_id, - bot_token, - guild_id=None): +async def remove_all_commands_in(bot_id, bot_token, guild_id=None): """ Remove all slash commands in area. @@ -128,24 +118,13 @@ async def remove_all_commands_in(bot_id, :param bot_token: Token of the bot. :param guild_id: ID of the guild to remove commands. Pass `None` to remove all global commands. """ - commands = await get_all_commands( - bot_id, - bot_token, - guild_id - ) + commands = await get_all_commands(bot_id, bot_token, guild_id) for x in commands: - await remove_slash_command( - bot_id, - bot_token, - guild_id, - x['id'] - ) + await remove_slash_command(bot_id, bot_token, guild_id, x["id"]) -async def get_all_guild_commands_permissions(bot_id, - bot_token, - guild_id): +async def get_all_guild_commands_permissions(bot_id, bot_token, guild_id): """ A coroutine that sends a gets all the commands permissions for that guild. @@ -167,10 +146,7 @@ async def get_all_guild_commands_permissions(bot_id, return await resp.json() -async def get_guild_command_permissions(bot_id, - bot_token, - guild_id, - command_id): +async def get_guild_command_permissions(bot_id, bot_token, guild_id, command_id): """ A coroutine that sends a request to get a single command's permissions in guild @@ -192,13 +168,8 @@ async def get_guild_command_permissions(bot_id, raise RequestFailure(resp.status, await resp.text()) return await resp.json() - -async def update_single_command_permissions(bot_id, - bot_token, - guild_id, - command_id, - permissions): +async def update_single_command_permissions(bot_id, bot_token, guild_id, command_id, permissions): """ A coroutine that sends a request to update a single command's permissions in guild @@ -212,22 +183,23 @@ async def update_single_command_permissions(bot_id, """ url = f"https://discord.com/api/v8/applications/{bot_id}/guilds/{guild_id}/commands/{command_id}/permissions" async with aiohttp.ClientSession() as session: - async with session.put(url, headers={"Authorization": f"Bot {bot_token}"}, json=permissions) as resp: + async with session.put( + url, headers={"Authorization": f"Bot {bot_token}"}, json=permissions + ) as resp: if resp.status == 429: _json = await resp.json() await asyncio.sleep(_json["retry_after"]) - return await update_single_command_permissions(bot_id, bot_token, guild_id, command_id, permissions) + return await update_single_command_permissions( + bot_id, bot_token, guild_id, command_id, permissions + ) if not 200 <= resp.status < 300: raise RequestFailure(resp.status, await resp.text()) return await resp.json() -async def update_guild_commands_permissions(bot_id, - bot_token, - guild_id, - cmd_permissions): +async def update_guild_commands_permissions(bot_id, bot_token, guild_id, cmd_permissions): """ - A coroutine that updates permissions for all commands in a guild. + A coroutine that updates permissions for all commands in a guild. :param bot_id: User ID of the bot. :param bot_token: Token of the bot. @@ -238,21 +210,27 @@ async def update_guild_commands_permissions(bot_id, """ url = f"https://discord.com/api/v8/applications/{bot_id}/guilds/{guild_id}/commands/permissions" async with aiohttp.ClientSession() as session: - async with session.put(url, headers={"Authorization": f"Bot {bot_token}"}, json=cmd_permissions) as resp: + async with session.put( + url, headers={"Authorization": f"Bot {bot_token}"}, json=cmd_permissions + ) as resp: if resp.status == 429: _json = await resp.json() await asyncio.sleep(_json["retry_after"]) - return await update_guild_commands_permissions(bot_id, bot_token, guild_id, cmd_permissions) + return await update_guild_commands_permissions( + bot_id, bot_token, guild_id, cmd_permissions + ) if not 200 <= resp.status < 300: raise RequestFailure(resp.status, await resp.text()) return await resp.json() -def create_option(name: str, - description: str, - option_type: typing.Union[int, type], - required: bool, - choices: list = None) -> dict: +def create_option( + name: str, + description: str, + option_type: typing.Union[int, type], + required: bool, + choices: list = None, +) -> dict: """ Creates option used for creating slash command. @@ -269,25 +247,34 @@ def create_option(name: str, .. note:: ``choices`` must either be a list of `option type dicts `_ - or a list of single string values. + or a list of single string values. """ - if not isinstance(option_type, int) or isinstance(option_type, bool): #Bool values are a subclass of int + if not isinstance(option_type, int) or isinstance( + option_type, bool + ): # Bool values are a subclass of int original_type = option_type option_type = SlashCommandOptionType.from_type(original_type) if option_type is None: - raise IncorrectType(f"The type {original_type} is not recognized as a type that Discord accepts for slash commands.") + raise IncorrectType( + f"The type {original_type} is not recognized as a type that Discord accepts for slash commands." + ) choices = choices or [] - choices = [choice if isinstance(choice, dict) else {"name": choice, "value": choice} for choice in choices] + choices = [ + choice if isinstance(choice, dict) else {"name": choice, "value": choice} + for choice in choices + ] return { "name": name, "description": description, "type": option_type, "required": required, - "choices": choices + "choices": choices, } -def generate_options(function: Callable, description: str = "No description.", connector: dict = None) -> list: +def generate_options( + function: Callable, description: str = "No description.", connector: dict = None +) -> list: """ Generates a list of options from the type hints of a command. You currently can type hint: str, int, bool, discord.User, discord.Channel, discord.Role @@ -301,7 +288,7 @@ def generate_options(function: Callable, description: str = "No description.", c """ options = [] if connector: - connector = {y: x for x, y in connector.items()} # Flip connector. + connector = {y: x for x, y in connector.items()} # Flip connector. params = iter(inspect.signature(function).parameters.values()) if next(params).name in ("self", "cls"): # Skip 1. (+ 2.) parameter, self/cls and ctx @@ -320,9 +307,11 @@ def generate_options(function: Callable, description: str = "No description.", c args = getattr(param.annotation, "__args__", None) if args: param = param.replace(annotation=args[0]) - required = not args[-1] is type(None) + required = not isinstance(args[-1], type(None)) - option_type = SlashCommandOptionType.from_type(param.annotation) or SlashCommandOptionType.STRING + option_type = ( + SlashCommandOptionType.from_type(param.annotation) or SlashCommandOptionType.STRING + ) name = param.name if not connector else connector[param.name] options.append(create_option(name, description or "No Description.", option_type, required)) @@ -337,13 +326,12 @@ def create_choice(value: Union[str, int], name: str): :param name: Name of the choice. :return: dict """ - return { - "value": value, - "name": name - } + return {"value": value, "name": name} -def create_permission(id:int, id_type: typing.Union[int, SlashCommandPermissionType], permission: bool): +def create_permission( + id: int, id_type: typing.Union[int, SlashCommandPermissionType], permission: bool +): """ Create a single command permission. @@ -355,32 +343,36 @@ def create_permission(id:int, id_type: typing.Union[int, SlashCommandPermissionT .. note:: For @everyone permission, set id_type as role and id as guild id. """ - if not (isinstance(id_type, int) or isinstance(id_type, bool)): #Bool values are a subclass of int + if not ( + isinstance(id_type, int) or isinstance(id_type, bool) + ): # Bool values are a subclass of int original_type = id_type id_type = SlashCommandPermissionType.from_type(original_type) if id_type is None: - raise IncorrectType(f"The type {original_type} is not recognized as a type that Discord accepts for slash command permissions.") - return { - "id": id, - "type": id_type, - "permission": permission - } + raise IncorrectType( + f"The type {original_type} is not recognized as a type that Discord accepts for slash command permissions." + ) + return {"id": id, "type": id_type, "permission": permission} -def create_multi_ids_permission(ids: typing.List[int], id_type: typing.Union[int, SlashCommandPermissionType], permission: bool): +def create_multi_ids_permission( + ids: typing.List[int], id_type: typing.Union[int, SlashCommandPermissionType], permission: bool +): """ Creates a list of permissions from list of ids with common id_type and permission state. :param ids: List of target ids to apply the permission on. - :param id_type: Type of the id. + :param id_type: Type of the id. :param permission: State of the permission. ``True`` to allow access, ``False`` to disallow access. """ return [create_permission(id, id_type, permission) for id in set(ids)] def generate_permissions( - allowed_roles: typing.List[int] = None, allowed_users: typing.List[int] = None, - disallowed_roles: typing.List[int] = None, disallowed_users: typing.List[int] = None + allowed_roles: typing.List[int] = None, + allowed_users: typing.List[int] = None, + disallowed_roles: typing.List[int] = None, + disallowed_users: typing.List[int] = None, ): """ Creates a list of permissions. @@ -392,14 +384,22 @@ def generate_permissions( :return: list """ permissions = [] - + if allowed_roles: - permissions.extend(create_multi_ids_permission(allowed_roles, SlashCommandPermissionType.ROLE, True)) + permissions.extend( + create_multi_ids_permission(allowed_roles, SlashCommandPermissionType.ROLE, True) + ) if allowed_users: - permissions.extend(create_multi_ids_permission(allowed_users, SlashCommandPermissionType.USER, True)) + permissions.extend( + create_multi_ids_permission(allowed_users, SlashCommandPermissionType.USER, True) + ) if disallowed_roles: - permissions.extend(create_multi_ids_permission(disallowed_roles, SlashCommandPermissionType.ROLE, False)) + permissions.extend( + create_multi_ids_permission(disallowed_roles, SlashCommandPermissionType.ROLE, False) + ) if disallowed_users: - permissions.extend(create_multi_ids_permission(disallowed_users, SlashCommandPermissionType.USER, False)) + permissions.extend( + create_multi_ids_permission(disallowed_users, SlashCommandPermissionType.USER, False) + ) return permissions diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py index add1a104e..6d64cc9a9 100644 --- a/discord_slash/utils/manage_components.py +++ b/discord_slash/utils/manage_components.py @@ -1,7 +1,9 @@ -import uuid import enum import typing +import uuid + import discord + from ..context import ComponentContext from ..error import IncorrectFormat @@ -21,13 +23,13 @@ def create_actionrow(*components: dict) -> dict: """ if not components or len(components) > 5: raise IncorrectFormat("Number of components in one row should be between 1 and 5.") - if ComponentsType.select in [component["type"] for component in components] and len(components) > 1: + if ( + ComponentsType.select in [component["type"] for component in components] + and len(components) > 1 + ): raise IncorrectFormat("Action row must have only one select component and nothing else") - return { - "type": ComponentsType.actionrow, - "components": components - } + return {"type": ComponentsType.actionrow, "components": components} class ButtonStyle(enum.IntEnum): @@ -59,12 +61,14 @@ def emoji_to_dict(emoji: typing.Union[discord.Emoji, discord.PartialEmoji, str]) return emoji if emoji else {} -def create_button(style: typing.Union[ButtonStyle, int], - label: str = None, - emoji: typing.Union[discord.Emoji, discord.PartialEmoji, str] = None, - custom_id: str = None, - url: str = None, - disabled: bool = False) -> dict: +def create_button( + style: typing.Union[ButtonStyle, int], + label: str = None, + emoji: typing.Union[discord.Emoji, discord.PartialEmoji, str] = None, + custom_id: str = None, + url: str = None, + disabled: bool = False, +) -> dict: """ Creates a button component for use with the ``components`` field. Must be inside an ActionRow to be used (see :meth:`create_actionrow`). @@ -118,7 +122,9 @@ def create_button(style: typing.Union[ButtonStyle, int], return data -def create_select_option(label: str, value: str, emoji=None, description: str = None, default: bool = False): +def create_select_option( + label: str, value: str, emoji=None, description: str = None, default: bool = False +): """ Creates an option for select components. @@ -135,11 +141,13 @@ def create_select_option(label: str, value: str, emoji=None, description: str = "value": value, "description": description, "default": default, - "emoji": emoji + "emoji": emoji, } -def create_select(options: typing.List[dict], custom_id=None, placeholder=None, min_values=None, max_values=None): +def create_select( + options: typing.List[dict], custom_id=None, placeholder=None, min_values=None, max_values=None +): """ Creates a select (dropdown) component for use with the ``components`` field. Must be inside an ActionRow to be used (see :meth:`create_actionrow`). @@ -159,8 +167,9 @@ def create_select(options: typing.List[dict], custom_id=None, placeholder=None, } -async def wait_for_component(client: discord.Client, component: typing.Union[dict, str], check=None, timeout=None) \ - -> ComponentContext: +async def wait_for_component( + client: discord.Client, component: typing.Union[dict, str], check=None, timeout=None +) -> ComponentContext: """ Waits for a component interaction. Only accepts interactions based on the custom ID of the component, and optionally a check function. @@ -172,16 +181,20 @@ async def wait_for_component(client: discord.Client, component: typing.Union[dic :param timeout: The number of seconds to wait before timing out and raising :exc:`asyncio.TimeoutError`. :raises: :exc:`asyncio.TimeoutError` """ + def _check(ctx): if check and not check(ctx): return False - return (component["custom_id"] if isinstance(component, dict) else component) == ctx.custom_id + return ( + component["custom_id"] if isinstance(component, dict) else component + ) == ctx.custom_id return await client.wait_for("component", check=_check, timeout=timeout) -async def wait_for_any_component(client: discord.Client, message: typing.Union[discord.Message, int], - check=None, timeout=None) -> ComponentContext: +async def wait_for_any_component( + client: discord.Client, message: typing.Union[discord.Message, int], check=None, timeout=None +) -> ComponentContext: """ Waits for any component interaction. Only accepts interactions based on the message ID given and optionally a check function. @@ -193,9 +206,12 @@ async def wait_for_any_component(client: discord.Client, message: typing.Union[d :param timeout: The number of seconds to wait before timing out and raising :exc:`asyncio.TimeoutError`. :raises: :exc:`asyncio.TimeoutError` """ + def _check(ctx): if check and not check(ctx): return False - return (message.id if isinstance(message, discord.Message) else message) == ctx.origin_message_id + return ( + message.id if isinstance(message, discord.Message) else message + ) == ctx.origin_message_id return await client.wait_for("component", check=_check, timeout=timeout) diff --git a/docs/images/scope.jpg b/docs/_static/scope.jpg similarity index 100% rename from docs/images/scope.jpg rename to docs/_static/scope.jpg diff --git a/docs/conf.py b/docs/conf.py index 8cb9de81e..1e77fbb77 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,45 +12,43 @@ # import os import sys -import sphinx_rtd_theme -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) -# -- Project information ----------------------------------------------------- +from discord_slash import __version__ -project = 'discord-py-slash-command' -copyright = '2020-2021, eunwoo1104' -author = 'eunwoo1104' +# -- Project information ----------------------------------------------------- +project = "discord-py-slash-command" +copyright = "2020-2021, eunwoo1104" +author = "eunwoo1104" +release = __version__ +version = ".".join(__version__.split(".", 2)[:2]) # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.intersphinx", - "sphinx_rtd_theme" -] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx_rtd_theme"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = 'en' +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', "test.py", "test2.py", "test3.py", ".idea", "setup.py"] +exclude_patterns = ["_build"] # This should fix wrong sort -autodoc_member_order = 'bysource' +autodoc_member_order = "bysource" # -- Options for HTML output ------------------------------------------------- @@ -62,10 +60,10 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Intersphinx intersphinx_mapping = { - 'py': ('https://docs.python.org/3', None), - 'discord': ("https://discordpy.readthedocs.io/en/latest/", None) -} \ No newline at end of file + "py": ("https://docs.python.org/3", None), + "discord": ("https://discordpy.readthedocs.io/en/latest/", None), +} diff --git a/docs/quickstart.rst b/docs/quickstart.rst index b5542d9f9..0e2cf44ce 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -15,7 +15,7 @@ After reading that, there is one more step before inviting your bot. The second step will now be setting your scope correctly for the bot to properly recognize slash commands, as shown here: -.. image:: images/scope.jpg +.. image:: _static/scope.jpg Then, invite your bot to your guild. @@ -47,7 +47,7 @@ slash commands just yet. We can do that by adding this code shown here: Make sure this code is added before the client.run() call! It also needs to be under on_ready, otherwise, this will not work. """ - + guild_ids = [789032594456576001] # Put your server ID in this array. @slash.slash(name="ping", guild_ids=guild_ids) diff --git a/pre_push.py b/pre_push.py new file mode 100755 index 000000000..42f63078e --- /dev/null +++ b/pre_push.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Run static analysis on the project.""" + +import sys +from os import path +from shutil import rmtree +from subprocess import CalledProcessError, check_call +from tempfile import mkdtemp + +current_directory = path.abspath(path.join(__file__, "..")) + + +def do_process(args, shell=False): + """Run program provided by args. + + Return True on success. + + Output failed message on non-zero exit and return False. + + Exit if command is not found. + + """ + print(f"Running: {' '.join(args)}") + try: + check_call(args, shell=shell) + except CalledProcessError: + print(f"\nFailed: {' '.join(args)}") + return False + except Exception as exc: + sys.stderr.write(f"{str(exc)}\n") + sys.exit(1) + return True + + +def run_static(): + """Runs static tests. + + Returns a statuscode of 0 if everything ran correctly. Otherwise, it will return + statuscode 1 + + """ + success = True + # Formatters + success &= do_process(["black", "."]) + success &= do_process(["isort", "."]) + # Linters + success &= do_process(["flake8", "--exclude=.eggs,build,docs,.venv*"]) + + tmp_dir = mkdtemp() + try: + success &= do_process(["sphinx-build", "-W", "--keep-going", "docs", tmp_dir]) + finally: + rmtree(tmp_dir) + + return success + + +def main(): + success = True + try: + success &= run_static() + except KeyboardInterrupt: + return 1 + return int(not success) + + +if __name__ == "__main__": + exit_code = main() + print("\npre_push.py: Success!" if not exit_code else "\npre_push.py: Fail") + sys.exit(exit_code) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..23201eee1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[tool.black] +exclude = '/(\.eggs|\.git|\.mypy_cache|\.venv.*|_build|build|dist)/' +line-length = 100 + +[tool.isort] +profile = "black" +line_length = 100 \ No newline at end of file diff --git a/readthedocs.yml b/readthedocs.yml index 30c52062b..2acdf8945 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -21,4 +21,7 @@ formats: python: version: 3.7 install: - - requirements: requirements.txt \ No newline at end of file + - method: pip + extra_requirements: + - readthedocs + path: . \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 58e12d83b..000000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -discord.py -aiohttp -sphinx -sphinx-rtd-theme \ No newline at end of file diff --git a/setup.py b/setup.py index 1cdbde936..8a215962d 100644 --- a/setup.py +++ b/setup.py @@ -1,20 +1,52 @@ -import setuptools +import re +from codecs import open +from os import path + +from setuptools import find_packages, setup + +PACKAGE_NAME = "discord_slash" +HERE = path.abspath(path.dirname(__file__)) with open("README.md", "r", encoding="UTF-8") as f: - long_description = f.read() + README = f.read() +with open(path.join(HERE, PACKAGE_NAME, "const.py"), encoding="utf-8") as fp: + VERSION = re.search('__version__ = "([^"]+)"', fp.read()).group(1) + +extras = { + "lint": ["black", "flake8", "isort"], + "readthedocs": ["sphinx", "sphinx-rtd-theme"], +} +extras["lint"] += extras["readthedocs"] +extras["dev"] = extras["lint"] + extras["readthedocs"] -setuptools.setup( +setup( name="discord-py-slash-command", - version="1.2.2", + version=VERSION, author="eunwoo1104", author_email="sions04@naver.com", description="A simple discord slash command handler for discord.py.", - long_description=long_description, + extras_require=extras, + install_requires=["discord.py", "aiohttp"], + license="MIT License", + long_description=README, long_description_content_type="text/markdown", url="https://github.com/eunwoo1104/discord-py-slash-command", - packages=setuptools.find_packages(), - python_requires='>=3.6', + packages=find_packages(), + python_requires=">=3.6", classifiers=[ - "Programming Language :: Python :: 3" - ] + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Topic :: Internet", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Libraries", + "Topic :: Utilities", + ], )