Browse Source

Merge branch 'Rapptz:master' into voice-messages

pull/10230/head
blord0 6 days ago
committed by GitHub
parent
commit
f4f5d930a3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      .git-blame-ignore-revs
  2. 2
      .github/workflows/lint.yml
  3. 2
      discord/enums.py
  4. 11
      discord/ext/tasks/__init__.py
  5. 4
      discord/message.py
  6. 20
      discord/permissions.py
  7. 11
      discord/ui/action_row.py
  8. 21
      discord/ui/container.py
  9. 4
      discord/ui/dynamic.py
  10. 8
      discord/ui/item.py
  11. 4
      discord/ui/label.py
  12. 18
      discord/ui/modal.py
  13. 49
      discord/ui/section.py
  14. 4
      discord/ui/text_input.py
  15. 28
      discord/ui/view.py
  16. 17
      docs/api.rst
  17. 18
      docs/interactions/api.rst
  18. 15
      docs/whats_new.rst

2
.git-blame-ignore-revs

@ -0,0 +1,2 @@
# Replace Black with Ruff, then format whole project.
44a44e938fb2bd0bb085d8aa4577abeb01653ad3

2
.github/workflows/lint.yml

@ -27,7 +27,7 @@ jobs:
- name: Install dependencies
id: install-deps
run: |
python -m pip install --upgrade pip setuptools wheel ruff==0.12 requests
python -m pip install --upgrade pip setuptools wheel ruff==0.12 requests "typing_extensions>=4.3,<5"
pip install -U -r requirements.txt
- name: Setup node.js

2
discord/enums.py

@ -488,7 +488,7 @@ class AuditLogAction(Enum):
AuditLogAction.home_settings_update: AuditLogActionCategory.update,
}
# fmt: on
return lookup[self]
return lookup.get(self, None)
@property
def target_type(self) -> Optional[str]:

11
discord/ext/tasks/__init__.py

