diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index 4946e6da6..0c59b929f 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -203,6 +203,9 @@ class ActionRow(Item[V]): def _swap_item(self, base: Item, new: DynamicItem, custom_id: str) -> None: child_index = self._children.index(base) self._children[child_index] = new # type: ignore + base._detach_view() + new._update_view(self.view) + new._parent = self @property def width(self): @@ -299,6 +302,7 @@ class ActionRow(Item[V]): if self._view: self._view._add_count(-1) self._weight -= item.width + item._detach_view() return self @@ -330,6 +334,8 @@ class ActionRow(Item[V]): """ if self._view: self._view._add_count(-len(self._children)) + for item in self._children: + item._detach_view() self._children.clear() self._weight = 0 return self diff --git a/discord/ui/container.py b/discord/ui/container.py index 11aa7c0e6..b88b188a4 100644 --- a/discord/ui/container.py +++ b/discord/ui/container.py @@ -197,6 +197,9 @@ class Container(Item[V]): def _swap_item(self, base: Item, new: DynamicItem, custom_id: str) -> None: child_index = self._children.index(base) self._children[child_index] = new # type: ignore + base._detach_view() + new._update_view(self.view) + new._parent = self @property def children(self) -> List[Item[V]]: @@ -336,6 +339,7 @@ class Container(Item[V]): else: if self._view: self._view._add_count(-item._total_count) + item._detach_view() return self def find_item(self, id: int, /) -> Optional[Item[V]]: @@ -367,5 +371,7 @@ class Container(Item[V]): if self._view: self._view._add_count(-len(tuple(self.walk_children()))) + for item in self._children: + item._detach_view() self._children.clear() return self diff --git a/discord/ui/item.py b/discord/ui/item.py index c6f165d5c..2c1b48be0 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -206,6 +206,10 @@ class Item(Generic[V]): def _update_view(self, view) -> None: self._view = view + def _detach_view(self) -> None: + self._update_view(None) + self._parent = None + def copy(self) -> Self: return copy.deepcopy(self) diff --git a/discord/ui/section.py b/discord/ui/section.py index ff687e0be..bca00c08a 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -212,6 +212,7 @@ class Section(Item[V]): else: if self._view: self._view._add_count(-1) + item._detach_view() return self @@ -244,6 +245,8 @@ class Section(Item[V]): if self._view: self._view._add_count(-len(self._children)) # we don't count the accessory because it is required + for item in self._children: + item._detach_view() self._children.clear() return self diff --git a/tests/test_ui_view.py b/tests/test_ui_view.py index 57b17a6dc..933323b2c 100644 --- a/tests/test_ui_view.py +++ b/tests/test_ui_view.py @@ -28,6 +28,12 @@ import discord import pytest +class DynamicButton(discord.ui.DynamicItem[discord.ui.Button], template=r'dynamic:(?P[0-9]+)'): + @classmethod + async def from_custom_id(cls, interaction, item, match): + return cls(discord.ui.Button(label='Dynamic', custom_id=match.group(0))) + + def test_add_item_with_full_row(): view = discord.ui.View() @@ -86,3 +92,91 @@ def test_layout_view_add_item_with_too_many_children(): assert view.total_children_count == max_item_limit - 1 assert row.view is None assert all(item.view is None for item in row.children) + + +def test_section_remove_item_clears_view_and_parent(): + view = discord.ui.LayoutView() + section = discord.ui.Section('test', accessory=discord.ui.Button(label='Test')) + view.add_item(section) + item = section.children[0] + + section.remove_item(item) + + assert item.view is None + assert item.parent is None + + +def test_action_row_remove_item_clears_view_and_parent(): + view = discord.ui.LayoutView() + row = discord.ui.ActionRow() + item = discord.ui.Button(label='Test') + row.add_item(item) + view.add_item(row) + + row.remove_item(item) + + assert item.view is None + assert item.parent is None + + +def test_container_remove_item_clears_view_and_parent(): + view = discord.ui.LayoutView() + container = discord.ui.Container() + item = discord.ui.TextDisplay('test') + container.add_item(item) + view.add_item(container) + + container.remove_item(item) + + assert item.view is None + assert item.parent is None + + +@pytest.mark.asyncio +async def test_action_row_dynamic_item_swap_preserves_parent_checks(): + calls = [] + + class Row(discord.ui.ActionRow): + async def interaction_check(self, interaction): + calls.append('row') + return True + + base = discord.ui.Button(label='Dynamic', custom_id='dynamic:1') + row = Row(base) + view = discord.ui.LayoutView() + view.add_item(row) + item = DynamicButton(discord.ui.Button(label='Dynamic', custom_id='dynamic:1')) + + row._swap_item(base, item, 'dynamic:1') + await item._run_checks(None) + + assert item.view is view + assert item.parent is row + assert base.view is None + assert base.parent is None + assert calls == ['row'] + + +@pytest.mark.asyncio +async def test_container_dynamic_item_swap_preserves_parent_checks(): + calls = [] + + class Container(discord.ui.Container): + async def interaction_check(self, interaction): + calls.append('container') + return True + + base = discord.ui.ActionRow(discord.ui.Button(label='Dynamic', custom_id='dynamic:1')) + container = Container(base) + view = discord.ui.LayoutView() + view.add_item(container) + item = DynamicButton(discord.ui.Button(label='Dynamic', custom_id='dynamic:1')) + + container._swap_item(base, item, 'dynamic:1') + await item._run_checks(None) + + assert item.view is view + assert item.parent is container + assert base.view is None + assert base.parent is None + assert calls == ['container']