Fix nested UI item parent state

This commit is contained in:
harumaki4649
2026-07-10 19:37:41 -04:00
committed by GitHub
parent f6dbb848d0
commit d2996be144
5 changed files with 113 additions and 0 deletions
+6
View File
@@ -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
+6
View File
@@ -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
+4
View File
@@ -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)
+3
View File
@@ -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
+94
View File
@@ -28,6 +28,12 @@ import discord
import pytest
class DynamicButton(discord.ui.DynamicItem[discord.ui.Button], template=r'dynamic:(?P<number>[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']