@ -229,7 +229,7 @@ class Loop(Generic[LF]):
# Sometimes asyncio is cheeky and wakes up a few microseconds before our target
# time, causing it to repeat a run.
while self._is_explicit_time() and self._next_iteration <= self._last_iteration:
_log.warn(
_log.warning(
(
'Clock drift detected for task %s. Woke up at %s but needed to sleep until %s. '
'Sleeping until %s again to correct clock'
@ -249,7 +249,14 @@ class Loop(Generic[LF]):
self._last_iteration_failed = True
if not self.reconnect:
raise
await asyncio.sleep(backoff.delay())
retry_after = backoff.delay()
_log.exception(
'Handling exception in internal background task %s. Retrying in %.2fs',
self.coro.__qualname__,
retry_after,
)
await asyncio.sleep(retry_after)
else:
if self._stop_next_iteration:
return

4
discord/message.py

@ -488,7 +488,7 @@ class MessageSnapshot:
Extra features of the the message snapshot.
stickers: List[:class:`StickerItem`]
A list of sticker items given to the message.
components: List[Union[:class:`ActionRow`, :class:`Button`, :class:`SelectMenu`, :class:`Container`, :class:`SectionComponent`, :class:`TextDisplay`, :class:`MediaGalleryComponent`, :class:`FileComponent`, :class:`SeparatorComponent`, :class:`ThumbnailComponent`]]
components: List[:class:`Component`]]
A list of components in the message.
"""
@ -2099,7 +2099,7 @@ class Message(PartialMessage, Hashable):
A list of sticker items given to the message.
.. versionadded:: 1.6
components: List[Union[:class:`ActionRow`, :class:`Button`, :class:`SelectMenu`]]
components: List[:class:`Component`]
A list of components in the message.
If :attr:`Intents.message_content` is not enabled this will always be an empty list
unless the bot is mentioned or the message is a direct message.

20
discord/permissions.py

@ -88,6 +88,7 @@ if TYPE_CHECKING:
use_soundboard: BoolOrNoneT
use_external_sounds: BoolOrNoneT
send_voice_messages: BoolOrNoneT
set_voice_channel_status: BoolOrNoneT
create_expressions: BoolOrNoneT
create_events: BoolOrNoneT
send_polls: BoolOrNoneT
@ -252,7 +253,7 @@ class Permissions(BaseFlags):
permissions set to ``True``.
"""
# Some of these are 0 because we don't want to set unnecessary bits
return cls(0b0000_0000_0000_1110_0111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111)
return cls(0b0000_0000_0000_1111_0111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111)
@classmethod
def _timeout_mask(cls) -> int:
@ -383,8 +384,12 @@ class Permissions(BaseFlags):
@classmethod
def voice(cls) -> Self:
"""A factory method that creates a :class:`Permissions` with all
"Voice" permissions from the official Discord UI set to ``True``."""
return cls(0b0000_0000_0000_0000_0010_0100_1000_0000_0000_0011_1111_0000_0000_0011_0000_0000)
"Voice" permissions from the official Discord UI set to ``True``.
.. versionchanged:: 2.7
Added :attr:`set_voice_channel_status` permission.
"""
return cls(0b0000_0000_0000_0001_0010_0100_1000_0000_0000_0011_1111_0000_0000_0011_0000_0000)
@classmethod
def stage(cls) -> Self:
@ -839,6 +844,14 @@ class Permissions(BaseFlags):
"""
return 1 << 46
@flag_value
def set_voice_channel_status(self) -> int:
""":class:`bool`: Returns ``True`` if a user can set voice channel status.
.. versionadded:: 2.7
"""
return 1 << 48
@flag_value
def send_polls(self) -> int:
""":class:`bool`: Returns ``True`` if a user can send poll messages.
@ -989,6 +1002,7 @@ class PermissionOverwrite:
use_soundboard: Optional[bool]
use_external_sounds: Optional[bool]
send_voice_messages: Optional[bool]
set_voice_channel_status: Optional[bool]
create_expressions: Optional[bool]
create_events: Optional[bool]
send_polls: Optional[bool]

11
discord/ui/action_row.py

@ -67,6 +67,7 @@ if TYPE_CHECKING:
from ..components import SelectOption
from ..interactions import Interaction
from .container import Container
from .dynamic import DynamicItem
SelectCallbackDecorator = Callable[[ItemCallbackType['S', BaseSelectT]], BaseSelectT]
@ -194,10 +195,19 @@ class ActionRow(Item[V]):
# it should error anyways.
return True
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
@property
def width(self):
return 5
@property
def _total_count(self) -> int:
# 1 for self and all children
return 1 + len(self._children)
@property
def type(self) -> Literal[ComponentType.action_row]:
return ComponentType.action_row
@ -348,6 +358,7 @@ class ActionRow(Item[V]):
The function being decorated should have three parameters, ``self`` representing
the :class:`discord.ui.ActionRow`, the :class:`discord.Interaction` you receive and
the :class:`discord.ui.Button` being pressed.
.. note::
Buttons with a URL or a SKU cannot be created with this function.

21
discord/ui/container.py

@ -50,6 +50,7 @@ if TYPE_CHECKING:
from ..components import Container as ContainerComponent
from ..interactions import Interaction
from .dynamic import DynamicItem
S = TypeVar('S', bound='Container', covariant=True)
V = TypeVar('V', bound='LayoutView', covariant=True)
@ -198,6 +199,10 @@ class Container(Item[V]):
def _has_children(self):
return True
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
@property
def children(self) -> List[Item[V]]:
"""List[:class:`Item`]: The children of this container."""
@ -229,6 +234,11 @@ class Container(Item[V]):
def width(self):
return 5
@property
def _total_count(self) -> int:
# 1 for self and all children
return 1 + len(tuple(self.walk_children()))
def _is_v2(self) -> bool:
return True
@ -308,10 +318,8 @@ class Container(Item[V]):
if not isinstance(item, Item):
raise TypeError(f'expected Item not {item.__class__.__name__}')
if item._has_children() and self._view:
self._view._add_count(len(tuple(item.walk_children()))) # type: ignore
elif self._view:
self._view._add_count(1)
if self._view:
self._view._add_count(item._total_count)
self._children.append(item)
item._update_view(self.view)
@ -336,10 +344,7 @@ class Container(Item[V]):
pass
else:
if self._view:
if item._has_children():
self._view._add_count(-len(tuple(item.walk_children()))) # type: ignore
else:
self._view._add_count(-1)
self._view._add_count(-item._total_count)
return self
def find_item(self, id: int, /) -> Optional[Item[V]]:

4
discord/ui/dynamic.py

@ -168,6 +168,10 @@ class DynamicItem(Generic[BaseT], Item[Union[View, LayoutView]]):
def width(self) -> int:
return self.item.width
@property
def _total_count(self) -> int:
return self.item._total_count
@classmethod
async def from_custom_id(
cls: Type[Self], interaction: Interaction[ClientT], item: Item[Any], match: re.Match[str], /

8
discord/ui/item.py

@ -44,6 +44,7 @@ if TYPE_CHECKING:
from ..components import Component
from .action_row import ActionRow
from .container import Container
from .dynamic import DynamicItem
I = TypeVar('I', bound='Item[Any]')
V = TypeVar('V', bound='BaseView', covariant=True)
@ -118,6 +119,9 @@ class Item(Generic[V]):
return self._provided_custom_id
return True
def _swap_item(self, base: Item, new: DynamicItem, custom_id: str) -> None:
raise ValueError
def __repr__(self) -> str:
attrs = ' '.join(f'{key}={getattr(self, key)!r}' for key in self.__item_repr_attributes__)
return f'<{self.__class__.__name__} {attrs}>'
@ -143,6 +147,10 @@ class Item(Generic[V]):
def width(self) -> int:
return 1
@property
def _total_count(self) -> int:
return 1
@property
def view(self) -> Optional[V]:
"""Optional[Union[:class:`View`, :class:`LayoutView`]]: The underlying view for this item."""

4
discord/ui/label.py

@ -35,7 +35,7 @@ if TYPE_CHECKING:
from typing_extensions import Self
from ..types.components import LabelComponent as LabelComponentPayload
from .view import View
from .view import BaseView
# fmt: off
@ -44,7 +44,7 @@ __all__ = (
)
# fmt: on
V = TypeVar('V', bound='View', covariant=True)
V = TypeVar('V', bound='BaseView', covariant=True)
class Label(Item[V]):

18
discord/ui/modal.py

@ -33,8 +33,9 @@ from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, ClassVar, List
from ..utils import MISSING, find
from .._types import ClientT
from .item import Item
from .view import View
from .label import Label
from .view import BaseView
from .select import BaseSelect
from .text_input import TextInput
if TYPE_CHECKING:
from typing_extensions import Self
@ -53,7 +54,7 @@ __all__ = (
_log = logging.getLogger(__name__)
class Modal(View):
class Modal(BaseView):
"""Represents a UI modal.
This object must be inherited to create a modal popup window within discord.
@ -204,9 +205,7 @@ class Modal(View):
children = sorted(self._children, key=key)
components: List[Dict[str, Any]] = []
for child in children:
if isinstance(child, Label):
components.append(child.to_component_dict()) # type: ignore
else:
if isinstance(child, (BaseSelect, TextInput)):
# Every implicit child wrapped in an ActionRow in a modal
# has a single child of width 5
# It's also deprecated to use ActionRow in modals
@ -216,6 +215,8 @@ class Modal(View):
'components': [child.to_component_dict()],
}
)
else:
components.append(child.to_component_dict())
return components
@ -234,3 +235,8 @@ class Modal(View):
}
return payload
def add_item(self, item: Item[Any]) -> Self:
if len(self._children) >= 5:
raise ValueError('maximum number of children exceeded (5)')
return super().add_item(item)

