Skip to content

Conversation

doluk
Copy link
Contributor

@doluk doluk commented May 27, 2025

Summary

Looking at https://discord.com/developers/docs/components/reference#user-select I noticed that there is support for preselecting users, channels, roles, mentionables for the non string selects. This pr adds support for this.

Information

  • This PR fixes an issue.
  • This PR adds something new (e.g. new method or parameters).
  • This PR is a breaking change (e.g. methods or parameters removed/renamed).
  • This PR is not a code change (e.g. documentation, README, typehinting,
    examples, ...).

Checklist

  • I have searched the open pull requests for duplicates.
  • If code changes were made then they have been tested.
    • I have updated the documentation to reflect the changes.
  • If type: ignore comments were used, a comment is also left explaining why.
  • I have updated the changelog to include these changes.

doluk added 5 commits May 27, 2025 17:18
This commit expands handling of "default_values" to include objects like users, roles, channels, and threads in select menus other than string selects. It introduces type checks and conversions for these objects and ensures proper serialization to payloads.
Add support for creating SelectDefaultValue from dictionaries and improve serialization by converting default_values elements to dictionaries. Ensure consistency by using the enum value for type in payload generation.
raise InvalidArgument(
"default_values can only be set on non string selects"
)
if not isinstance(value, list):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the necessity of these checks. value is already typed. Wait for other feedback.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of those checks are again similar to how the options work/are checked.
My argument for stricter type checking and not just relying on the typing would be the similarities to the options could lead to confusion between both and with those checks we can avoid some reports and provide a better exception

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The specific checks I'm talking about are the list and all(isinstance) ones. While I feel like the other might be justified, it feels redundant to have type checks with also typed parameters. Yes, to some extent it is good to have better exceptions, but python isn't strictly typed anyways, as of itself, and it feels a little arbitrary to add them here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lumabots maybe you have thoughts on this ?

raise InvalidArgument(
"default values have to be of type GuildChannel or Thread"
)
elif self.type is ComponentType.channel_select:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a dict mapping could be used for setting default_value_type

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@doluk Anything about this ?

@Paillat-dev
Copy link
Member

@NeloBlivion Would this interfere with #2707 ?

Copy link
Member

@JustaSqu1d JustaSqu1d left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor changes and docs suggestions

Add this feature to the pre-existing select example.

@doluk
Copy link
Contributor Author

doluk commented Jun 6, 2025

I addressed most of the review comments, the two left open are something which need a final decision on what to do. @Paillat-dev @JustaSqu1d
Hope the example I added is suitable

@pullapprove4 pullapprove4 bot requested a review from plun1331 August 5, 2025 19:15
@Paillat-dev Paillat-dev self-assigned this Aug 5, 2025
@Paillat-dev
Copy link
Member

@doluk can you take a look at the merge conflicts ?

@doluk
Copy link
Contributor Author

doluk commented Aug 5, 2025

the component v2 merge created that, I am not sure there is an easy way out of it. I was thinking about opening another pr from the current main instead of trying to resolve the conflicts somehow

@Paillat-dev
Copy link
Member

@doluk I am going to try the merge and we'll see..

@Paillat-dev
Copy link
Member

@doluk I merged it but I am not sure it is all good, would you mind taking a look ?

