diff --git a/discord/abc.py b/discord/abc.py index fec57b52a..7f10811c4 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1130,10 +1130,10 @@ class GuildChannel: channel list (or category if given). This is mutually exclusive with ``beginning``, ``before``, and ``after``. before: :class:`~discord.abc.Snowflake` - The channel that should be before our current channel. + Whether to move the channel before the given channel. This is mutually exclusive with ``beginning``, ``end``, and ``after``. after: :class:`~discord.abc.Snowflake` - The channel that should be after our current channel. + Whether to move the channel after the given channel. This is mutually exclusive with ``beginning``, ``end``, and ``before``. offset: :class:`int` The number of channels to offset the move by. For example, diff --git a/discord/asset.py b/discord/asset.py index 7b9b71133..e3422f311 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -246,6 +246,17 @@ class Asset(AssetMixin): animated=animated, ) + @classmethod + def _from_guild_banner(cls, state: _State, guild_id: int, member_id: int, banner: str) -> Self: + animated = banner.startswith('a_') + format = 'gif' if animated else 'png' + return cls( + state, + url=f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}.{format}?size=1024", + key=banner, + animated=animated, + ) + @classmethod def _from_avatar_decoration(cls, state: _State, avatar_decoration: str) -> Self: return cls( diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 830c58662..6c559009d 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -126,6 +126,10 @@ class Converter(Protocol[T_co]): raise a :exc:`.CommandError` derived exception as it will properly propagate to the error handlers. + Note that if this method is called manually, :exc:`Exception` + should be caught to handle the cases where a subclass does + not explicitly inherit from :exc:`.CommandError`. + Parameters ----------- ctx: :class:`.Context` diff --git a/discord/guild.py b/discord/guild.py index f6c6b7d1b..89b64c465 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1126,7 +1126,7 @@ class Guild(Hashable): self, name: str, channel_type: Literal[ChannelType.text], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, ) -> Coroutine[Any, Any, TextChannelPayload]: @@ -1137,7 +1137,7 @@ class Guild(Hashable): self, name: str, channel_type: Literal[ChannelType.voice], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, ) -> Coroutine[Any, Any, VoiceChannelPayload]: @@ -1148,7 +1148,7 @@ class Guild(Hashable): self, name: str, channel_type: Literal[ChannelType.stage_voice], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, ) -> Coroutine[Any, Any, StageChannelPayload]: @@ -1159,7 +1159,7 @@ class Guild(Hashable): self, name: str, channel_type: Literal[ChannelType.category], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, ) -> Coroutine[Any, Any, CategoryChannelPayload]: @@ -1170,7 +1170,7 @@ class Guild(Hashable): self, name: str, channel_type: Literal[ChannelType.news], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, ) -> Coroutine[Any, Any, NewsChannelPayload]: @@ -1181,7 +1181,7 @@ class Guild(Hashable): self, name: str, channel_type: Literal[ChannelType.news, ChannelType.text], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, ) -> Coroutine[Any, Any, Union[TextChannelPayload, NewsChannelPayload]]: @@ -1192,7 +1192,7 @@ class Guild(Hashable): self, name: str, channel_type: Literal[ChannelType.forum], - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, ) -> Coroutine[Any, Any, ForumChannelPayload]: @@ -1203,7 +1203,7 @@ class Guild(Hashable): self, name: str, channel_type: ChannelType, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = ..., category: Optional[Snowflake] = ..., **options: Any, ) -> Coroutine[Any, Any, GuildChannelPayload]: @@ -1213,7 +1213,7 @@ class Guild(Hashable): self, name: str, channel_type: ChannelType, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, category: Optional[Snowflake] = None, **options: Any, ) -> Coroutine[Any, Any, GuildChannelPayload]: @@ -1253,7 +1253,7 @@ class Guild(Hashable): topic: str = MISSING, slowmode_delay: int = MISSING, nsfw: bool = MISSING, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, default_auto_archive_duration: int = MISSING, default_thread_slowmode_delay: int = MISSING, ) -> TextChannel: @@ -1395,7 +1395,7 @@ class Guild(Hashable): user_limit: int = MISSING, rtc_region: Optional[str] = MISSING, video_quality_mode: VideoQualityMode = MISSING, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, ) -> VoiceChannel: """|coro| @@ -1488,7 +1488,7 @@ class Guild(Hashable): user_limit: int = MISSING, rtc_region: Optional[str] = MISSING, video_quality_mode: VideoQualityMode = MISSING, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, ) -> StageChannel: """|coro| @@ -1581,7 +1581,7 @@ class Guild(Hashable): self, name: str, *, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, reason: Optional[str] = None, position: int = MISSING, ) -> CategoryChannel: @@ -1636,7 +1636,7 @@ class Guild(Hashable): category: Optional[CategoryChannel] = None, slowmode_delay: int = MISSING, nsfw: bool = MISSING, - overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + overwrites: Mapping[Union[Role, Member, Object], PermissionOverwrite] = MISSING, reason: Optional[str] = None, default_auto_archive_duration: int = MISSING, default_thread_slowmode_delay: int = MISSING, diff --git a/discord/member.py b/discord/member.py index 74ba86932..2eadacd27 100644 --- a/discord/member.py +++ b/discord/member.py @@ -322,6 +322,7 @@ class Member(discord.abc.Messageable, _UserTag): '_user', '_state', '_avatar', + '_banner', '_flags', '_avatar_decoration_data', ) @@ -358,6 +359,7 @@ class Member(discord.abc.Messageable, _UserTag): self.nick: Optional[str] = data.get('nick', None) self.pending: bool = data.get('pending', False) self._avatar: Optional[str] = data.get('avatar') + self._banner: Optional[str] = data.get('banner') self._permissions: Optional[int] self._flags: int = data['flags'] self._avatar_decoration_data: Optional[AvatarDecorationData] = data.get('avatar_decoration_data') @@ -438,6 +440,7 @@ class Member(discord.abc.Messageable, _UserTag): self._permissions = member._permissions self._state = member._state self._avatar = member._avatar + self._banner = member._banner self._avatar_decoration_data = member._avatar_decoration_data # Reference will not be copied unless necessary by PRESENCE_UPDATE @@ -466,6 +469,7 @@ class Member(discord.abc.Messageable, _UserTag): self.timed_out_until = utils.parse_time(data.get('communication_disabled_until')) self._roles = utils.SnowflakeList(map(int, data['roles'])) self._avatar = data.get('avatar') + self._banner = data.get('banner') self._flags = data.get('flags', 0) self._avatar_decoration_data = data.get('avatar_decoration_data') @@ -649,6 +653,28 @@ class Member(discord.abc.Messageable, _UserTag): return None return Asset._from_guild_avatar(self._state, self.guild.id, self.id, self._avatar) + @property + def display_banner(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the member's displayed banner, if any. + + This is the member's guild banner if available, otherwise it's their + global banner. If the member has no banner set then ``None`` is returned. + + .. versionadded:: 2.5 + """ + return self.guild_banner or self._user.banner + + @property + def guild_banner(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns an :class:`Asset` for the guild banner + the member has. If unavailable, ``None`` is returned. + + .. versionadded:: 2.5 + """ + if self._banner is None: + return None + return Asset._from_guild_banner(self._state, self.guild.id, self.id, self._banner) + @property def activity(self) -> Optional[ActivityTypes]: """Optional[Union[:class:`BaseActivity`, :class:`Spotify`]]: Returns the primary diff --git a/discord/poll.py b/discord/poll.py index c523f1609..7dc3897ac 100644 --- a/discord/poll.py +++ b/discord/poll.py @@ -388,9 +388,6 @@ class Poll: # self.created_at = message.created_at # duration = self.created_at - expiry - if (duration.total_seconds() / 3600) > 168: # As the duration may exceed little milliseconds then we fix it - duration = datetime.timedelta(days=7) - self = cls( duration=duration, multiple=multiselect, diff --git a/discord/types/member.py b/discord/types/member.py index 6968edb6f..88fb619fd 100644 --- a/discord/types/member.py +++ b/discord/types/member.py @@ -48,6 +48,7 @@ class Member(PartialMember, total=False): pending: bool permissions: str communication_disabled_until: str + banner: NotRequired[Optional[str]] avatar_decoration_data: NotRequired[AvatarDecorationData] diff --git a/discord/ui/select.py b/discord/ui/select.py index b7a8e694c..6738b9727 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -499,7 +499,7 @@ class Select(BaseSelect[V]): The number of options exceeds 25. """ - if len(self._underlying.options) > 25: + if len(self._underlying.options) >= 25: raise ValueError('maximum number of options already provided') self._underlying.options.append(option) diff --git a/tests/test_ui_selects.py b/tests/test_ui_selects.py new file mode 100644 index 000000000..a9019c3de --- /dev/null +++ b/tests/test_ui_selects.py @@ -0,0 +1,39 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +import discord +import pytest + + +@pytest.mark.asyncio +async def test_add_option(): + select = discord.ui.Select() + + for i in range(1, 25 + 1): + select.add_option(label=str(i), value=str(i)) + + with pytest.raises(ValueError): + select.add_option(label="26", value="26")