Skip to content

Commit 89e349f

Browse files
authored
feat!: smarter option decorator (#950)
* feat: smarter option decorators * feat!: optimize option decorator BREAKING: breaks previous implementation
1 parent b0001f7 commit 89e349f

File tree

3 files changed

+60
-52
lines changed

3 files changed

+60
-52
lines changed

docs/migration.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,9 @@ your option name! Example:
220220
bot = interactions.Client("TOKEN", default_scope=1234567890)
221221
222222
@bot.command(default_scope=False)
223-
@interactions.option(str, name="opt1", required=True) # description is optional.
224-
@interactions.option(4, name="opt2", description="This is an option.", converter="hi", required=True)
225-
@interactions.option(interactions.Channel, name="opt3")
223+
@interactions.option() # type and name default to ones in the parameter.
224+
@interactions.option(name="opt2", description="This is an option.", converter="hi")
225+
@interactions.option() # same kwargs as Option
226226
async def command_with_options(
227227
ctx, opt1: str, hi: int, opt3: interactions.Channel = None
228228
):

docs/quickstart.rst

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -200,15 +200,16 @@ The :ref:`@option() <models.command:Application Command Models>` decorator creat
200200
bot = interactions.Client(token="your_secret_bot_token")
201201
202202
@bot.command(scope=the_id_of_your_guild)
203-
@interactions.option(str, name="text", description="What you want to say", required=True)
203+
@interactions.option()
204204
async def say_something(ctx: interactions.CommandContext, text: str):
205205
"""say something!"""
206206
await ctx.send(f"You said '{text}'!")
207207
208-
* 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``.
209-
* All other arguments in the decorator are keyword arguments only.
210-
* The ``name`` field is required.
208+
* All arguments in the decorator are keyword arguments only.
209+
* The ``type`` and ``name`` fields default to the typehint and the name of the parameter.
211210
* The ``description`` field is optional and defaults to ``"No description set``.
211+
* The ``required`` field defaults to whether the default for the parameter is empty.
212+
* 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``.
212213
* Any parameters from ``Option`` can be passed into the ``@option()`` decorator.
213214

214215
.. note::
@@ -258,9 +259,9 @@ Here is the structure of a subcommand:
258259
)
259260
async def cmd(ctx: interactions.CommandContext, sub_command: str, second_option: str = "", option: int = None):
260261
if sub_command == "command_name":
261-
await ctx.send(f"You selected the command_name sub command and put in {option}")
262+
await ctx.send(f"You selected the command_name sub command and put in {option}")
262263
elif sub_command == "second_command":
263-
await ctx.send(f"You selected the second_command sub command and put in {second_option}")
264+
await ctx.send(f"You selected the second_command sub command and put in {second_option}")
264265
265266
You can also create subcommands using the command system:
266267

@@ -276,13 +277,13 @@ You can also create subcommands using the command system:
276277
pass
277278
278279
@base_command.subcommand()
279-
@interactions.option(str, name="option", description="A descriptive description", required=False)
280+
@interactions.option(description="A descriptive description")
280281
async def command_name(ctx: interactions.CommandContext, option: int = None):
281282
"""A descriptive description"""
282283
await ctx.send(f"You selected the command_name sub command and put in {option}")
283284
284285
@base_command.subcommand()
285-
@interactions.option(str, name="second_option", description="A descriptive description", required=True)
286+
@interactions.option(description="A descriptive description")
286287
async def second_command(ctx: interactions.CommandContext, second_option: str):
287288
"""A descriptive description"""
288289
await ctx.send(f"You selected the second_command sub command and put in {second_option}")

interactions/client/models/command.py

Lines changed: 48 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from ...api.models.guild import Guild
1010
from ...api.models.member import Member
1111
from ...api.models.message import Attachment
12-
from ...api.models.misc import File, Image, Snowflake
12+
from ...api.models.misc import Snowflake
1313
from ...api.models.role import Role
1414
from ...api.models.user import User
1515
from ..enums import ApplicationCommandType, Locale, OptionType, PermissionType
@@ -215,66 +215,73 @@ class ApplicationCommand(DictSerializerMixin):
215215

