Skip to content

Commit e2cf721

Browse files
authored
Fix container items having out of date internal state
1 parent 8f2cb60 commit e2cf721

File tree

6 files changed

+68
-54
lines changed

6 files changed

+68
-54
lines changed

discord/ui/action_row.py

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@
2424

2525
from __future__ import annotations
2626

27+
import copy
2728
from typing import (
2829
TYPE_CHECKING,
2930
Any,
3031
Callable,
3132
ClassVar,
32-
Coroutine,
3333
Dict,
3434
Generator,
3535
List,
@@ -42,7 +42,7 @@
4242
overload,
4343
)
4444

45-
from .item import Item, ContainedItemCallbackType as ItemCallbackType
45+
from .item import Item, ContainedItemCallbackType as ItemCallbackType, _ItemCallback
4646
from .button import Button, button as _button
4747
from .select import select as _select, Select, UserSelect, RoleSelect, ChannelSelect, MentionableSelect
4848
from ..components import ActionRow as ActionRowComponent
@@ -65,7 +65,6 @@
6565
)
6666
from ..emoji import Emoji
6767
from ..components import SelectOption
68-
from ..interactions import Interaction
6968
from .container import Container
7069
from .dynamic import DynamicItem
7170

@@ -77,18 +76,6 @@
7776
__all__ = ('ActionRow',)
7877

7978

80-
class _ActionRowCallback:
81-
__slots__ = ('row', 'callback', 'item')
82-
83-
def __init__(self, callback: ItemCallbackType[S, Any], row: ActionRow, item: Item[Any]) -> None:
84-
self.callback: ItemCallbackType[Any, Any] = callback
85-
self.row: ActionRow = row
86-
self.item: Item[Any] = item
87-
88-
def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]:
89-
return self.callback(self.row, interaction, self.item)
90-
91-
9279
class ActionRow(Item[V]):
9380
r"""Represents a UI action row.
9481
@@ -143,8 +130,9 @@ def __init__(
143130
) -> None:
144131
super().__init__()
145132
self._children: List[Item[V]] = self._init_children()
146-
self._children.extend(children)
147133
self._weight: int = sum(i.width for i in self._children)
134+
for child in children:
135+
self.add_item(child)
148136

149137
if self._weight > 5:
150138
raise ValueError('maximum number of children exceeded')
@@ -173,8 +161,8 @@ def _init_children(self) -> List[Item[Any]]:
173161

174162
for func in self.__action_row_children_items__:
175163
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
176-
item.callback = _ActionRowCallback(func, self, item) # type: ignore
177-
item._parent = getattr(func, '__discord_ui_parent__', self)
164+
item.callback = _ItemCallback(func, self, item) # type: ignore
165+
item._parent = self
178166
setattr(self, func.__name__, item)
179167
children.append(item)
180168
return children
@@ -184,6 +172,23 @@ def _update_view(self, view) -> None:
184172
for child in self._children:
185173
child._view = view
186174

175+
def copy(self) -> ActionRow[V]:
176+
new = copy.copy(self)
177+
children = []
178+
for child in new._children:
179+
newch = child.copy()
180+
newch._parent = new
181+
if isinstance(newch.callback, _ItemCallback):
182+
newch.callback.parent = new
183+
children.append(newch)
184+
new._children = children
185+
new._parent = self._parent
186+
new._update_view(self.view)
187+
return new
188+
189+
def __deepcopy__(self, memo) -> ActionRow[V]:
190+
return self.copy()
191+
187192
def _has_children(self):
188193
return True
189194

discord/ui/button.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import os
3131

3232

33-
from .item import Item, ContainedItemCallbackType as ItemCallbackType
33+
from .item import Item, ContainedItemCallbackType as ItemCallbackType, _ItemCallback
3434
from ..enums import ButtonStyle, ComponentType
3535
from ..partial_emoji import PartialEmoji, _EmojiTag
3636
from ..components import Button as ButtonComponent
@@ -304,6 +304,9 @@ def copy(self) -> Self:
304304
sku_id=self.sku_id,
305305
id=self.id,
306306
)
307+
if isinstance(new.callback, _ItemCallback):
308+
new.callback.item = new
309+
new._update_view(self.view)
307310
return new
308311

309312
def __deepcopy__(self, memo) -> Self:

discord/ui/container.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
TYPE_CHECKING,
3030
Any,
3131
ClassVar,
32-
Coroutine,
3332
Dict,
3433
Generator,
3534
List,
@@ -39,7 +38,7 @@
3938
Union,
4039
)
4140

42-
from .item import Item, ContainedItemCallbackType as ItemCallbackType
41+
from .item import Item, ContainedItemCallbackType as ItemCallbackType, _ItemCallback
4342
from .view import _component_to_item, LayoutView
4443
from ..enums import ComponentType
4544
from ..utils import get as _utils_get
@@ -49,7 +48,6 @@
4948
from typing_extensions import Self
5049

5150
from ..components import Container as ContainerComponent
52-
from ..interactions import Interaction
5351
from .dynamic import DynamicItem
5452

5553
S = TypeVar('S', bound='Container', covariant=True)
@@ -58,18 +56,6 @@
5856
__all__ = ('Container',)
5957

6058

61-
class _ContainerCallback:
62-
__slots__ = ('container', 'callback', 'item')
63-
64-
def __init__(self, callback: ItemCallbackType[S, Any], container: Container, item: Item[Any]) -> None:
65-
self.callback: ItemCallbackType[Any, Any] = callback
66-
self.container: Container = container
67-
self.item: Item[Any] = item
68-
69-
def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]:
70-
return self.callback(self.container, interaction, self.item)
71-
72-
7359
class Container(Item[V]):
7460
r"""Represents a UI container.
7561
@@ -163,7 +149,7 @@ def _init_children(self) -> List[Item[Any]]:
163149
# action rows can be created inside containers, and then callbacks can exist here
164150
# so we create items based off them
165151
item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__)
166-
item.callback = _ContainerCallback(raw, self, item) # type: ignore
152+
item.callback = _ItemCallback(raw, self, item) # type: ignore
167153
setattr(self, raw.__name__, item)
168154
# this should not fail because in order for a function to be here it should be from
169155
# an action row and must have passed the check in __init_subclass__, but still
@@ -196,6 +182,15 @@ def _update_view(self, view) -> bool:
196182
child._update_view(view)
197183
return True
198184

185+
def copy(self) -> Container[V]:
186+
new = copy.deepcopy(self)
187+
for child in new._children:
188+
newch = child.copy()
189+
newch._parent = new
190+
new._parent = self._parent
191+
new._update_view(self.view)
192+
return new
193+
199194
def _has_children(self):
200195
return True
201196

discord/ui/item.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@
5555
ContainedItemCallbackType = Callable[[C, Interaction[Any], I], Coroutine[Any, Any, Any]]
5656

5757

58+
class _ItemCallback:
59+
__slots__ = ('parent', 'callback', 'item')
60+
61+
def __init__(self, callback: ContainedItemCallbackType[Any, Any], parent: Any, item: Item[Any]) -> None:
62+
self.callback: ItemCallbackType[Any, Any] = callback
63+
self.parent: Any = parent
64+
self.item: Item[Any] = item
65+
66+
def __repr__(self) -> str:
67+
return f'<ItemCallback callback={self.callback!r} parent={self.parent!r} item={self.item!r}>'
68+
69+
def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]:
70+
return self.callback(self.parent, interaction, self.item)
71+
72+
5873
class Item(Generic[V]):
5974
"""Represents the base UI item that all UI components inherit from.
6075

discord/ui/select.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@
3939
Sequence,
4040
)
4141
from contextvars import ContextVar
42+
import copy
4243
import inspect
4344
import os
4445

45-
from .item import Item, ContainedItemCallbackType as ItemCallbackType
46+
from .item import Item, ContainedItemCallbackType as ItemCallbackType, _ItemCallback
4647
from ..enums import ChannelType, ComponentType, SelectDefaultValueType
4748
from ..partial_emoji import PartialEmoji
4849
from ..emoji import Emoji
@@ -70,7 +71,7 @@
7071
)
7172

7273
if TYPE_CHECKING:
73-
from typing_extensions import TypeAlias, TypeGuard
74+
from typing_extensions import TypeAlias, TypeGuard, Self
7475

7576
from .view import BaseView
7677
from .action_row import ActionRow
@@ -269,6 +270,14 @@ def __init__(
269270
self.row = row
270271
self._values: List[PossibleValue] = []
271272

273+
def copy(self) -> Self:
274+
new = copy.copy(self)
275+
if isinstance(new.callback, _ItemCallback):
276+
new.callback.item = new
277+
new._parent = self._parent
278+
new._update_view(self.view)
279+
return new
280+
272281
@property
273282
def id(self) -> Optional[int]:
274283
"""Optional[:class:`int`]: The ID of this select."""

discord/ui/view.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
Any,
2929
Callable,
3030
ClassVar,
31-
Coroutine,
3231
Dict,
3332
Generator,
3433
Iterator,
@@ -50,7 +49,7 @@
5049
import time
5150
import os
5251

53-
from .item import Item, ItemCallbackType
52+
from .item import Item, ItemCallbackType, _ItemCallback
5453
from .select import Select
5554
from .dynamic import DynamicItem
5655
from ..components import (
@@ -207,18 +206,6 @@ def clear(self) -> None:
207206
self.weights = [0, 0, 0, 0, 0]
208207

209208

210-
class _ViewCallback:
211-
__slots__ = ('view', 'callback', 'item')
212-
213-
def __init__(self, callback: ItemCallbackType[Any, Any], view: BaseView, item: Item[BaseView]) -> None:
214-
self.callback: ItemCallbackType[Any, Any] = callback
215-
self.view: BaseView = view
216-
self.item: Item[BaseView] = item
217-
218-
def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]:
219-
return self.callback(self.view, interaction, self.item)
220-
221-
222209
class BaseView:
223210
__discord_ui_view__: ClassVar[bool] = False
224211
__discord_ui_modal__: ClassVar[bool] = False
@@ -252,13 +239,13 @@ def _init_children(self) -> List[Item[Self]]:
252239
item._update_view(self)
253240
parent = getattr(item, '__discord_ui_parent__', None)
254241
if parent and parent._view is None:
255-
parent._view = self
242+
parent._update_view(self)
256243
children.append(item)
257244
parents[raw] = item
258245
else:
259246
item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__)
260-
item.callback = _ViewCallback(raw, self, item) # type: ignore
261-
item._view = self
247+
item.callback = _ItemCallback(raw, self, item) # type: ignore
248+
item._update_view(self)
262249
if isinstance(item, Select):
263250
item.options = [option.copy() for option in item.options]
264251
setattr(self, raw.__name__, item)

0 commit comments

Comments
 (0)