Skip to content

Commit 45ca471

Browse files
authored
Updatet main to 1.5.1
1 parent e42c759 commit 45ca471

File tree

9 files changed

+283
-126
lines changed

9 files changed

+283
-126
lines changed

discord/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
from . import utils, opus, abc
5252
from .enums import *
5353
from .embeds import Embed
54-
from .components import Button, SelectionMenu, ActionRow, ButtonColor, ButtonStyle, select_option
54+
from .components import Button, SelectMenu, ActionRow, ButtonColor, ButtonStyle, select_option
5555
from .mentions import AllowedMentions
5656
from .shard import AutoShardedClient, ShardInfo
5757
from .player import *

discord/abc.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,10 @@ def __init__(self, **kwargs):
175175
self.id = kwargs.pop('id')
176176
self.allow = int(kwargs.pop('allow_new', 0))
177177
self.deny = int(kwargs.pop('deny_new', 0))
178-
type = kwargs.pop('type')
179-
if type not in ('role', 'user', 'member'):
180-
type = {0: 'role', 1: 'member'}.get(type)
181-
self.type = sys.intern(type)
178+
_type = kwargs.pop('type')
179+
if _type not in ('role', 'user', 'member'):
180+
_type = {0: 'role', 1: 'member'}.get(_type)
181+
self.type = sys.intern(_type)
182182