216216

217217
def option(
218-
option_type: OptionType,
218+
description: str = "No description set",
219219
/,
220-
name: str,
221-
description: Optional[str] = "No description set",
222220
**kwargs,
223221
) -> Callable[[Callable[..., Awaitable]], Callable[..., Awaitable]]:
224222
r"""
225223
A decorator for adding options to a command.
226224
225+
The ``type`` and ``name`` of the option are defaulted to the parameter's typehint and name.
226+
227+
When the ``name`` of the option differs from the parameter name,
228+
the ``converter`` field will default to the name of the parameter.
229+
227230
The structure of an option:
228231
229232
.. code-block:: python
230233
231234
@client.command()
232-
@interactions.option(str, name="opt", ...)
235+
@interactions.option("description (optional)") # kwargs are optional, same as Option
233236
async def my_command(ctx, opt: str):
234237
...
235238
236-
:param option_type: The type of the option.
237-
:type option_type: OptionType
238-
:param name: The name of the option.
239-
:type name: str
240-
:param description?: The description of the option. Defaults to ``"No description set"``.
239+
:param description?: The description of the option. Defaults to "No description set".
241240
:type description?: str
242-
:param \**kwargs: The keyword arguments of the option, same as :class:`Option`.
243-
:type \**kwargs: dict
241+
:param \**kwargs?: The keyword arguments of the option, same as :class:`Option`.
242+
:type \**kwargs?: dict
244243
"""
245-
if option_type in (str, int, float, bool):
246-
if option_type is str:
247-
option_type = OptionType.STRING
248-
elif option_type is int:
249-
option_type = OptionType.INTEGER
250-
elif option_type is float:
251-
option_type = OptionType.NUMBER
252-
elif option_type is bool:
253-
option_type = OptionType.BOOLEAN
254-
elif option_type in (Member, User):
255-
option_type = OptionType.USER
256-
elif option_type is Channel:
257-
option_type = OptionType.CHANNEL
258-
elif option_type is Role:
259-
option_type = OptionType.ROLE
260-
elif option_type in (Attachment, File, Image):
261-
option_type = OptionType.ATTACHMENT
262-
263-
option: Option = Option(
264-
type=option_type,
265-
name=name,
266-
description=description,
267-
**kwargs,
268-
)
269244

270245
def decorator(coro: Callable[..., Awaitable]) -> Callable[..., Awaitable]:
271-
nonlocal option
246+
parameters = list(signature(coro).parameters.values())
247+
248+
if not hasattr(coro, "_options") or not isinstance(coro._options, list):
249+
coro._options = []
250+
251+
param = parameters[-1 - len(coro._options)]
272252

273-
if hasattr(coro, "_options") and isinstance(coro._options, list):
274-
coro._options.insert(0, option)
275-
else:
276-
coro._options = [option]
253+
option_type = kwargs.get("type", param.annotation)
254+
name = kwargs.pop("name", param.name)
255+
if name != param.name:
256+
kwargs["converter"] = param.name
277257

258+
if option_type is param.empty:
259+
raise LibraryException(
260+
code=12,
261+
message=f"No type specified for option '{name}'.",
262+
)
263+
264+
option_types = {
265+
str: OptionType.STRING,
266+
int: OptionType.INTEGER,
267+
bool: OptionType.BOOLEAN,
268+
User: OptionType.USER,
269+
Member: OptionType.USER,
270+
Channel: OptionType.CHANNEL,
271+
Role: OptionType.ROLE,
272+
float: OptionType.NUMBER,
273+
Attachment: OptionType.ATTACHMENT,
274+
}
275+
option_type = option_types.get(option_type, option_type)
276+
277+
_option = Option(
278+
type=option_type,
279+
name=name,
280+
description=kwargs.pop("description", description),
281+
required=kwargs.pop("required", param.default is param.empty),
282+
**kwargs,
283+
)
284+
coro._options.insert(0, _option)
278285
return coro
279286

280287
return decorator

0 commit comments

Comments
 (0)