Skip to content

Commit 3b8b79f

Browse files
DA-344pre-commit-ci[bot]LulalabyPaillat-devplun1331
authored
feat: Add support for default_values in Selects (#2899)
Signed-off-by: Lala Sabathil <[email protected]> Signed-off-by: plun1331 <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lala Sabathil <[email protected]> Co-authored-by: Paillat <[email protected]> Co-authored-by: Lala Sabathil <[email protected]> Co-authored-by: plun1331 <[email protected]>
1 parent ec5da54 commit 3b8b79f

File tree

14 files changed

+699
-42
lines changed

14 files changed

+699
-42
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ These changes are available on the `master` branch, but have not yet been releas
2020
- Added support for setting guild-specific `avatar`, `banner`, and `bio` for the bot
2121
user through `Member.edit`.
2222
([#2908](https://github.com/Pycord-Development/pycord/pull/2908))
23+
- Added support for select default values.
24+
([#2899](https://github.com/Pycord-Development/pycord/pull/2899))
25+
- Adds a new generic parameter to selects to type `ui.Select.values` return type.
26+
- Adds `SelectDefaultValue` object to create select default values.
27+
- Adds `SelectDefaultValueType` enum.
28+
- Adds pre-typed and pre-constructed with select_type `ui.Select` aliases for the
29+
different select types: `ui.StringSelect`, `ui.UserSelect`, `ui.RoleSelect`,
30+
`ui.MentionableSelect`, and `ui.ChannelSelect`.
2331

2432
### Changed
2533

discord/abc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ class User(Snowflake, Protocol):
235235
name: str
236236
discriminator: str
237237
global_name: str | None
238-
avatar: Asset
238+
avatar: Asset | None
239239
bot: bool
240240

241241
@property

discord/components.py

Lines changed: 186 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
from __future__ import annotations
2727

28-
from typing import TYPE_CHECKING, Any, ClassVar, Iterator, TypeVar
28+
from typing import TYPE_CHECKING, Any, ClassVar, Iterator, TypeVar, overload
2929

3030
from .asset import AssetMixin
3131
from .colour import Colour
@@ -34,6 +34,7 @@
3434
ChannelType,
3535
ComponentType,
3636
InputTextStyle,
37+
SelectDefaultValueType,
3738
SeparatorSpacingSize,
3839
try_enum,
3940
)
@@ -42,9 +43,9 @@
4243
from .utils import MISSING, find, get_slots
4344

4445
if TYPE_CHECKING:
46+
from . import abc
4547
from .emoji import AppEmoji, GuildEmoji
4648
from .types.components import ActionRow as ActionRowPayload
47-
from .types.components import BaseComponent as BaseComponentPayload
4849
from .types.components import ButtonComponent as ButtonComponentPayload
4950
from .types.components import Component as ComponentPayload
5051
from .types.components import ContainerComponent as ContainerComponentPayload
@@ -54,6 +55,7 @@
5455
from .types.components import MediaGalleryComponent as MediaGalleryComponentPayload
5556
from .types.components import MediaGalleryItem as MediaGalleryItemPayload
5657
from .types.components import SectionComponent as SectionComponentPayload
58+
from .types.components import SelectDefaultValue as SelectDefaultValuePayload
5759
from .types.components import SelectMenu as SelectMenuPayload
5860
from .types.components import SelectOption as SelectOptionPayload
5961
from .types.components import SeparatorComponent as SeparatorComponentPayload
@@ -78,6 +80,7 @@
7880
"Separator",
7981
"Container",
8082
"Label",
83+
"SelectDefaultValue",
8184
)
8285

8386
C = TypeVar("C", bound="Component")
@@ -437,6 +440,7 @@ class SelectMenu(Component):
437440
"channel_types",
438441
"disabled",
439442
"required",
443+
"default_values",
440444
)
441445

442446
__repr_info__: ClassVar[tuple[str, ...]] = __slots__
@@ -457,6 +461,9 @@ def __init__(self, data: SelectMenuPayload):
457461
try_enum(ChannelType, ct) for ct in data.get("channel_types", [])
458462
]
459463
self.required: bool | None = data.get("required")
464+
self.default_values: list[SelectDefaultValue] = SelectDefaultValue._from_data(
465+
data.get("default_values")
466+
)
460467

461468
def to_dict(self) -> SelectMenuPayload:
462469
payload: SelectMenuPayload = {
@@ -476,10 +483,187 @@ def to_dict(self) -> SelectMenuPayload:
476483
payload["placeholder"] = self.placeholder
477484
if self.required is not None:
478485
payload["required"] = self.required
486+
if self.type is not ComponentType.string_select:
487+
payload["default_values"] = [dv.to_dict() for dv in self.default_values]
479488

480489
return payload
481490

482491

492+
class SelectDefaultValue:
493+
r"""Represents a :class:`discord.SelectMenu`\s default value.
494+
495+
This is only applicable to selects of type other than :attr:`ComponentType.string_select`.
496+
497+
.. versionadded:: 2.7
498+
499+
Parameters
500+
----------
501+
object: :class:`abc.Snowflake`
502+
The model type this select default value is based of.
503+
504+
Below, is a table defining the model instance type and the default value type it will be mapped:
505+
506+
+-----------------------------------+--------------------------------------------------------------------------+
507+
| Model Type | Default Value Type |
508+
+-----------------------------------+--------------------------------------------------------------------------+
509+
| :class:`discord.User` | :attr:`discord.SelectDefaultValueType.user` |
510+
+-----------------------------------+--------------------------------------------------------------------------+
511+
| :class:`discord.Member` | :attr:`discord.SelectDefaultValueType.user` |
512+
+-----------------------------------+--------------------------------------------------------------------------+
513+
| :class:`discord.Role` | :attr:`discord.SelectDefaultValueType.role` |
514+
+-----------------------------------+--------------------------------------------------------------------------+
515+
| :class:`discord.abc.GuildChannel` | :attr:`discord.SelectDefaultValueType.channel` |
516+
+-----------------------------------+--------------------------------------------------------------------------+
517+
| :class:`discord.Object` | depending on :attr:`discord.Object.type`, it will be mapped to any above |
518+
+-----------------------------------+--------------------------------------------------------------------------+
519+
520+
If you pass a model that is not defined in the table, ``TypeError`` will be raised.
521+
522+
.. note::
523+
524+
The :class:`discord.abc.GuildChannel` protocol includes :class:`discord.TextChannel`, :class:`discord.VoiceChannel`, :class:`discord.StageChannel`,
525+
:class:`discord.ForumChannel`, :class:`discord.Thread`, :class:`discord.MediaChannel`. This list is not exhaustive, and is bound to change
526+
based of the new channel types Discord adds.
527+
528+
id: :class:`int`
529+
The ID of the default value. This cannot be used with ``object``.
530+
type: :class:`SelectDefaultValueType`
531+
The default value type. This cannot be used with ``object``.
532+
533+
Raises
534+
------
535+
TypeError
536+
You did not provide any parameter, you provided all parameters, or you provided ``id`` but not ``type``.
537+
"""
538+
539+
__slots__ = ("id", "type")
540+
541+
@overload
542+
def __init__(
543+
self,
544+
object: abc.Snowflake,
545+
/,
546+
) -> None: ...
547+
548+
@overload
549+
def __init__(
550+
self,
551+
/,
552+
*,
553+
id: int,
554+
type: SelectDefaultValueType,
555+
) -> None: ...
556+
557+
def __init__(
558+
self,
559+
object: abc.Snowflake = MISSING,
560+
/,
561+
*,
562+
id: int = MISSING,
563+
type: SelectDefaultValueType = MISSING,
564+
) -> None:
565+
self.id: int = id
566+
self.type: SelectDefaultValueType = type
567+
if object is not MISSING:
568+
if any(p is not MISSING for p in (id, type)):
569+
raise TypeError("you cannot pass id or type when passing object")
570+
self._handle_model(object, inst=self)
571+
elif id is not MISSING and type is not MISSING:
572+
self.id = id
573+
self.type = type
574+
else:
575+
raise TypeError("you must provide an object model, or an id and type")
576+
577+
def __repr__(self) -> str:
578+
return f"<SelectDefaultValue id={self.id} type={self.type}>"
579+
580+
@classmethod
581+
def _from_data(
582+
cls, default_values: list[SelectDefaultValuePayload] | None
583+
) -> list[SelectDefaultValue]:
584+
if not default_values:
585+
return []
586+
return [
587+
cls(id=int(d["id"]), type=try_enum(SelectDefaultValueType, d["type"]))
588+
for d in default_values
589+
]
590+
591+
@classmethod
592+
def _handle_model(
593+
cls,
594+
model: abc.Snowflake,
595+
select_type: ComponentType | None = None,
596+
inst: SelectDefaultValue | None = None,
597+
) -> SelectDefaultValue:
598+
# preventing >circular imports<
599+
from discord import Member, Object, Role, User, abc
600+
from discord.user import _UserTag
601+
602+
instances_mapping: dict[
603+
type, tuple[tuple[ComponentType, ...], SelectDefaultValueType]
604+
] = {
605+
Role: (
606+
(ComponentType.role_select, ComponentType.mentionable_select),
607+
SelectDefaultValueType.role,
608+
),
609+
User: (
610+
(ComponentType.user_select, ComponentType.mentionable_select),
611+
SelectDefaultValueType.user,
612+
),
613+
Member: (
614+
(ComponentType.user_select, ComponentType.mentionable_select),
615+
SelectDefaultValueType.user,
616+
),
617+
_UserTag: (
618+
(ComponentType.user_select, ComponentType.mentionable_select),
619+
SelectDefaultValueType.user,
620+
),
621+
abc.GuildChannel: (
622+
(ComponentType.channel_select,),
623+
SelectDefaultValueType.channel,
624+
),
625+
}
626+
627+
obj_id = model.id
628+
obj_type = model.__class__
629+
630+
if isinstance(model, Object):
631+
obj_type = model.type
632+
633+
sel_types = None
634+
def_type = None
635+
636+
for typ, (st, dt) in instances_mapping.items():
637+
if issubclass(obj_type, typ):
638+
sel_types = st
639+
def_type = dt
640+
break
641+
642+
if sel_types is None or def_type is None:
643+
raise TypeError(
644+
f"{obj_type.__name__} is not a valid instance for a select default value"
645+
)
646+
647+
# we can't actually check select types when not in a select context
648+
if select_type is not None and select_type not in sel_types:
649+
raise TypeError(
650+
f"{model.__class__.__name__} objects can not be set as a default value for {select_type.value} selects",
651+
)
652+
653+
if inst is None:
654+
return cls(id=obj_id, type=def_type)
655+
else:
656+
inst.id = obj_id
657+
inst.type = def_type
658+
return inst
659+
660+
def to_dict(self) -> SelectDefaultValuePayload:
661+
return {
662+
"id": self.id,
663+
"type": self.type.value,
664+
}
665+
666+
483667
class SelectOption:
484668
"""Represents a :class:`discord.SelectMenu`'s option.
485669

discord/enums.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"ThreadArchiveDuration",
8484
"SubscriptionStatus",
8585
"SeparatorSpacingSize",
86+
"SelectDefaultValueType",
8687
)
8788

8889

@@ -1120,6 +1121,14 @@ def __int__(self):
11201121
return self.value
11211122

11221123

1124+
class SelectDefaultValueType(Enum):
1125+
"""Represents the default value type of a select menu."""
1126+
1127+
channel = "channel"
1128+
role = "role"
1129+
user = "user"
1130+
1131+
11231132
T = TypeVar("T")
11241133

11251134

discord/object.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
if TYPE_CHECKING:
3434
import datetime
3535

36+
from .abc import Snowflake
37+
3638
SupportsIntCast = Union[SupportsInt, str, bytes, bytearray]
3739

3840
__all__ = ("Object",)
@@ -70,9 +72,11 @@ class Object(Hashable):
7072
----------
7173
id: :class:`int`
7274
The ID of the object.
75+
type: type[:class:`abc.Snowflake`]
76+
The model this object's ID is based off.
7377
"""
7478

75-
def __init__(self, id: SupportsIntCast):
79+
def __init__(self, id: SupportsIntCast, type: type[Snowflake] = utils.MISSING):
7680
try:
7781
id = int(id)
7882
except ValueError:
@@ -81,6 +85,7 @@ def __init__(self, id: SupportsIntCast):
8185
) from None
8286
else:
8387
self.id = id
88+
self.type: type[Snowflake] = type or self.__class__
8489

8590
def __repr__(self) -> str:
8691
return f"<Object id={self.id!r}>"

discord/types/components.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
ButtonStyle = Literal[1, 2, 3, 4, 5, 6]
3838
InputTextStyle = Literal[1, 2]
3939
SeparatorSpacingSize = Literal[1, 2]
40+
SelectDefaultValueType = Literal["channel", "role", "user"]
4041

4142

4243
class BaseComponent(TypedDict):
@@ -46,7 +47,7 @@ class BaseComponent(TypedDict):
4647

4748
class ActionRow(BaseComponent):
4849
type: Literal[1]
49-
components: list[ButtonComponent, InputText, SelectMenu]
50+
components: list[ButtonComponent | InputText | SelectMenu]
5051

5152

5253
class ButtonComponent(BaseComponent):
@@ -80,6 +81,11 @@ class SelectOption(TypedDict):
8081
default: bool
8182

8283

84+
class SelectDefaultValue(TypedDict):
85+
id: Snowflake
86+
type: SelectDefaultValueType
87+
88+
8389
class SelectMenu(BaseComponent):
8490
placeholder: NotRequired[str]
8591
min_values: NotRequired[int]
@@ -90,6 +96,7 @@ class SelectMenu(BaseComponent):
9096
type: Literal[3, 5, 6, 7, 8]
9197
custom_id: str
9298
required: NotRequired[bool]
99+
default_values: NotRequired[list[SelectDefaultValue]]
93100

94101

95102
class TextDisplayComponent(BaseComponent):
@@ -100,7 +107,7 @@ class TextDisplayComponent(BaseComponent):
100107
class SectionComponent(BaseComponent):
101108
type: Literal[9]
102109
components: list[TextDisplayComponent]
103-
accessory: NotRequired[ThumbnailComponent, ButtonComponent]
110+
accessory: NotRequired[ThumbnailComponent | ButtonComponent]
104111

105112

106113
class UnfurledMediaItem(TypedDict):

discord/ui/button.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ def button(
294294
emoji: str | GuildEmoji | AppEmoji | PartialEmoji | None = None,
295295
row: int | None = None,
296296
id: int | None = None,
297-
) -> Callable[[ItemCallbackType], ItemCallbackType]:
297+
) -> Callable[[ItemCallbackType[Button[V]]], Button[V]]:
298298
"""A decorator that attaches a button to a component.
299299
300300
The function being decorated should have three parameters, ``self`` representing
@@ -347,4 +347,4 @@ def decorator(func: ItemCallbackType) -> ItemCallbackType:
347347
}
348348
return func
349349

350-
return decorator
350+
return decorator # type: ignore # lie to the type checkers, because after a View is instated, the button callback is converted into a Button instance

0 commit comments

Comments
 (0)