Skip to content
This repository was archived by the owner on Feb 3, 2025. It is now read-only.

Commit b908e31

Browse files
committed
Add enum shortcut to option with choices
discord/ext/slash/__init__.py: + ChoiceEnum is the base class for the enum * Options constructed from enums reference the enum in a private attr * This required modifying Option.clone() * Context converts values to enum values if the _enum is present * Option(description) can now take a ChoiceEnum as the description * This sets the description to the docstring, the type to STRING, and the choices to the enum names and value * Parameters annotated with ChoiceEnum subclasses are treated as if annotated with Option(ChoiceEnum) demo_bot.py: * /math -> /numbers + /numbers now supports choosing the operator, using a ChoiceEnum docs/discord-ext-slash.rst: + ChoiceEnum docs
1 parent 25eed7c commit b908e31

File tree

3 files changed

+61
-10
lines changed

3 files changed

+61
-10
lines changed

demo_bot.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,16 +115,35 @@ async def names(
115115
emb.add_field(name='Text Channel Name', value=text_channel.name)
116116
await ctx.respond(embed=emb, ephemeral=True)
117117

118-
@client.slash_cmd(name='math')
118+
class ArithmeticOperator(slash.ChoiceEnum):
119+
# The docstring is used as the description of the
120+
# option - it is required
121+
"""The operation to perform on the numbers."""
122+
# Only string options are supported by ChoiceEnum
123+
ADDITION = '+'
124+
SUBTRACTION = '-'
125+
MULTIPLICATION = '\N{MULTIPLICATION SIGN}'
126+
DIVISION = '\N{DIVISION SIGN}'
127+
128+
@client.slash_cmd()
119129
async def numbers(
120130
ctx: slash.Context,
121131
num1: slash.Option(description='The first number',
122132
min_value=0),
133+
operator: ArithmeticOperator, # see above
123134
num2: slash.Option(description='The second number',
124135
min_value=-4.20, max_value=6.9),
125136
):
126137
"""Do some math! (With limitations)"""
127-
await ctx.respond(num1 + num2, ephemeral=True)
138+
if operator == ArithmeticOperator.ADDITION:
139+
value = num1 + num2
140+
elif operator == ArithmeticOperator.SUBTRACTION:
141+
value = num1 - num2
142+
elif operator == ArithmeticOperator.MULTIPLICATION:
143+
value = num1 * num2
144+
elif operator == ArithmeticOperator.DIVISION:
145+
value = num1 / num2
146+
await ctx.respond(value, ephemeral=True)
128147

129148
@client.slash_cmd(default_permission=False)
130149
async def stop(ctx: slash.Context):

discord/ext/slash/__init__.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@ async def repeat( # command name
5454
from __future__ import annotations
5555
import sys
5656
from warnings import warn
57-
from enum import IntEnum, IntFlag
58-
from typing import Coroutine, Dict, Iterable, Set, Tuple, Union, Optional, Mapping, Any, List
57+
from enum import Enum, IntEnum, IntFlag
58+
from typing import (
59+
Coroutine, Dict, Iterable, Set, Tuple,
60+
Type, Union, Optional, Mapping, Any, List)
5961
from functools import partial
6062
from inspect import signature
6163
import logging
@@ -69,6 +71,7 @@ async def repeat( # command name
6971
'ApplicationCommandPermissionType',
7072
'InteractionResponseType',
7173
'CallbackFlags',
74+
'ChoiceEnum',
7275
'Context',
7376
'Interaction',
7477
'Option',
@@ -186,6 +189,13 @@ class CallbackFlags(IntFlag):
186189

187190
MessageFlags = CallbackFlags
188191

192+
class ChoiceEnum(Enum):
193+
"""Callback parameters annotated with subclasses of this class
194+
will use the enums as choices. See the ``/numbers`` command in the
195+
demo bot for an example.
196+
"""
197+
pass
198+
189199
class _Route(discord.http.Route):
190200
BASE = 'https://discord.com/api/v8'
191201

@@ -347,6 +357,9 @@ async def _kwargs_from_options(self, options, resolved):
347357
else:
348358
raise commands.CommandInvokeError(
349359
f'No such option: {opt["name"]!r}')
360+
_enum = self.command.options[opt['name']]._enum
361+
if _enum is not None:
362+
value = _enum.__members__[value]
350363
opttype = self.command.options[opt['name']].type
351364
try:
352365
opttype = ApplicationCommandOptionType(opttype)
@@ -628,6 +641,10 @@ class Option:
628641
Constructor arguments map directly to attributes, besides the ones below
629642
which have different type signatures:
630643
644+
:param description:
645+
Annotating a parameter with ``EnumClass`` has the same effect as
646+
with ``Option(description=EnumClass)``.
647+
:type description: Union[str, Type[ChoiceEnum]]
631648
:param choices:
632649
Strings are converted into :class:`Choice` objects with the same
633650
``name`` and ``value``. :class:`dict` objects are passed as kwargs to
@@ -692,14 +709,16 @@ class Option:
692709
min_value: Union[int, float, None] = None
693710
max_value: Union[int, float, None] = None
694711

712+
_enum: Optional[Type[ChoiceEnum]] = None
713+
695714
@staticmethod
696715
def value_to_enum(value: Union[int, discord.ChannelType]):
697716
if isinstance(value, discord.ChannelType):
698717
return value
699718
return discord.ChannelType(value)
700719

701720
def __init__(
702-
self, description: str,
721+
self, description: Union[str, Type[ChoiceEnum]],
703722
type: ApplicationCommandOptionType = ApplicationCommandOptionType.STRING,
704723
**kwargs
705724
):
@@ -732,7 +751,15 @@ def __init__(
732751
self.type = ApplicationCommandOptionType.INTEGER
733752
elif isinstance(self.min_value, float):
734753
self.type = ApplicationCommandOptionType.NUMBER
735-
self.description = description
754+
if isinstance(description, str):
755+
self.description = description
756+
elif issubclass(description, ChoiceEnum):
757+
kwargs['choices'] = [
758+
Choice(desc.value, attr)
759+
for attr, desc in description.__members__.items()]
760+
self._enum = description
761+
self.description = description.__doc__
762+
self.type = ApplicationCommandOptionType.STRING
736763
self.required = kwargs.pop('required', False)
737764
choices = kwargs.pop('choices', None)
738765
if choices is not None:
@@ -764,7 +791,10 @@ def to_dict(self):
764791
return data
765792

766793
def clone(self):
767-
return type(self)(**self.to_dict())
794+
value = type(self)(**self.to_dict())
795+
if self._enum is not None:
796+
value._enum = self._enum
797+
return value
768798

769799
class Choice:
770800
"""Represents one choice for an option value.
@@ -920,9 +950,8 @@ def __init__(self, coro: Coroutine, **kwargs):
920950
except:
921951
typ = param.empty
922952
if (
923-
not (isinstance(typ, Option) or (
924-
isinstance(typ, type) and issubclass(typ, Context)
925-
))
953+
not (isinstance(typ, Option) or (isinstance(typ, type) and (
954+
issubclass(typ, ChoiceEnum) or issubclass(typ, Context))))
926955
and param.default is param.empty
927956
):
928957
if not found_self_arg:
@@ -937,6 +966,8 @@ def __init__(self, coro: Coroutine, **kwargs):
937966
try:
938967
if issubclass(typ, Context):
939968
self._ctx_arg = param.name
969+
elif issubclass(typ, ChoiceEnum):
970+
typ = Option(description=typ)
940971
except TypeError: # not even a class
941972
pass
942973
if isinstance(typ, Option):

docs/discord-ext-slash.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Enums
6969
.. autoclass:: InteractionResponseType
7070
.. autoclass:: CallbackFlags
7171
.. autoclass:: MessageFlags
72+
.. autoclass:: ChoiceEnum
7273

7374
Events
7475
------

0 commit comments

Comments
 (0)