From a16fc23c00a11b53d05cfb0005ce5d710387abd8 Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 5 Dec 2025 10:29:18 +0200 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=20=E2=99=BB=EF=B8=8F=20Handle=20dis?= =?UTF-8?q?cord.HTTPException=20during=20message=20edits=20in=20Paginator?= =?UTF-8?q?=20and=20BaseView?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- discord/ext/pages/pagination.py | 26 +++++++++++++++----------- discord/ui/view.py | 31 ++++++++++++++++++------------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/discord/ext/pages/pagination.py b/discord/ext/pages/pagination.py index 8dbf1c0f43..59a1158472 100644 --- a/discord/ext/pages/pagination.py +++ b/discord/ext/pages/pagination.py @@ -24,6 +24,7 @@ from __future__ import annotations +import contextlib from typing import List import discord @@ -456,7 +457,8 @@ def __init__( async def update( self, - pages: None | ( + pages: None + | ( list[PageGroup] | list[Page] | list[str] @@ -527,7 +529,7 @@ async def update( | list[str] | list[Page] | list[list[discord.Embed] | discord.Embed] - ) = (pages if pages is not None else self.pages) + ) = pages if pages is not None else self.pages self.show_menu = show_menu if show_menu is not None else self.show_menu if pages is not None and all(isinstance(pg, PageGroup) for pg in pages): self.page_groups = self.pages if self.show_menu else None @@ -597,11 +599,12 @@ async def on_timeout(self) -> None: page = self.pages[self.current_page] page = self.get_page_content(page) files = page.update_files() - await self.message.edit( - view=self, - files=files or [], - attachments=[], - ) + async with contextlib.suppress(discord.HTTPException): + await self.message.edit( + view=self, + files=files or [], + attachments=[], + ) async def disable( self, @@ -709,7 +712,9 @@ async def goto_page( try: if interaction: - await interaction.response.defer() # needed to force webhook message edit route for files kwarg support + await ( + interaction.response.defer() + ) # needed to force webhook message edit route for files kwarg support await interaction.followup.edit_message( message_id=self.message.id, content=page.content, @@ -952,9 +957,8 @@ async def send( ctx: Context, target: discord.abc.Messageable | None = None, target_message: str | None = None, - reference: None | ( - discord.Message | discord.MessageReference | discord.PartialMessage - ) = None, + reference: None + | (discord.Message | discord.MessageReference | discord.PartialMessage) = None, allowed_mentions: discord.AllowedMentions | None = None, mention_author: bool | None = None, delete_after: float | None = None, diff --git a/discord/ui/view.py b/discord/ui/view.py index 9d03339d09..07d5154469 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -26,6 +26,7 @@ from __future__ import annotations import asyncio +import contextlib import os import sys import time @@ -42,11 +43,12 @@ from typing_extensions import Self +import discord + from ..components import ActionRow as ActionRowComponent from ..components import Button as ButtonComponent -from ..components import Component +from ..components import Component, FileComponent, _component_factory from ..components import Container as ContainerComponent -from ..components import FileComponent from ..components import Label as LabelComponent from ..components import MediaGallery as MediaGalleryComponent from ..components import Section as SectionComponent @@ -54,8 +56,6 @@ from ..components import Separator as SeparatorComponent from ..components import TextDisplay as TextDisplayComponent from ..components import Thumbnail as ThumbnailComponent -from ..components import _component_factory -from ..utils import find from .core import ItemInterface from .item import ItemCallbackType, ViewItem @@ -96,7 +96,6 @@ def _walk_all_components_v2(components: list[Component]) -> Iterator[Component]: def _component_to_item(component: Component) -> ViewItem[V]: - if isinstance(component, ButtonComponent): from .button import Button @@ -309,7 +308,8 @@ async def on_timeout(self) -> None: message = self.message if message: - m = await message.edit(view=self) + async with contextlib.suppress(discord.HTTPException): + m = await message.edit(view=self) if m: self._message = m @@ -681,7 +681,7 @@ def add_item(self, item: ViewItem[V]) -> Self: if item._underlying.is_v2(): raise ValueError( - f"cannot use V2 components in View. Use DesignerView instead." + "cannot use V2 components in View. Use DesignerView instead." ) if isinstance(item._underlying, ActionRowComponent): for i in item.children: @@ -718,7 +718,9 @@ def clear_items(self) -> None: def refresh(self, components: list[Component]): # This is pretty hacky at the moment old_state: dict[tuple[int, str], ViewItem[V]] = { - (item.type.value, item.custom_id): item for item in self.children if item.is_dispatchable() # type: ignore + (item.type.value, item.custom_id): item + for item in self.children + if item.is_dispatchable() # type: ignore } children: list[ViewItem[V]] = [ item for item in self.children if not item.is_dispatchable() @@ -878,7 +880,7 @@ def add_item(self, item: ViewItem[V]) -> Self: if isinstance(item._underlying, (SelectComponent, ButtonComponent)): raise ValueError( - f"cannot add Select or Button to DesignerView directly. Use ActionRow instead." + "cannot add Select or Button to DesignerView directly. Use ActionRow instead." ) super().add_item(item) @@ -909,9 +911,9 @@ def is_components_v2(self) -> bool: class ViewStore: def __init__(self, state: ConnectionState): # (component_type, message_id, custom_id): (BaseView, ViewItem) - self._views: dict[tuple[int, int | None, str], tuple[BaseView, ViewItem[V]]] = ( - {} - ) + self._views: dict[ + tuple[int, int | None, str], tuple[BaseView, ViewItem[V]] + ] = {} # message_id: View self._synced_message_views: dict[int, BaseView] = {} self._state: ConnectionState = state @@ -942,7 +944,10 @@ def add_view(self, view: BaseView, message_id: int | None = None): view._start_listening_from_store(self) for item in view.walk_children(): if item.is_storable(): - self._views[(item.type.value, message_id, item.custom_id)] = (view, item) # type: ignore + self._views[(item.type.value, message_id, item.custom_id)] = ( + view, + item, + ) # type: ignore if message_id is not None: self._synced_message_views[message_id] = view From c09094a59f0adf94e777505d1b6b54f280cf887b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 5 Dec 2025 08:32:36 +0000 Subject: [PATCH 2/3] style(pre-commit): auto fixes from pre-commit.com hooks --- discord/ext/pages/pagination.py | 10 +++++----- discord/ui/view.py | 10 ++++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/discord/ext/pages/pagination.py b/discord/ext/pages/pagination.py index 59a1158472..5db5c80fa4 100644 --- a/discord/ext/pages/pagination.py +++ b/discord/ext/pages/pagination.py @@ -457,8 +457,7 @@ def __init__( async def update( self, - pages: None - | ( + pages: None | ( list[PageGroup] | list[Page] | list[str] @@ -529,7 +528,7 @@ async def update( | list[str] | list[Page] | list[list[discord.Embed] | discord.Embed] - ) = pages if pages is not None else self.pages + ) = (pages if pages is not None else self.pages) self.show_menu = show_menu if show_menu is not None else self.show_menu if pages is not None and all(isinstance(pg, PageGroup) for pg in pages): self.page_groups = self.pages if self.show_menu else None @@ -957,8 +956,9 @@ async def send( ctx: Context, target: discord.abc.Messageable | None = None, target_message: str | None = None, - reference: None - | (discord.Message | discord.MessageReference | discord.PartialMessage) = None, + reference: None | ( + discord.Message | discord.MessageReference | discord.PartialMessage + ) = None, allowed_mentions: discord.AllowedMentions | None = None, mention_author: bool | None = None, delete_after: float | None = None, diff --git a/discord/ui/view.py b/discord/ui/view.py index 07d5154469..ab2224e347 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -47,8 +47,9 @@ from ..components import ActionRow as ActionRowComponent from ..components import Button as ButtonComponent -from ..components import Component, FileComponent, _component_factory +from ..components import Component from ..components import Container as ContainerComponent +from ..components import FileComponent from ..components import Label as LabelComponent from ..components import MediaGallery as MediaGalleryComponent from ..components import Section as SectionComponent @@ -56,6 +57,7 @@ from ..components import Separator as SeparatorComponent from ..components import TextDisplay as TextDisplayComponent from ..components import Thumbnail as ThumbnailComponent +from ..components import _component_factory from .core import ItemInterface from .item import ItemCallbackType, ViewItem @@ -911,9 +913,9 @@ def is_components_v2(self) -> bool: class ViewStore: def __init__(self, state: ConnectionState): # (component_type, message_id, custom_id): (BaseView, ViewItem) - self._views: dict[ - tuple[int, int | None, str], tuple[BaseView, ViewItem[V]] - ] = {} + self._views: dict[tuple[int, int | None, str], tuple[BaseView, ViewItem[V]]] = ( + {} + ) # message_id: View self._synced_message_views: dict[int, BaseView] = {} self._state: ConnectionState = state From a198b8d89f49d603b121e1bc2a9df0f36591ff1c Mon Sep 17 00:00:00 2001 From: Lumouille <144063653+Lumabots@users.noreply.github.com> Date: Fri, 5 Dec 2025 10:33:36 +0200 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=20=E2=99=BB=EF=B8=8F=20Update=20cha?= =?UTF-8?q?ngelog=20to=20reflect=20changes=20in=20BaseView=20and=20Paginat?= =?UTF-8?q?or=20timeout=20behavior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5696f04d6..49122efaf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ These changes are available on the `master` branch, but have not yet been releas ### Changed +- Changed `BaseView.on_timeout` and `Paginator.on_timeout` behavior: views no longer + raise errors on timeout. + ([#3019](https://github.com/Pycord-Development/pycord/pull/3019)) + ### Fixed - Fixed breaking change in `ui.Select` Generic typing by adding default values to