Skip to content

Commit 9a5554f

Browse files
NeloBlivionPaillat-dev
authored andcommitted
feat: components v2 & View improvements (Pycord-Development#2707)
Signed-off-by: UK <[email protected]> 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: Dorukyum <[email protected]> Co-authored-by: Paillat <[email protected]> Co-authored-by: Ice Wolfy <[email protected]> Co-authored-by: plun1331 <[email protected]> Co-authored-by: JustaSqu1d <[email protected]> (cherry picked from commit a5aa21f)
1 parent bbed889 commit 9a5554f

33 files changed

+2757
-165
lines changed

discord/abc.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1545,6 +1545,12 @@ async def send(
15451545
raise InvalidArgument(f"view parameter must be View not {view.__class__!r}")
15461546

15471547
components = view.to_components()
1548+
if view.is_components_v2():
1549+
if embeds or content:
1550+
raise TypeError(
1551+
"cannot send embeds or content with a view using v2 component logic"
1552+
)
1553+
flags.is_components_v2 = True
15481554
else:
15491555
components = None
15501556

@@ -1605,8 +1611,10 @@ async def send(
16051611

16061612
ret = state.create_message(channel=channel, data=data)
16071613
if view:
1608-
state.store_view(view, ret.id)
1614+
if view.is_dispatchable():
1615+
state.store_view(view, ret.id)
16091616
view.message = ret
1617+
view.refresh(ret.components)
16101618

16111619
if delete_after is not None:
16121620
await ret.delete(delay=delete_after)

discord/bot.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -803,13 +803,13 @@ async def process_application_commands(self, interaction: Interaction, auto_sync
803803

804804
ctx = await self.get_application_context(interaction)
805805
if command:
806-
ctx.command = command
806+
interaction.command = command
807807
await self.invoke_application_command(ctx)
808808

809809
async def on_application_command_auto_complete(self, interaction: Interaction, command: ApplicationCommand) -> None:
810810
async def callback() -> None:
811811
ctx = await self.get_autocomplete_context(interaction)
812-
ctx.command = command
812+
interaction.command = command
813813
return await command.invoke_autocomplete_callback(ctx)
814814

815815
autocomplete_task = self._bot.loop.create_task(callback())

discord/channel.py

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,16 @@
2626
from __future__ import annotations
2727

2828
import datetime
29-
from typing import TYPE_CHECKING, Any, Callable, Iterable, Mapping, TypeVar, overload
29+
from typing import (
30+
TYPE_CHECKING,
31+
Any,
32+
Callable,
33+
Iterable,
34+
Mapping,
35+
Sequence,
36+
TypeVar,
37+
overload,
38+
)
3039

3140
import discord.abc
3241

@@ -45,7 +54,7 @@
4554
)
4655
from .errors import ClientException, InvalidArgument
4756
from .file import File
48-
from .flags import ChannelFlags
57+
from .flags import ChannelFlags, MessageFlags
4958
from .invite import Invite
5059
from .iterators import ArchivedThreadIterator
5160
from .mixins import Hashable
@@ -71,12 +80,15 @@
7180

7281
if TYPE_CHECKING:
7382
from .abc import Snowflake, SnowflakeTime
83+
from .embeds import Embed
7484
from .guild import Guild
7585
from .guild import GuildChannel as GuildChannelType
7686
from .member import Member, VoiceState
87+
from .mentions import AllowedMentions
7788
from .message import EmojiInputType, Message, PartialMessage
7889
from .role import Role
7990
from .state import ConnectionState
91+
from .sticker import GuildSticker, StickerItem
8092
from .types.channel import CategoryChannel as CategoryChannelPayload
8193
from .types.channel import DMChannel as DMChannelPayload
8294
from .types.channel import ForumChannel as ForumChannelPayload
@@ -87,6 +99,7 @@
8799
from .types.channel import VoiceChannel as VoiceChannelPayload
88100
from .types.snowflake import SnowflakeList
89101
from .types.threads import ThreadArchiveDuration
102+
from .ui.view import View
90103
from .user import BaseUser, ClientUser, User
91104
from .webhook import Webhook
92105

@@ -1137,18 +1150,20 @@ async def edit(self, *, reason=None, **options):
11371150
async def create_thread(
11381151
self,
11391152
name: str,
1140-
content=None,
1153+
content: str | None = None,
11411154
*,
1142-
embed=None,
1143-
embeds=None,
1144-
file=None,
1145-
files=None,
1146-
stickers=None,
1147-
delete_message_after=None,
1148-
nonce=None,
1149-
allowed_mentions=None,
1150-
view=None,
1151-
applied_tags=None,
1155+
embed: Embed | None = None,
1156+
embeds: list[Embed] | None = None,
1157+
file: File | None = None,
1158+
files: list[File] | None = None,
1159+
stickers: Sequence[GuildSticker | StickerItem] | None = None,
1160+
delete_message_after: float | None = None,
1161+
nonce: int | str | None = None,
1162+
allowed_mentions: AllowedMentions | None = None,
1163+
view: View | None = None,
1164+
applied_tags: list[ForumTag] | None = None,
1165+
suppress: bool = False,
1166+
silent: bool = False,
11521167
auto_archive_duration: ThreadArchiveDuration | utils.Undefined = MISSING,
11531168
slowmode_delay: int | utils.Undefined = MISSING,
11541169
reason: str | None = None,
@@ -1242,11 +1257,22 @@ async def create_thread(
12421257
else:
12431258
allowed_mentions = allowed_mentions.to_dict()
12441259

1260+
flags = MessageFlags(
1261+
suppress_embeds=bool(suppress),
1262+
suppress_notifications=bool(silent),
1263+
)
1264+
12451265
if view:
12461266
if not hasattr(view, "__discord_ui_view__"):
12471267
raise InvalidArgument(f"view parameter must be View not {view.__class__!r}")
12481268

12491269
components = view.to_components()
1270+
if view.is_components_v2():
1271+
if embeds or content:
1272+
raise TypeError(
1273+
"cannot send embeds or content with a view using v2 component logic"
1274+
)
1275+
flags.is_components_v2 = True
12501276
else:
12511277
components = None
12521278

@@ -1282,6 +1308,7 @@ async def create_thread(
12821308
auto_archive_duration=auto_archive_duration or self.default_auto_archive_duration,
12831309
rate_limit_per_user=slowmode_delay or self.slowmode_delay,
12841310
applied_tags=applied_tags,
1311+
flags=flags.value,
12851312
reason=reason,
12861313
)
12871314
finally:
@@ -1291,7 +1318,7 @@ async def create_thread(
12911318

12921319
ret = Thread(guild=self.guild, state=self._state, data=data)
12931320
msg = ret.get_partial_message(int(data["last_message_id"]))
1294-
if view:
1321+
if view and view.is_dispatchable():
12951322
state.store_view(view, msg.id)
12961323

12971324
if delete_message_after is not None:

discord/client.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,11 @@
7070
if TYPE_CHECKING:
7171
from .abc import GuildChannel, PrivateChannel, Snowflake, SnowflakeTime
7272
from .channel import DMChannel
73+
from .interaction import Interaction
7374
from .member import Member
7475
from .message import Message
7576
from .poll import Poll
77+
from .ui.item import Item
7678
from .voice_client import VoiceProtocol
7779

7880
__all__ = ("Client",)
@@ -538,6 +540,38 @@ async def on_error(self, event_method: str, *args: Any, **kwargs: Any) -> None:
538540
print(f"Ignoring exception in {event_method}", file=sys.stderr)
539541
traceback.print_exc()
540542

543+
async def on_view_error(
544+
self, error: Exception, item: Item, interaction: Interaction
545+
) -> None:
546+
"""|coro|
547+
548+
The default view error handler provided by the client.
549+
550+
This only fires for a view if you did not define its :func:`~discord.ui.View.on_error`.
551+
"""
552+
553+
print(
554+
f"Ignoring exception in view {interaction.view} for item {item}:",
555+
file=sys.stderr,
556+
)
557+
traceback.print_exception(
558+
error.__class__, error, error.__traceback__, file=sys.stderr
559+
)
560+
561+
async def on_modal_error(self, error: Exception, interaction: Interaction) -> None:
562+
"""|coro|
563+
564+
The default modal error handler provided by the client.
565+
The default implementation prints the traceback to stderr.
566+
567+
This only fires for a modal if you did not define its :func:`~discord.ui.Modal.on_error`.
568+
"""
569+
570+
print(f"Ignoring exception in modal {interaction.modal}:", file=sys.stderr)
571+
traceback.print_exception(
572+
error.__class__, error, error.__traceback__, file=sys.stderr
573+
)
574+
541575
# hooks
542576

543577
async def _call_before_identify_hook(self, shard_id: int | None, *, initial: bool = False) -> None:

discord/commands/context.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,13 @@ class ApplicationContext(discord.abc.Messageable):
8080
The bot that the command belongs to.
8181
interaction: :class:`.Interaction`
8282
The interaction object that invoked the command.
83-
command: :class:`.ApplicationCommand`
84-
The command that this context belongs to.
8583
"""
8684

8785
def __init__(self, bot: Bot, interaction: Interaction):
8886
self.bot = bot
8987
self.interaction = interaction
9088

9189
# below attributes will be set after initialization
92-
self.command: ApplicationCommand = None # type: ignore
9390
self.focused: Option = None # type: ignore
9491
self.value: str = None # type: ignore
9592
self.options: dict = None # type: ignore
@@ -136,6 +133,15 @@ async def invoke(
136133
"""
137134
return await command(self, *args, **kwargs)
138135

136+
@property
137+
def command(self) -> ApplicationCommand | None:
138+
"""The command that this context belongs to."""
139+
return self.interaction.command
140+
141+
@command.setter
142+
def command(self, value: ApplicationCommand | None) -> None:
143+
self.interaction.command = value
144+
139145
@cached_property
140146
def channel(self) -> InteractionChannel | None:
141147
"""Union[:class:`abc.GuildChannel`, :class:`PartialMessageable`, :class:`Thread`]:
@@ -384,8 +390,6 @@ class AutocompleteContext:
384390
The bot that the command belongs to.
385391
interaction: :class:`.Interaction`
386392
The interaction object that invoked the autocomplete.
387-
command: :class:`.ApplicationCommand`
388-
The command that this context belongs to.
389393
focused: :class:`.Option`
390394
The option the user is currently typing.
391395
value: :class:`.str`
@@ -394,13 +398,12 @@ class AutocompleteContext:
394398
A name to value mapping of the options that the user has selected before this option.
395399
"""
396400

397-
__slots__ = ("bot", "interaction", "command", "focused", "value", "options")
401+
__slots__ = ("bot", "interaction", "focused", "value", "options")
398402

399403
def __init__(self, bot: Bot, interaction: Interaction):
400404
self.bot = bot
401405
self.interaction = interaction
402406

403-
self.command: ApplicationCommand = None # type: ignore
404407
self.focused: Option = None # type: ignore
405408
self.value: str = None # type: ignore
406409
self.options: dict = None # type: ignore
@@ -414,3 +417,12 @@ def cog(self) -> Cog | None:
414417
return None
415418

416419
return self.command.cog
420+
421+
@property
422+
def command(self) -> ApplicationCommand | None:
423+
"""The command that this context belongs to."""
424+
return self.interaction.command
425+
426+
@command.setter
427+
def command(self, value: ApplicationCommand | None) -> None:
428+
self.interaction.command = value

0 commit comments

Comments
 (0)