49
discord/ui/section.py

@ -29,12 +29,13 @@ from typing import TYPE_CHECKING, Any, Dict, Generator, List, Literal, Optional,
from .item import Item
from .text_display import TextDisplay
from ..enums import ComponentType
from ..utils import MISSING, get as _utils_get
from ..utils import get as _utils_get
if TYPE_CHECKING:
from typing_extensions import Self
from .view import LayoutView
from .dynamic import DynamicItem
from ..components import SectionComponent
V = TypeVar('V', bound='LayoutView', covariant=True)
@ -57,11 +58,6 @@ class Section(Item[V]):
The section accessory.
id: Optional[:class:`int`]
The ID of this component. This must be unique across the view.
Attributes
----------
accessory: :class:`Item`
The section accessory.
"""
__item_repr_attributes__ = (
@ -72,7 +68,7 @@ class Section(Item[V]):
__slots__ = (
'_children',
'accessory',
'_accessory',
)
def __init__(
@ -83,13 +79,11 @@ class Section(Item[V]):
) -> None:
super().__init__()
self._children: List[Item[V]] = []
if children:
if len(children) > 3:
raise ValueError('maximum number of children exceeded')
self._children.extend(
[c if isinstance(c, Item) else TextDisplay(c) for c in children],
)
self.accessory: Item[V] = accessory
for child in children:
self.add_item(child)
accessory._parent = self
self._accessory: Item[V] = accessory
self.id = id
def __repr__(self) -> str:
@ -108,9 +102,31 @@ class Section(Item[V]):
def width(self):
return 5
@property
def _total_count(self) -> int:
# Count the accessory, ourselves, and all children
return 2 + len(self._children)
@property
def accessory(self) -> Item[V]:
""":class:`Item`: The section's accessory."""
return self._accessory
@accessory.setter
def accessory(self, value: Item[V]) -> None:
if not isinstance(value, Item):
raise TypeError(f'Expected an Item, got {value.__class__.__name__!r} instead')
value._parent = self
self._accessory = value
def _is_v2(self) -> bool:
return True
def _swap_item(self, base: Item, new: DynamicItem, custom_id: str) -> None:
if self.accessory.is_dispatchable() and getattr(self.accessory, 'custom_id', None) == custom_id:
self.accessory = new # type: ignore
def walk_children(self) -> Generator[Item[V], None, None]:
"""An iterator that recursively walks through all the children of this section
and its children, if applicable. This includes the `accessory`.
@ -235,9 +251,8 @@ class Section(Item[V]):
def from_component(cls, component: SectionComponent) -> Self:
from .view import _component_to_item
# using MISSING as accessory so we can create the new one with the parent set
self = cls(id=component.id, accessory=MISSING)
self.accessory = _component_to_item(component.accessory, self)
accessory = _component_to_item(component.accessory, None)
self = cls(id=component.id, accessory=accessory)
self.id = component.id
self._children = [_component_to_item(c, self) for c in component.children]

4
discord/ui/text_input.py

@ -37,7 +37,7 @@ if TYPE_CHECKING:
from ..types.components import TextInput as TextInputPayload
from ..types.interactions import ModalSubmitTextInputInteractionData as ModalSubmitTextInputInteractionDataPayload
from .view import View
from .view import BaseView
from ..interactions import Interaction
@ -47,7 +47,7 @@ __all__ = (
)
# fmt: on
V = TypeVar('V', bound='View', covariant=True)
V = TypeVar('V', bound='BaseView', covariant=True)
class TextInput(Item[V]):

28
discord/ui/view.py

@ -300,6 +300,12 @@ class BaseView:
if self.__timeout:
self.__timeout_expiry = time.monotonic() + self.__timeout
def _swap_item(self, base: Item, new: DynamicItem, custom_id: str) -> None:
# if an error is raised it is catched by the try/except block that calls
# this function
child_index = self._children.index(base)
self._children[child_index] = new # type: ignore
@property
def timeout(self) -> Optional[float]:
"""Optional[:class:`float`]: The timeout in seconds from last interaction with the UI before no longer accepting input.
@ -329,7 +335,9 @@ class BaseView:
@property
def total_children_count(self) -> int:
""":class:`int`: The total number of children in this view, including those from nested items."""
""":class:`int`: The total number of children in this view, including those from nested items.
.. versionadded:: 2.6"""
return self._total_children
@classmethod
@ -422,12 +430,7 @@ class BaseView:
raise ValueError('v2 items cannot be added to this view')
item._update_view(self)
added = 1
if item._has_children():
added += len(tuple(item.walk_children())) # type: ignore
self._add_count(added)
self._add_count(item._total_count)
self._children.append(item)
return self
@ -448,10 +451,7 @@ class BaseView:
except ValueError:
pass
else:
removed = 1
if item._has_children():
removed += len(tuple(item.walk_children())) # type: ignore
self._add_count(-removed)
self._add_count(-item._total_count)
return self
@ -657,6 +657,8 @@ class BaseView:
"""An iterator that recursively walks through all the children of this view
and its children, if applicable.
.. versionadded:: 2.6
Yields
------
:class:`Item`
@ -954,11 +956,9 @@ class ViewStore:
parent = base_item._parent or view
try:
child_index = parent._children.index(base_item) # type: ignore
parent._swap_item(base_item, item, custom_id)
except ValueError:
return
else:
parent._children[child_index] = item # type: ignore
item._view = view
item._rendered_row = base_item._rendered_row

17
docs/api.rst

@ -6091,23 +6091,6 @@ PollMedia
.. autoclass:: PollMedia
:members:
UnfurledMediaItem
~~~~~~~~~~~~~~~~~
.. attributetable:: UnfurledMediaItem
.. autoclass:: UnfurledMediaItem
:members:
MediaGalleryItem
~~~~~~~~~~~~~~~~
.. attributetable:: MediaGalleryItem
.. autoclass:: MediaGalleryItem
:members:
Exceptions
------------

18
docs/interactions/api.rst

@ -303,6 +303,24 @@ Choice
:members:
UnfurledMediaItem
~~~~~~~~~~~~~~~~~
.. attributetable:: UnfurledMediaItem
.. autoclass:: UnfurledMediaItem
:members:
MediaGalleryItem
~~~~~~~~~~~~~~~~
.. attributetable:: MediaGalleryItem
.. autoclass:: MediaGalleryItem
:members:
Enumerations
-------------

15
docs/whats_new.rst

@ -11,6 +11,21 @@ Changelog
This page keeps a detailed human friendly rendering of what's new and changed
in specific versions.
.. _vp2p6p1:
v2.6.1
-------
Bug Fixes
~~~~~~~~~~
- Fix :attr:`ui.Section.children` and :attr:`ui.Section.accessory` having ``None`` as the :attr:`Item.parent` (:issue:`10269`)
- Fix error when using a :class:`ui.DynamicItem` inside an :class:`ui.Section`
- Fix :class:`ui.DynamicItem` not working when set as an :attr:`ui.Section.acessory` (:issue:`10271`)
- Fix :attr:`ui.LayoutView.total_children_count` being inaccurate when adding nested items
- Fix crash when accessing :attr:`AuditLogEntry.category` for unknown audit log actions
- |tasks| Add logging statement when a handled exception occurs (:issue:`10276`)
.. _vp2p6p0:
v2.6.0

Loading…
Cancel
Save