From 63fe4ebbf0376f0cebfbe54920e16c5621f25f60 Mon Sep 17 00:00:00 2001 From: Toricane Date: Wed, 20 Jul 2022 22:12:59 -0700 Subject: [PATCH 1/2] feat: smarter option decorators --- docs/migration.rst | 6 +- docs/quickstart.rst | 17 ++--- interactions/client/models/command.py | 92 +++++++++++++++------------ 3 files changed, 62 insertions(+), 53 deletions(-) diff --git a/docs/migration.rst b/docs/migration.rst index 0f285f43c..63215106f 100644 --- a/docs/migration.rst +++ b/docs/migration.rst @@ -220,9 +220,9 @@ your option name! Example: bot = interactions.Client("TOKEN", default_scope=1234567890) @bot.command(default_scope=False) - @interactions.option(str, name="opt1", required=True) # description is optional. - @interactions.option(4, name="opt2", description="This is an option.", converter="hi", required=True) - @interactions.option(interactions.Channel, name="opt3") + @interactions.option() # type and name default to ones in the parameter. + @interactions.option(name="opt2", description="This is an option.", converter="hi") + @interactions.option() # same kwargs as Option async def command_with_options( ctx, opt1: str, hi: int, opt3: interactions.Channel = None ): diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 267f49124..240700507 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -200,15 +200,16 @@ The :ref:`@option() ` decorator creat bot = interactions.Client(token="your_secret_bot_token") @bot.command(scope=the_id_of_your_guild) - @interactions.option(str, name="text", description="What you want to say", required=True) + @interactions.option() async def say_something(ctx: interactions.CommandContext, text: str): """say something!""" await ctx.send(f"You said '{text}'!") -* The first field in the ``@option()`` decorator is the type of the option. This is positional only and required. You can use integers, the default Python types, the ``OptionType`` enum, or supported objects such as ``interactions.Channel``. -* All other arguments in the decorator are keyword arguments only. -* The ``name`` field is required. +* All arguments in the decorator are keyword arguments only. +* The ``type`` and ``name`` fields default to the typehint and the name of the parameter. * The ``description`` field is optional and defaults to ``"No description set``. +* The ``required`` field defaults to whether the default for the parameter is empty. +* For typehinting or inputting the ``type`` field, you can use integers, the default Python types, the ``OptionType`` enum, or supported objects such as ``interactions.Channel``. * Any parameters from ``Option`` can be passed into the ``@option()`` decorator. .. note:: @@ -258,9 +259,9 @@ Here is the structure of a subcommand: ) async def cmd(ctx: interactions.CommandContext, sub_command: str, second_option: str = "", option: int = None): if sub_command == "command_name": - await ctx.send(f"You selected the command_name sub command and put in {option}") + await ctx.send(f"You selected the command_name sub command and put in {option}") elif sub_command == "second_command": - await ctx.send(f"You selected the second_command sub command and put in {second_option}") + await ctx.send(f"You selected the second_command sub command and put in {second_option}") You can also create subcommands using the command system: @@ -276,13 +277,13 @@ You can also create subcommands using the command system: pass @base_command.subcommand() - @interactions.option(str, name="option", description="A descriptive description", required=False) + @interactions.option(description="A descriptive description") async def command_name(ctx: interactions.CommandContext, option: int = None): """A descriptive description""" await ctx.send(f"You selected the command_name sub command and put in {option}") @base_command.subcommand() - @interactions.option(str, name="second_option", description="A descriptive description", required=True) + @interactions.option(description="A descriptive description") async def second_command(ctx: interactions.CommandContext, second_option: str): """A descriptive description""" await ctx.send(f"You selected the second_command sub command and put in {second_option}") diff --git a/interactions/client/models/command.py b/interactions/client/models/command.py index 7614b0f3e..dfd1eae1f 100644 --- a/interactions/client/models/command.py +++ b/interactions/client/models/command.py @@ -210,68 +210,76 @@ class ApplicationCommand(DictSerializerMixin): def option( - option_type: OptionType, + _coro: Callable[..., Awaitable] = MISSING, /, - name: str, - description: Optional[str] = "No description set", **kwargs, -) -> Callable[[Callable[..., Awaitable]], Callable[..., Awaitable]]: +) -> Union[ + Callable[..., Awaitable], Callable[[Callable[..., Awaitable]], Callable[..., Awaitable]] +]: r""" A decorator for adding options to a command. + The type and name of the option are defaulted to the parameter's typehint and name. + The structure of an option: .. code-block:: python @client.command() - @interactions.option(str, name="opt", ...) + @interactions.option(...) # kwargs are optional, same as Option async def my_command(ctx, opt: str): ... - :param option_type: The type of the option. - :type option_type: OptionType - :param name: The name of the option. - :type name: str - :param description?: The description of the option. Defaults to ``"No description set"``. - :type description?: str - :param \**kwargs: The keyword arguments of the option, same as :class:`Option`. - :type \**kwargs: dict + :param _coro?: The coroutine to be wrapped. Do not input this unless you know what you're doing. + :type _coro?: Callable[..., Awaitable] + :param \**kwargs?: The keyword arguments of the option, same as :class:`Option`. + :type \**kwargs?: dict """ - if option_type in (str, int, float, bool): - if option_type is str: - option_type = OptionType.STRING - elif option_type is int: - option_type = OptionType.INTEGER - elif option_type is float: - option_type = OptionType.NUMBER - elif option_type is bool: - option_type = OptionType.BOOLEAN - elif option_type in (Member, User): - option_type = OptionType.USER - elif option_type is Channel: - option_type = OptionType.CHANNEL - elif option_type is Role: - option_type = OptionType.ROLE - elif option_type in (Attachment, File, Image): - option_type = OptionType.ATTACHMENT - - option: Option = Option( - type=option_type, - name=name, - description=description, - **kwargs, - ) def decorator(coro: Callable[..., Awaitable]) -> Callable[..., Awaitable]: - nonlocal option + parameters = list(signature(coro).parameters.values()) + + if not hasattr(coro, "_options") or not isinstance(coro._options, list): + coro._options = [] - if hasattr(coro, "_options") and isinstance(coro._options, list): - coro._options.insert(0, option) - else: - coro._options = [option] + param = parameters[-1 - len(coro._options)] + option_type = kwargs.get("type", param.annotation) + if option_type is param.empty: + raise LibraryException( + code=12, + message=f"No type specified for option {kwargs.get('name', param.name)}.", + ) + if option_type in (str, int, float, bool): + if option_type is str: + option_type = OptionType.STRING + elif option_type is int: + option_type = OptionType.INTEGER + elif option_type is float: + option_type = OptionType.NUMBER + elif option_type is bool: + option_type = OptionType.BOOLEAN + elif option_type in (Member, User): + option_type = OptionType.USER + elif option_type is Channel: + option_type = OptionType.CHANNEL + elif option_type is Role: + option_type = OptionType.ROLE + elif option_type in (Attachment, File, Image): + option_type = OptionType.ATTACHMENT + + _option = Option( + type=option_type, + name=kwargs.get("name", param.name), + description=kwargs.get("description", "No description set"), + required=kwargs.get("required", param.default is param.empty), + **kwargs, + ) + coro._options.insert(0, _option) return coro + if _coro is not MISSING: + return decorator(_coro) return decorator From a96154333f3189cb131e311a5a7ff448069a21e0 Mon Sep 17 00:00:00 2001 From: Toricane Date: Thu, 21 Jul 2022 16:49:13 -0700 Subject: [PATCH 2/2] feat!: optimize option decorator BREAKING: breaks previous implementation --- interactions/client/models/command.py | 63 +++++++++++++-------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/interactions/client/models/command.py b/interactions/client/models/command.py index dfd1eae1f..7f583b715 100644 --- a/interactions/client/models/command.py +++ b/interactions/client/models/command.py @@ -9,7 +9,7 @@ from ...api.models.guild import Guild from ...api.models.member import Member from ...api.models.message import Attachment -from ...api.models.misc import File, Image, Snowflake +from ...api.models.misc import Snowflake from ...api.models.role import Role from ...api.models.user import User from ..enums import ApplicationCommandType, Locale, OptionType, PermissionType @@ -210,28 +210,29 @@ class ApplicationCommand(DictSerializerMixin): def option( - _coro: Callable[..., Awaitable] = MISSING, + description: str = "No description set", /, **kwargs, -) -> Union[ - Callable[..., Awaitable], Callable[[Callable[..., Awaitable]], Callable[..., Awaitable]] -]: +) -> Callable[[Callable[..., Awaitable]], Callable[..., Awaitable]]: r""" A decorator for adding options to a command. - The type and name of the option are defaulted to the parameter's typehint and name. + The ``type`` and ``name`` of the option are defaulted to the parameter's typehint and name. + + When the ``name`` of the option differs from the parameter name, + the ``converter`` field will default to the name of the parameter. The structure of an option: .. code-block:: python @client.command() - @interactions.option(...) # kwargs are optional, same as Option + @interactions.option("description (optional)") # kwargs are optional, same as Option async def my_command(ctx, opt: str): ... - :param _coro?: The coroutine to be wrapped. Do not input this unless you know what you're doing. - :type _coro?: Callable[..., Awaitable] + :param description?: The description of the option. Defaults to "No description set". + :type description?: str :param \**kwargs?: The keyword arguments of the option, same as :class:`Option`. :type \**kwargs?: dict """ @@ -245,41 +246,39 @@ def decorator(coro: Callable[..., Awaitable]) -> Callable[..., Awaitable]: param = parameters[-1 - len(coro._options)] option_type = kwargs.get("type", param.annotation) + name = kwargs.pop("name", param.name) + if name != param.name: + kwargs["converter"] = param.name + if option_type is param.empty: raise LibraryException( code=12, - message=f"No type specified for option {kwargs.get('name', param.name)}.", + message=f"No type specified for option '{name}'.", ) - if option_type in (str, int, float, bool): - if option_type is str: - option_type = OptionType.STRING - elif option_type is int: - option_type = OptionType.INTEGER - elif option_type is float: - option_type = OptionType.NUMBER - elif option_type is bool: - option_type = OptionType.BOOLEAN - elif option_type in (Member, User): - option_type = OptionType.USER - elif option_type is Channel: - option_type = OptionType.CHANNEL - elif option_type is Role: - option_type = OptionType.ROLE - elif option_type in (Attachment, File, Image): - option_type = OptionType.ATTACHMENT + + option_types = { + str: OptionType.STRING, + int: OptionType.INTEGER, + bool: OptionType.BOOLEAN, + User: OptionType.USER, + Member: OptionType.USER, + Channel: OptionType.CHANNEL, + Role: OptionType.ROLE, + float: OptionType.NUMBER, + Attachment: OptionType.ATTACHMENT, + } + option_type = option_types.get(option_type, option_type) _option = Option( type=option_type, - name=kwargs.get("name", param.name), - description=kwargs.get("description", "No description set"), - required=kwargs.get("required", param.default is param.empty), + name=name, + description=kwargs.pop("description", description), + required=kwargs.pop("required", param.default is param.empty), **kwargs, ) coro._options.insert(0, _option) return coro - if _coro is not MISSING: - return decorator(_coro) return decorator