183183
def _asdict(self):
184184
return {

discord/client.py

Lines changed: 135 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -362,30 +362,36 @@ def dispatch(self, event, *args, **kwargs):
362362
if listeners:
363363
removed = []
364364
for i, (future, condition) in enumerate(listeners):
365-
if future.cancelled():
366-
removed.append(i)
367-
continue
365+
if isinstance(future, asyncio.Future):
366+
if future.cancelled():
367+
removed.append(i)
368+
continue
368369

369-
try:
370-
result = condition(*args)
371-
except Exception as exc:
372-
future.set_exception(exc)
373-
removed.append(i)
370+
try:
371+
result = condition(*args)
372+
except Exception as exc:
373+
future.set_exception(exc)
374+
removed.append(i)
375+
else:
376+
if result:
377+
if len(args) == 0:
378+
future.set_result(None)
379+
elif len(args) == 1:
380+
future.set_result(args[0])
381+
else:
382+
future.set_result(args)
383+
removed.append(i)
384+
385+
if len(removed) == len(listeners):
386+
self._listeners.pop(event)
387+
else:
388+
for idx in reversed(removed):
389+
del listeners[idx]
374390
else:
391+
result = condition(*args)
375392
if result:
376-
if len(args) == 0:
377-
future.set_result(None)
378-
elif len(args) == 1:
379-
future.set_result(args[0])
380-
else:
381-
future.set_result(args)
382-
removed.append(i)
393+
self._schedule_event(future, method, *args, **kwargs)
383394

384-
if len(removed) == len(listeners):
385-
self._listeners.pop(event)
386-
else:
387-
for idx in reversed(removed):
388-
del listeners[idx]
389395

390396
try:
391397
coro = getattr(self, method)
@@ -1016,6 +1022,115 @@ async def on_ready():
10161022
setattr(self, coro.__name__, coro)
10171023
log.debug('%s has successfully been registered as an event', coro.__name__)
10181024
return coro
1025+
1026+
def on_click(self, custom_id=None):
1027+
"""A decorator that registers a raw_button_click event that checks on execution if the ``custom_id's`` are the same; if so, the :func:`func` is called..
1028+
1029+
You can find more info about this in the `documentation <https://discordpy-message-components.readthedocs.io/en/latest/additions.html#on-click>`.
1030+
1031+
The func must be a :ref:`coroutine <coroutine>`, if not, :exc:`TypeError` is raised.
1032+
1033+
Example
1034+
-------
1035+
1036+
.. code-block:: python
1037+
1038+
# the Button
1039+
Button(label='Hey im a cool blue Button',
1040+
custom_id='cool blue Button',
1041+
style=ButtonColor.blurple)
1042+
1043+
# function thats called when the Button pressed
1044+
@client.on_click(custom_id='cool blue Button')
1045+
async def cool_blue_button(i: discord.Interaction):
1046+
await i.respond('Hey you pressed a `cool blue Button`!', hidden=True)
1047+
1048+
Raises
1049+
------
1050+
:class:`TypeError`
1051+
The coroutine passed is not actually a coroutine.
1052+
"""
1053+
def decorator(func):
1054+
if not asyncio.iscoroutinefunction(func):
1055+
raise TypeError('event registered must be a coroutine function')
1056+
1057+
_name = custom_id if custom_id is not None else func.__name__
1058+
1059+
def check(i):
1060+
return i.component.custom_id == str(_name)
1061+
1062+
try:
1063+
listeners = self._listeners['raw_button_click']
1064+
except KeyError:
1065+
listeners = []
1066+
self._listeners['raw_button_click'] = listeners
1067+
1068+
listeners.append((func, check))
1069+
return func
1070+
1071+
return decorator
1072+
1073+
def on_select(self, custom_id=None):
1074+
"""A decorator with which you can assign a function to a specific :class:`SelectMenu` (or its custom_id).
1075+
1076+
.. note::
1077+
This will always give exactly one Parameter of type `discord.Interaction <./interaction.html#discord-interaction>`_ like an `raw_selection_select-Event <#on-raw-button-click>`_.
1078+
1079+
.. important::
1080+
The Function this decorator attached to must be an corountine (means an awaitable)
1081+
1082+
Parameters
1083+
----------
1084+
1085+
:attr:`custom_id`: Optional[str]
1086+
1087+
If the :attr:`custom_id` of the SelectMenu could not use as an function name or you want to give the function a diferent name then the custom_id use this one to set the custom_id.
1088+
1089+
1090+
Example
1091+
-------
1092+
1093+
.. code-block:: python
1094+
1095+
# the SelectMenu
1096+
SelectMenu(custom_id='choose_your_gender',
1097+
options=[
1098+
select_option(label='Female', value='Female', emoji='♀️'),
1099+
select_option(label='Male', value='Male', emoji='♂️'),
1100+
select_option(label='Non Binary', value='Non Binary', emoji='⚧')
1101+
], placeholder='Choose your Gender')
1102+
1103+
# function thats called when the SelectMenu is used
1104+
@client.on_select()
1105+
async def choose_your_gender(i: discord.Interaction):
1106+
await i.respond(f'You selected `{i.component.values[0]}`!', hidden=True)
1107+
1108+
Raises
1109+
--------
1110+
TypeError
1111+
The coroutine passed is not actually a coroutine.
1112+
"""
1113+
def decorator(func):
1114+
if not asyncio.iscoroutinefunction(func):
1115+
raise TypeError('event registered must be a coroutine function')
1116+
1117+
_name = custom_id if custom_id is not None else func.__name__
1118+
1119+
def check(i):
1120+
return i.component.custom_id == str(_name)
1121+
1122+
try:
1123+
listeners = self._listeners['raw_selection_select']
1124+
except KeyError:
1125+
listeners = []
1126+
self._listeners['raw_selection_select'] = listeners
1127+
1128+
listeners.append((func, check))
1129+
return func
1130+
1131+
return decorator
1132+
1133+
10191134

10201135
async def change_presence(self, *, activity=None, status=None, afk=False):
10211136
"""|coro|

discord/components.py

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from .errors import InvalidArgument, InvalidButtonUrl, URLAndCustomIDNotAlowed, InvalidData, EmptyActionRow
3535
URL_REGEX = r"(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'\".,<>?«»“”‘’]))"
3636

37-
__all__ = ('ButtonStyle', 'ButtonColor', 'Button', 'SelectionMenu', 'ActionRow', 'select_option')
37+
__all__ = ('ButtonStyle', 'ButtonColor', 'Button', 'SelectMenu', 'ActionRow', 'select_option')
3838

3939

4040
class ButtonStyle:
@@ -47,9 +47,8 @@ class ButtonStyle:
4747
For more information about the Button-Styles visit the `Discord-API Documentation <https://discord.com/developers/docs/interactions/message-components#buttons-button-styles>`_.
4848
4949
"""
50-
51-
def __repr__(self):
52-
return f'<ButtonStyle {" ".join(k+"="+str(v) for k,v in ButtonStyle.__dict__.items())}>'
50+
def __repr__(self) -> str:
51+
return f'<ButtonStyle {" ".join(k+"="+str(v) for k,v in self.__dict__.items())}>'
5352

5453
Primary = 1
5554
Secondary = 2
@@ -74,11 +73,12 @@ class ButtonColor:
7473
green = ButtonStyle.Success
7574
red = ButtonStyle.Danger
7675
grey_url = ButtonStyle.Link_Button
77-
78-
def __repr__():
79-
return f'<ButtonColor {" ".join(k+"="+str(v) for k, v in ButtonColor.__dict__.items())}>'
76+
77+
def __repr__(self) -> str:
78+
return f'<ButtonColor {" ".join(k+"="+str(v) for k, v in self.__dict__.items())}>'
8079

8180
class Button:
81+
8282
"""
8383
:class:`Button`
8484
@@ -237,7 +237,7 @@ def from_dict(cls, data: dict):
237237
return cls(style=style, label=label, emoji=emoji, custom_id=custom_id, url=url, disabled=disabled)
238238

239239

240-
def select_option(label: str, value: str, emoji: Union[PartialEmoji, str]=None, description: str=None, default=False) -> dict:
240+
def select_option(label: str, value: str, emoji: Union[PartialEmoji, str] = None, description: str = None, default = False) -> dict:
241241
if isinstance(emoji, PartialEmoji):
242242
emoji = emoji
243243
if isinstance(emoji, Emoji):
@@ -246,52 +246,71 @@ def select_option(label: str, value: str, emoji: Union[PartialEmoji, str]=None,
246246
emoji = PartialEmoji(name=emoji)
247247
else:
248248
emoji = None
249-
250249
base = {'label': label,
251250
'value': value,
252-
'description': description,
253251
'default': default}
252+
if description:
253+
base['description'] = str(description)
254254
if emoji:
255255
base['emoji'] = emoji.to_dict()
256256
return base
257257

258258

259-
class SelectionMenu:
259+
class SelectMenu:
260260

261261
"""
262-
Represents an Discord-dropdown-Menue
263-
.. note ::
264-
This Feature is ``not`` released jet!
262+
Represents an ``Discord-Select-Menu``
263+
264+
.. note::
265+
For more information about Select-Menus wisit the `Discord-API-Documentation <https://discord.com/developers/docs/interactions/message-components#select-menus>`_.
266+
265267
"""
266268

267-
__slots__ = ('custom_id', 'options', 'placeholder', 'min_values', 'max_values')
269+
__slots__ = ('custom_id', 'options', 'placeholder', 'min_values', 'max_values', 'disabled')
268270

269-
def __init__(self, **kwargs):
270-
self.options: list = kwargs.get('options', [])
271+
def __init__(self, custom_id: str, options: typing.List[typing.Dict], placeholder: str = None, min_values: int = 1, max_values: int = 1, disabled: bool = False):
272+
self.options: list = options
271273
if not any([isinstance(obj, dict) for obj in self.options]):
272-
raise InvalidData("SelectionMenu-Options have to bee an Dict like `{'label': 'that what should show up in Discord', 'value': 'that what the Discord-API sends to your Application if the option is chosen'}`, or usually an :function:`discord.components.create_option`.")
273-
self.custom_id: str = kwargs.get('custom_id', 'no_custom_id_set')
274-
self.placeholder: str = kwargs.get('placeholder', None)
275-
self.min_values = kwargs.get('min_values', None)
276-
self.max_values = kwargs.get('max_values', None)
274+
raise InvalidData(
275+
"Select menu options must be a list of dicts' like `{'label': 'what to display in Discord',"
276+
" 'value': 'what the Discord API sends to your application when the option is selected'}`; you can easily create"
277+
" it with the :function:`discord.components.create_option`.")
278+
elif len(self.options) > 25:
279+
raise InvalidArgument('The maximum number of options in a select menu is 25.')
280+
self.custom_id: str = custom_id
281+
if len(self.custom_id) > 100:
282+
raise("The maximum length of a custom-id is 100 characters.")
283+
self.placeholder: str = placeholder
284+
if self.placeholder and len(self.placeholder) > 100:
285+
raise AttributeError("The maximum length of a the placeholder is 100 characters.")
286+
self.min_values = min_values
287+
if min_values > 25 or min_values < 0:
288+
raise ValueError('The minimum number of elements to be selected must be between 0 and 25')
289+
self.max_values = max_values
290+
if self.max_values > 25 or self.max_values < 0:
291+
raise ValueError('The maximum number of elements to be selected must be between 0 and 25')
292+
self.disabled = disabled
277293

278294
def __repr__(self):
279-
return f'<SelectionMenu {", ".join([k + "=" + getattr(self, k, ) for k in self.__slots__])}>'
295+
return f'<SelectMenu {", ".join([k + "=" + getattr(self, k, ) for k in self.__slots__])}>'
280296

281297
def to_dict(self) -> dict:
282-
return {'type': 3, 'custom_id': self.custom_id, 'options': self.options, 'placeholder': self.placeholder, 'min_values': self.min_values, 'max_values': self.max_values}
298+
base = {'type': 3, 'custom_id': self.custom_id, 'options': self.options, 'placeholder': self.placeholder, 'min_values': self.min_values, 'max_values': self.max_values}
299+
if self.disabled is True:
300+
base['disabled'] = True
301+
return base
283302

284303
def update(self, **kwargs):
285304
self.__dict__.update((k, v) for k, v in kwargs.items() if k in self.__dict__.keys())
286305
return self
287306

288307
@classmethod
289308
def from_dict(cls, data: dict):
290-
custom_id = data.get('custom_id', None)
291-
options = data.get('options', None)
309+
custom_id = data.get('custom_id')
310+
options = data.get('options', [])
292311
placeholder = data.get('placeholder', None)
293-
min_values = data.get('min_values', None)
294-
max_values = data.get('max_values', None)
312+
min_values = data.get('min_values', 1)
313+
max_values = data.get('max_values', 1)
295314
return cls(custom_id=custom_id,
296315
options=options,
297316
placeholder=placeholder,
@@ -314,28 +333,32 @@ def __init__(self, *args, **kwargs):
314333
for obj in args:
315334
if isinstance(obj, Button):
316335
self.components.append(obj)
317-
elif isinstance(obj, SelectionMenu):
336+
elif isinstance(obj, SelectMenu):
318337
self.components.append(obj)
319338
elif isinstance(obj, dict):
320339
if not obj.get('type', None) in [2, 3]:
321-
raise InvalidData('if you use an Dict instead of Button or SelectionMenu you have to pass an type betwean 2 or 3')
322-
self.components.append({2: Button.from_dict(obj), 3: SelectionMenu.from_dict(obj)}.get(obj.get('type')))
340+
raise InvalidData('if you use an Dict instead of Button or SelectMenu you have to pass an type between 2 or 3')
341+
self.components.append({2: Button.from_dict(obj), 3: SelectMenu.from_dict(obj)}.get(obj.get('type')))
323342

324343
def __repr__(self):
325344
return f'<ActionRow components={self.components}>'
326345

327-
def sendable(self) -> Union[dict, EmptyActionRow]:
346+
def sendable(self) -> Union[list, EmptyActionRow]:
328347
base = []
329348
base.extend([{'type': 1, 'components': [obj.to_dict() for obj in self.components[five:5:]]} for five in range(0, len(self.components), 5)])
330349
objects = len([i['components'] for i in base])
331-
if any(len(ar['components']) < 1 for ar in base) and self.force is False:
350+
if any([any([part['type'] == 2]) and any([part['type'] == 3]) for part in base]):
351+
raise InvalidArgument('An Action Row containing a select menu cannot also contain buttons')
352+
elif any([any([part['type'] == 3]) and len(part) > 1 for part in base]):
353+
raise InvalidArgument('An Action Row can contain only one select menu')
354+
if any([len(ar['components']) < 1 for ar in base]) and self.force is False:
332355
raise EmptyActionRow()
333-
elif len(base) > 5 or objects > 5*5 :
356+
elif len(base) > 5 or objects > 25 :
334357
raise InvalidArgument(f"The maximum number of ActionRow's per message is 5 and they can only contain 5 buttons each; you have {len(base)} ActionRow's passed with {objects} objects")
335358
return base
336359

337360
def edit_obj(self, index: int, **kwargs):
338-
obj: Union[Button, SelectionMenu] = self.components.pop(index)
361+
obj: Union[Button, SelectMenu] = self.components.pop(index)
339362
self.components.insert(index, obj.update(**kwargs))
340363
return self
341364

@@ -383,7 +406,7 @@ def __init__(self, value):
383406
self._other_elements = []
384407
for obj in value:
385408
try:
386-
self._other_elements.extend(ActionRow.from_dict(obj))
409+
self._action_rows.append(ActionRow.from_dict(obj))
387410
except InvalidData:
388411
self._other_elements.append(obj)
389412
if self._other_elements:
@@ -399,4 +422,4 @@ def other_elements(self):
399422

400423
class ComponentType:
401424
Button = 2
402-
SlectionMenu = 3
425+
SelectMenu = 3

0 commit comments

Comments
 (0)