channel_types: list[ChannelType] | None = None,
disabled: bool = False,
row: int | None = None,
default_values: (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to be added to the docstring

@Paillat-dev Paillat-dev requested a review from NeloBlivion August 6, 2025 08:32
@plun1331
Copy link
Member

@doluk updates?

@Lulalaby
Copy link
Member

Would be nice if either

  • it's being worked on by you @doluk
  • or taken over by one of the contributors / core team

Another upcoming feature (in a few~ish weeks) depends on that as well

@plun1331 plun1331 requested a review from a team as a code owner August 23, 2025 04:48
@Lulalaby Lulalaby force-pushed the master branch 2 times, most recently from b55c125 to 82659b2 Compare August 30, 2025 21:10
@Lulalaby Lulalaby removed the on hold label Aug 30, 2025
@Lulalaby Lulalaby requested a review from a team as a code owner September 1, 2025 12:31
Copy link
Contributor

@DA-344 DA-344 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default_values should still be added to the required docstrings

Comment on lines +1094 to +1096
Channel = "channel"
Role = "role"
User = "user"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should follow the library style:

Suggested change
Channel = "channel"
Role = "role"
User = "user"
channel = "channel"
role = "role"
user = "user"

Comment on lines +625 to +626
self.id = id
self.type = type
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type annotations

Suggested change
self.id = id
self.type = type
self.id: int = id
self.type: SelectMenuDefaultValueType = type

max_values: int = 1,
disabled: bool = False,
row: int | None = None,
default_values: list[Member | User | Role] | None = None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should still allow SelectDefaultValue instances

Suggested change
default_values: list[Member | User | Role] | None = None,
default_values: list[Member | User | Role | SelectDefaultValue] | None = None,

max_values: int = 1,
disabled: bool = False,
row: int | None = None,
default_values: list[Role] | None = None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
default_values: list[Role] | None = None,
default_values: list[Role | SelectDefaultValue] | None = None,

custom_id: str | None = None,
min_values: int = 1,
max_values: int = 1,
default_values: list[Member | User] | None = None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
default_values: list[Member | User] | None = None,
default_values: list[Member | User | SelectDefaultValue] | None = None,

Comment on lines +163 to +219
elif select_type is ComponentType.role_select:
for r in default_values:
if not isinstance(r, Role):
raise ValueError(
f"default_values must be a list of Role objects, not {r.__class__.__name__}"
)
_default_values.append(
SelectDefaultValue(
id=r.id, type=SelectMenuDefaultValueType.Role
)
)
elif select_type is ComponentType.user_select:
for u in default_values:
if not isinstance(u, (User, Member)):
raise ValueError(
f"default_values must be a list of User/Member objects, "
f"not {u.__class__.__name__}"
)
_default_values.append(
SelectDefaultValue(
id=u.id, type=SelectMenuDefaultValueType.User
)
)
elif select_type is ComponentType.channel_select:
for c in default_values:
if not isinstance(c, GuildChannel):
raise ValueError(
f"default_values must be a list of GuildChannel objects, "
f"not {c.__class__.__name__}"
)
if channel_types and c.type not in channel_types:
raise ValueError(
f"default_values must be a list of channels of type {channel_types}, "
f"not {c.__class__.__name__}"
)
_default_values.append(
SelectDefaultValue(
id=c.id, type=SelectMenuDefaultValueType.Channel
)
)
elif select_type is ComponentType.mentionable_select:
for m in default_values:
if not isinstance(m, (User, Member, Role)):
raise ValueError(
f"default_values must be a list of User/Member/Role objects, "
f"not {m.__class__.__name__}"
)
_default_values.append(
SelectDefaultValue(
id=m.id,
type=(
SelectMenuDefaultValueType.Role
if isinstance(m, Role)
else SelectMenuDefaultValueType.User
),
)
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should still allow for SelectMenuDefaultValueType instances

Suggested change
elif select_type is ComponentType.role_select:
for r in default_values:
if not isinstance(r, Role):
raise ValueError(
f"default_values must be a list of Role objects, not {r.__class__.__name__}"
)
_default_values.append(
SelectDefaultValue(
id=r.id, type=SelectMenuDefaultValueType.Role
)
)
elif select_type is ComponentType.user_select:
for u in default_values:
if not isinstance(u, (User, Member)):
raise ValueError(
f"default_values must be a list of User/Member objects, "
f"not {u.__class__.__name__}"
)
_default_values.append(
SelectDefaultValue(
id=u.id, type=SelectMenuDefaultValueType.User
)
)
elif select_type is ComponentType.channel_select:
for c in default_values:
if not isinstance(c, GuildChannel):
raise ValueError(
f"default_values must be a list of GuildChannel objects, "
f"not {c.__class__.__name__}"
)
if channel_types and c.type not in channel_types:
raise ValueError(
f"default_values must be a list of channels of type {channel_types}, "
f"not {c.__class__.__name__}"
)
_default_values.append(
SelectDefaultValue(
id=c.id, type=SelectMenuDefaultValueType.Channel
)
)
elif select_type is ComponentType.mentionable_select:
for m in default_values:
if not isinstance(m, (User, Member, Role)):
raise ValueError(
f"default_values must be a list of User/Member/Role objects, "
f"not {m.__class__.__name__}"
)
_default_values.append(
SelectDefaultValue(
id=m.id,
type=(
SelectMenuDefaultValueType.Role
if isinstance(m, Role)
else SelectMenuDefaultValueType.User
),
)
)
elif select_type is ComponentType.role_select:
for r in default_values:
if isinstance(r, Role):
_default_values.append(SelectDefaultValue(id=r.id, type=SelectMenuDefaultValueType.role))
elif isinstance(r, SelectDefaultValue) and r.type is SelectMenuDefaultValueType.role:
_default_values.append(r)
else:
raise ValueError('default_values must be a list of Role or SelectDefaultValue objects of type SelectMenuDefaultValueType.role')
elif select_type is ComponentType.user_select:
for u in default_values:
if isinstance(u, (User, Member)):
_default_values.append(SelectDefaultValue(id=u.id, type=SelectMenuDefaultValueType.user))
elif isinstance(u, SelectDefaultValue) and u.type is SelectMenuDefaultValueType.user:
_default_values.append(u)
else:
raise ValueError('default_values must be a list of User, Member, or SelectDefaultValue objects of type SelectMenuDefaultValueType.user')
elif select_type is ComponentType.channel_select:
for c in default_values:
if isinstance(c, GuildChannel):
if channel_types and c.type not in channel_types:
raise TypeError(f'default_values channel types must be any of {channel_types}')
_default_values.append(SelectDefaultValue(id=c.id, type=SelectMenuDefaultValueType.channel))
elif isinstance(c, SelectDefaultValue) and c.type is SelectMenuDefaultValueType.channel:
_default_values.append(r)
else:
raise ValueError('default_values must be a list of GuildChannel or SelectDefaultValue objects of type SelectMenuDefaultValueType.channel')
elif select_type is ComponentType.mentionable_select:
for m in default_values:
if isinstance(m, (User, Member, Role)):
_default_values.append(SelectDefaultValue(id=m.id, type=SelectMenuDefaultValueType.role if isinstance(m, Role) else SelectMenuDefaultValueType.user))
elif isinstance(m, SelectDefaultValue) and m.type in (SelectMenuDefaultValueType.role, SelectMenuDefaultValueType.user):
_default_values.append(m)
else:
raise ValueError('default_values must be a list of Role, User, Member, or SelectDefaultValue objects of type SelectMenuDefaultValueType.role or SelectMenuDefaultValueType.user')

Comment on lines +425 to +429
def add_default_value(
self,
*,
default_value: User | Member | Role | GuildChannel | Thread,
):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allow for Object instances when you don't have full User, Member, Role or GuildChannel instances

Suggested change
def add_default_value(
self,
*,
default_value: User | Member | Role | GuildChannel | Thread,
):
def add_default_value(
self,
*,
default_value: User | Member | Role | GuildChannel | Thread | Object,
type: SelectMenuDefaultValueType = MISSING,
):

Comment on lines +437 to +439
default_value: Union[:class:`discord.User`, :class:`discord.Member`, :class:`discord.Role`,
:class:`discord.abc.GuildChannel`, :class:`discord.Thread`]
The to be added default value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
default_value: Union[:class:`discord.User`, :class:`discord.Member`, :class:`discord.Role`,
:class:`discord.abc.GuildChannel`, :class:`discord.Thread`]
The to be added default value
default_value: Union[:class:`discord.User`, :class:`discord.Member`, :class:`discord.Role`,
:class:`discord.abc.GuildChannel`, :class:`discord.Thread`, :class:`discord.Object`]
The to be added default value. If this is an ``Object`` instance, you must pass ``type`` if the select type is ``mentionable``.
type: :class:`discord.SelectMenuDefaultvalueType`
The default value type, required if ``default_value`` is an ``Object`` and the select type is ``mentionable``.

Comment on lines +448 to +494
default_value_type = None
if self.type is ComponentType.channel_select and not isinstance(
default_value, (GuildChannel, Thread)
):
raise InvalidArgument(
"Default values have to be of type GuildChannel or Thread with type ComponentType.channel_select"
)
elif self.type is ComponentType.channel_select:
default_value_type = SelectMenuDefaultValueType.Channel
if self.type is ComponentType.user_select and not isinstance(
default_value, (User, Member)
):
raise InvalidArgument(
"Default values have to be of type User or Member with type ComponentType.user_select"
)
elif self.type is ComponentType.user_select:
default_value_type = SelectMenuDefaultValueType.User
if self.type is ComponentType.role_select and not isinstance(
default_value, Role
):
raise InvalidArgument(
"Default values have to be of type Role with type ComponentType.role_select"
)
elif self.type is ComponentType.role_select:
default_value_type = SelectMenuDefaultValueType.Role
if self.type is ComponentType.mentionable_select and not isinstance(
default_value, (User, Member, Role)
):
raise InvalidArgument(
"Default values have to be of type User, Member or Role with type ComponentType.mentionable_select"
)
elif self.type is ComponentType.mentionable_select and isinstance(
default_value, (User, Member)
):
default_value_type = SelectMenuDefaultValueType.User
elif self.type is ComponentType.mentionable_select and isinstance(
default_value, Role
):
default_value_type = SelectMenuDefaultValueType.Role
if default_value_type is None:
raise InvalidArgument(
"Default values have to be of type User, Member, Role or GuildChannel"
)
default_value = SelectDefaultValue(
id=default_value.id,
type=default_value_type,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a mix of what paillat said and support for passing Object instances.

Suggested change
default_value_type = None
if self.type is ComponentType.channel_select and not isinstance(
default_value, (GuildChannel, Thread)
):
raise InvalidArgument(
"Default values have to be of type GuildChannel or Thread with type ComponentType.channel_select"
)
elif self.type is ComponentType.channel_select:
default_value_type = SelectMenuDefaultValueType.Channel
if self.type is ComponentType.user_select and not isinstance(
default_value, (User, Member)
):
raise InvalidArgument(
"Default values have to be of type User or Member with type ComponentType.user_select"
)
elif self.type is ComponentType.user_select:
default_value_type = SelectMenuDefaultValueType.User
if self.type is ComponentType.role_select and not isinstance(
default_value, Role
):
raise InvalidArgument(
"Default values have to be of type Role with type ComponentType.role_select"
)
elif self.type is ComponentType.role_select:
default_value_type = SelectMenuDefaultValueType.Role
if self.type is ComponentType.mentionable_select and not isinstance(
default_value, (User, Member, Role)
):
raise InvalidArgument(
"Default values have to be of type User, Member or Role with type ComponentType.mentionable_select"
)
elif self.type is ComponentType.mentionable_select and isinstance(
default_value, (User, Member)
):
default_value_type = SelectMenuDefaultValueType.User
elif self.type is ComponentType.mentionable_select and isinstance(
default_value, Role
):
default_value_type = SelectMenuDefaultValueType.Role
if default_value_type is None:
raise InvalidArgument(
"Default values have to be of type User, Member, Role or GuildChannel"
)
default_value = SelectDefaultValue(
id=default_value.id,
type=default_value_type,
)
if self.type is ComponentType.mentionable_select and isinstance(default_value, Object) and type is MISSING:
raise ValueError('type is required when the default_value is an Object and select type is mentionable')
elif self.type is ComponentType.mentionable_select and isinstance(default_value, (Member, User)):
default_value_type = SelectMenuDefaultValueType.user
elif self.type is ComponentType.mentionable_select and isinstance(default_value, Role):
default_value_type = SelectMenuDefaultValueType.role
elif self.type is ComponentType.mentionable_select:
default_value_type = type
else:
value_mapping = {
ComponentType.role_select: SelectMenuDefaultValueType.role,
ComponentType.channel_select: SelectMenuDefaultValueType.channel,
ComponentType.user_select: SelectMenuDefaultValueType.user,
}
try:
default_value_type = value_mapping[self.type]
except KeyError:
raise ValueError(f'invalid select type: {self.type}')
default_value = SelectDefaultValue(
id=default_value.id,
type=default_value_type,
)

Comment on lines +33 to 38
from ..components import SelectDefaultValue, SelectMenu, SelectOption
from ..emoji import AppEmoji, GuildEmoji
from ..enums import ChannelType, ComponentType
from ..enums import ChannelType, ComponentType, SelectMenuDefaultValueType
from ..errors import InvalidArgument
from ..interactions import Interaction
from ..member import Member
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes i've suggested require importing Object

Suggested change
from ..components import SelectDefaultValue, SelectMenu, SelectOption
from ..emoji import AppEmoji, GuildEmoji
from ..enums import ChannelType, ComponentType
from ..enums import ChannelType, ComponentType, SelectMenuDefaultValueType
from ..errors import InvalidArgument
from ..interactions import Interaction
from ..member import Member
from ..components import SelectDefaultValue, SelectMenu, SelectOption
from ..emoji import AppEmoji, GuildEmoji
from ..enums import ChannelType, ComponentType, SelectMenuDefaultValueType
from ..errors import InvalidArgument
from ..interactions import Interaction
from ..member import Member
from ..object import Object

@Paillat-dev Paillat-dev assigned DA-344 and unassigned Paillat-dev Sep 1, 2025
@Lulalaby Lulalaby closed this Sep 1, 2025
@Lulalaby
Copy link
Member

Lulalaby commented Sep 1, 2025

Let's do a clean new pr

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants