From 905ca749fa3437730f6227affe4202ceb2fe5c9b Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sun, 3 Apr 2022 19:23:57 -0400 Subject: [PATCH] Change View.children to be a property This allows users to call remove_item in a loop. Likewise, it prevents the footgun of doing children.append(...) which does not uphold the invariants with the weight system. --- discord/ui/modal.py | 2 +- discord/ui/view.py | 36 ++++++++++++++++++------------------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/discord/ui/modal.py b/discord/ui/modal.py index ceb8932f98a1..f43bdd522cfb 100644 --- a/discord/ui/modal.py +++ b/discord/ui/modal.py @@ -172,7 +172,7 @@ def _refresh(self, components: Sequence[ModalSubmitComponentInteractionDataPaylo if component['type'] == 1: self._refresh(component['components']) else: - item = find(lambda i: i.custom_id == component['custom_id'], self.children) # type: ignore + item = find(lambda i: i.custom_id == component['custom_id'], self._children) # type: ignore if item is None: _log.debug("Modal interaction referencing unknown item custom_id %s. Discarding", component['custom_id']) continue diff --git a/discord/ui/view.py b/discord/ui/view.py index b08b9a773ca2..4d5429597b9f 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -151,11 +151,6 @@ class View: timeout: Optional[:class:`float`] Timeout in seconds from last interaction with the UI before no longer accepting input. If ``None`` then there is no timeout. - - Attributes - ------------ - children: List[:class:`Item`] - The list of children attached to this view. """ __discord_ui_view__: ClassVar[bool] = True @@ -186,8 +181,8 @@ def _init_children(self) -> List[Item[Self]]: def __init__(self, *, timeout: Optional[float] = 180.0): self.__timeout = timeout - self.children: List[Item[Self]] = self._init_children() - self.__weights = _ViewWeights(self.children) + self._children: List[Item[Self]] = self._init_children() + self.__weights = _ViewWeights(self._children) self.id: str = os.urandom(16).hex() self.__cancel_callback: Optional[Callable[[View], None]] = None self.__timeout_expiry: Optional[float] = None @@ -195,7 +190,7 @@ def __init__(self, *, timeout: Optional[float] = 180.0): self.__stopped: asyncio.Future[bool] = asyncio.get_running_loop().create_future() def __repr__(self) -> str: - return f'<{self.__class__.__name__} timeout={self.timeout} children={len(self.children)}>' + return f'<{self.__class__.__name__} timeout={self.timeout} children={len(self._children)}>' async def __timeout_task_impl(self) -> None: while True: @@ -218,7 +213,7 @@ def to_components(self) -> List[Dict[str, Any]]: def key(item: Item) -> int: return item._rendered_row or 0 - children = sorted(self.children, key=key) + children = sorted(self._children, key=key) components: List[Dict[str, Any]] = [] for _, group in groupby(children, key=key): children = [item.to_component_dict() for item in group] @@ -257,6 +252,11 @@ def timeout(self, value: Optional[float]) -> None: self.__timeout = value + @property + def children(self) -> List[Item[Self]]: + """List[:class:`Item`]: The list of children attached to this view.""" + return self._children.copy() + @classmethod def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> View: """Converts a message's components into a :class:`View`. @@ -304,7 +304,7 @@ def add_item(self, item: Item[Any]) -> Self: or the row the item is trying to be added to is full. """ - if len(self.children) > 25: + if len(self._children) > 25: raise ValueError('maximum number of children exceeded') if not isinstance(item, Item): @@ -313,7 +313,7 @@ def add_item(self, item: Item[Any]) -> Self: self.__weights.add_item(item) item._view = self - self.children.append(item) + self._children.append(item) return self def remove_item(self, item: Item[Any]) -> Self: @@ -329,7 +329,7 @@ def remove_item(self, item: Item[Any]) -> Self: """ try: - self.children.remove(item) + self._children.remove(item) except ValueError: pass else: @@ -342,7 +342,7 @@ def clear_items(self) -> Self: This function returns the class instance to allow for fluent-style chaining. """ - self.children.clear() + self._children.clear() self.__weights.clear() return self @@ -445,7 +445,7 @@ def _refresh(self, components: List[Component]) -> None: # fmt: off old_state: Dict[Tuple[int, str], Item[Any]] = { (item.type.value, item.custom_id): item # type: ignore - for item in self.children + for item in self._children if item.is_dispatchable() } # fmt: on @@ -459,7 +459,7 @@ def _refresh(self, components: List[Component]) -> None: older._refresh_component(component) children.append(older) - self.children = children + self._children = children def stop(self) -> None: """Stops listening to interaction events from this view. @@ -492,7 +492,7 @@ def is_persistent(self) -> bool: A persistent view has all their components with a set ``custom_id`` and a :attr:`timeout` set to ``None``. """ - return self.timeout is None and all(item.is_persistent() for item in self.children) + return self.timeout is None and all(item.is_persistent() for item in self._children) async def wait(self) -> bool: """Waits until the view has finished interacting. @@ -547,7 +547,7 @@ def add_view(self, view: View, message_id: Optional[int] = None) -> None: self.__verify_integrity() - for item in view.children: + for item in view._children: if item.is_dispatchable(): self._views[(item.type.value, message_id, item.custom_id)] = (view, item) # type: ignore @@ -559,7 +559,7 @@ def remove_view(self, view: View) -> None: self._modals.pop(view.custom_id, None) # type: ignore return - for item in view.children: + for item in view._children: if item.is_dispatchable(): self._views.pop((item.type.value, item.custom_id), None) # type: ignore