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 - name: Install dependencies
id: install-deps id: install-deps
run: | 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 pip install -U -r requirements.txt
- name: Setup node.js - name: Setup node.js

2
discord/enums.py

@ -488,7 +488,7 @@ class AuditLogAction(Enum):
AuditLogAction.home_settings_update: AuditLogActionCategory.update, AuditLogAction.home_settings_update: AuditLogActionCategory.update,
} }
# fmt: on # fmt: on
return lookup[self] return lookup.get(self, None)
@property @property
def target_type(self) -> Optional[str]: 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 # Sometimes asyncio is cheeky and wakes up a few microseconds before our target
# time, causing it to repeat a run. # time, causing it to repeat a run.
while self._is_explicit_time() and self._next_iteration <= self._last_iteration: 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. ' 'Clock drift detected for task %s. Woke up at %s but needed to sleep until %s. '
'Sleeping until %s again to correct clock' 'Sleeping until %s again to correct clock'
@ -249,7 +249,14 @@ class Loop(Generic[LF]):
self._last_iteration_failed = True self._last_iteration_failed = True
if not self.reconnect: if not self.reconnect:
raise 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: else:
if self._stop_next_iteration: if self._stop_next_iteration:
return return

4
discord/message.py

@ -488,7 +488,7 @@ class MessageSnapshot:
Extra features of the the message snapshot. Extra features of the the message snapshot.
stickers: List[:class:`StickerItem`] stickers: List[:class:`StickerItem`]
A list of sticker items given to the message. 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. A list of components in the message.
""" """
@ -2099,7 +2099,7 @@ class Message(PartialMessage, Hashable):
A list of sticker items given to the message. A list of sticker items given to the message.
.. versionadded:: 1.6 .. versionadded:: 1.6
components: List[Union[:class:`ActionRow`, :class:`Button`, :class:`SelectMenu`]] components: List[:class:`Component`]
A list of components in the message. A list of components in the message.
If :attr:`Intents.message_content` is not enabled this will always be an empty list 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. 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_soundboard: BoolOrNoneT
use_external_sounds: BoolOrNoneT use_external_sounds: BoolOrNoneT
send_voice_messages: BoolOrNoneT send_voice_messages: BoolOrNoneT
set_voice_channel_status: BoolOrNoneT
create_expressions: BoolOrNoneT create_expressions: BoolOrNoneT
create_events: BoolOrNoneT create_events: BoolOrNoneT
send_polls: BoolOrNoneT send_polls: BoolOrNoneT
@ -252,7 +253,7 @@ class Permissions(BaseFlags):
permissions set to ``True``. permissions set to ``True``.
""" """
# Some of these are 0 because we don't want to set unnecessary bits # 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 @classmethod
def _timeout_mask(cls) -> int: def _timeout_mask(cls) -> int:
@ -383,8 +384,12 @@ class Permissions(BaseFlags):
@classmethod @classmethod
def voice(cls) -> Self: def voice(cls) -> Self:
"""A factory method that creates a :class:`Permissions` with all """A factory method that creates a :class:`Permissions` with all
"Voice" permissions from the official Discord UI set to ``True``.""" "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)
.. 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 @classmethod
def stage(cls) -> Self: def stage(cls) -> Self:
@ -839,6 +844,14 @@ class Permissions(BaseFlags):
""" """
return 1 << 46 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 @flag_value
def send_polls(self) -> int: def send_polls(self) -> int:
""":class:`bool`: Returns ``True`` if a user can send poll messages. """:class:`bool`: Returns ``True`` if a user can send poll messages.
@ -989,6 +1002,7 @@ class PermissionOverwrite:
use_soundboard: Optional[bool] use_soundboard: Optional[bool]
use_external_sounds: Optional[bool] use_external_sounds: Optional[bool]
send_voice_messages: Optional[bool] send_voice_messages: Optional[bool]
set_voice_channel_status: Optional[bool]
create_expressions: Optional[bool] create_expressions: Optional[bool]
create_events: Optional[bool] create_events: Optional[bool]
send_polls: Optional[bool] send_polls: Optional[bool]

11
discord/ui/action_row.py

@ -67,6 +67,7 @@ if TYPE_CHECKING:
from ..components import SelectOption from ..components import SelectOption
from ..interactions import Interaction from ..interactions import Interaction
from .container import Container from .container import Container
from .dynamic import DynamicItem
SelectCallbackDecorator = Callable[[ItemCallbackType['S', BaseSelectT]], BaseSelectT] SelectCallbackDecorator = Callable[[ItemCallbackType['S', BaseSelectT]], BaseSelectT]
@ -194,10 +195,19 @@ class ActionRow(Item[V]):
# it should error anyways. # it should error anyways.
return True 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 @property
def width(self): def width(self):
return 5 return 5
@property
def _total_count(self) -> int:
# 1 for self and all children
return 1 + len(self._children)
@property @property
def type(self) -> Literal[ComponentType.action_row]: def type(self) -> Literal[ComponentType.action_row]:
return 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 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.ActionRow`, the :class:`discord.Interaction` you receive and
the :class:`discord.ui.Button` being pressed. the :class:`discord.ui.Button` being pressed.
.. note:: .. note::
Buttons with a URL or a SKU cannot be created with this function. 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 ..components import Container as ContainerComponent
from ..interactions import Interaction from ..interactions import Interaction
from .dynamic import DynamicItem
S = TypeVar('S', bound='Container', covariant=True) S = TypeVar('S', bound='Container', covariant=True)
V = TypeVar('V', bound='LayoutView', covariant=True) V = TypeVar('V', bound='LayoutView', covariant=True)
@ -198,6 +199,10 @@ class Container(Item[V]):
def _has_children(self): def _has_children(self):
return True 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 @property
def children(self) -> List[Item[V]]: def children(self) -> List[Item[V]]:
"""List[:class:`Item`]: The children of this container.""" """List[:class:`Item`]: The children of this container."""
@ -229,6 +234,11 @@ class Container(Item[V]):
def width(self): def width(self):
return 5 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: def _is_v2(self) -> bool:
return True return True
@ -308,10 +318,8 @@ class Container(Item[V]):
if not isinstance(item, Item): if not isinstance(item, Item):
raise TypeError(f'expected Item not {item.__class__.__name__}') raise TypeError(f'expected Item not {item.__class__.__name__}')
if item._has_children() and self._view: if self._view:
self._view._add_count(len(tuple(item.walk_children()))) # type: ignore self._view._add_count(item._total_count)
elif self._view:
self._view._add_count(1)
self._children.append(item) self._children.append(item)
item._update_view(self.view) item._update_view(self.view)
@ -336,10 +344,7 @@ class Container(Item[V]):
pass pass
else: else:
if self._view: if self._view:
if item._has_children(): self._view._add_count(-item._total_count)
self._view._add_count(-len(tuple(item.walk_children()))) # type: ignore
else:
self._view._add_count(-1)
return self return self
def find_item(self, id: int, /) -> Optional[Item[V]]: 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: def width(self) -> int:
return self.item.width return self.item.width
@property
def _total_count(self) -> int:
return self.item._total_count
@classmethod @classmethod
async def from_custom_id( async def from_custom_id(
cls: Type[Self], interaction: Interaction[ClientT], item: Item[Any], match: re.Match[str], / 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 ..components import Component
from .action_row import ActionRow from .action_row import ActionRow
from .container import Container from .container import Container
from .dynamic import DynamicItem
I = TypeVar('I', bound='Item[Any]') I = TypeVar('I', bound='Item[Any]')
V = TypeVar('V', bound='BaseView', covariant=True) V = TypeVar('V', bound='BaseView', covariant=True)
@ -118,6 +119,9 @@ class Item(Generic[V]):
return self._provided_custom_id return self._provided_custom_id
return True return True
def _swap_item(self, base: Item, new: DynamicItem, custom_id: str) -> None:
raise ValueError
def __repr__(self) -> str: def __repr__(self) -> str:
attrs = ' '.join(f'{key}={getattr(self, key)!r}' for key in self.__item_repr_attributes__) attrs = ' '.join(f'{key}={getattr(self, key)!r}' for key in self.__item_repr_attributes__)
return f'<{self.__class__.__name__} {attrs}>' return f'<{self.__class__.__name__} {attrs}>'
@ -143,6 +147,10 @@ class Item(Generic[V]):
def width(self) -> int: def width(self) -> int:
return 1 return 1
@property
def _total_count(self) -> int:
return 1
@property @property
def view(self) -> Optional[V]: def view(self) -> Optional[V]:
"""Optional[Union[:class:`View`, :class:`LayoutView`]]: The underlying view for this item.""" """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 typing_extensions import Self
from ..types.components import LabelComponent as LabelComponentPayload from ..types.components import LabelComponent as LabelComponentPayload
from .view import View from .view import BaseView
# fmt: off # fmt: off
@ -44,7 +44,7 @@ __all__ = (
) )
# fmt: on # fmt: on
V = TypeVar('V', bound='View', covariant=True) V = TypeVar('V', bound='BaseView', covariant=True)
class Label(Item[V]): 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 ..utils import MISSING, find
from .._types import ClientT from .._types import ClientT
from .item import Item from .item import Item
from .view import View from .view import BaseView
from .label import Label from .select import BaseSelect
from .text_input import TextInput
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Self from typing_extensions import Self
@ -53,7 +54,7 @@ __all__ = (
_log = logging.getLogger(__name__) _log = logging.getLogger(__name__)
class Modal(View): class Modal(BaseView):
"""Represents a UI modal. """Represents a UI modal.
This object must be inherited to create a modal popup window within discord. 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) children = sorted(self._children, key=key)
components: List[Dict[str, Any]] = [] components: List[Dict[str, Any]] = []
for child in children: for child in children:
if isinstance(child, Label): if isinstance(child, (BaseSelect, TextInput)):
components.append(child.to_component_dict()) # type: ignore
else:
# Every implicit child wrapped in an ActionRow in a modal # Every implicit child wrapped in an ActionRow in a modal
# has a single child of width 5 # has a single child of width 5
# It's also deprecated to use ActionRow in modals # It's also deprecated to use ActionRow in modals
@ -216,6 +215,8 @@ class Modal(View):
'components': [child.to_component_dict()], 'components': [child.to_component_dict()],
} }
) )
else:
components.append(child.to_component_dict())
return components return components
@ -234,3 +235,8 @@ class Modal(View):
} }
return payload 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 .item import Item
from .text_display import TextDisplay from .text_display import TextDisplay
from ..enums import ComponentType from ..enums import ComponentType
from ..utils import MISSING, get as _utils_get from ..utils import get as _utils_get
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Self from typing_extensions import Self
from .view import LayoutView from .view import LayoutView
from .dynamic import DynamicItem
from ..components import SectionComponent from ..components import SectionComponent
V = TypeVar('V', bound='LayoutView', covariant=True) V = TypeVar('V', bound='LayoutView', covariant=True)
@ -57,11 +58,6 @@ class Section(Item[V]):
The section accessory. The section accessory.
id: Optional[:class:`int`] id: Optional[:class:`int`]
The ID of this component. This must be unique across the view. The ID of this component. This must be unique across the view.
Attributes
----------
accessory: :class:`Item`
The section accessory.
""" """
__item_repr_attributes__ = ( __item_repr_attributes__ = (
@ -72,7 +68,7 @@ class Section(Item[V]):
__slots__ = ( __slots__ = (
'_children', '_children',
'accessory', '_accessory',
) )
def __init__( def __init__(
@ -83,13 +79,11 @@ class Section(Item[V]):
) -> None: ) -> None:
super().__init__() super().__init__()
self._children: List[Item[V]] = [] self._children: List[Item[V]] = []
if children: for child in children:
if len(children) > 3: self.add_item(child)
raise ValueError('maximum number of children exceeded')
self._children.extend( accessory._parent = self
[c if isinstance(c, Item) else TextDisplay(c) for c in children], self._accessory: Item[V] = accessory
)
self.accessory: Item[V] = accessory
self.id = id self.id = id
def __repr__(self) -> str: def __repr__(self) -> str:
@ -108,9 +102,31 @@ class Section(Item[V]):
def width(self): def width(self):
return 5 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: def _is_v2(self) -> bool:
return True 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]: def walk_children(self) -> Generator[Item[V], None, None]:
"""An iterator that recursively walks through all the children of this section """An iterator that recursively walks through all the children of this section
and its children, if applicable. This includes the `accessory`. and its children, if applicable. This includes the `accessory`.
@ -235,9 +251,8 @@ class Section(Item[V]):
def from_component(cls, component: SectionComponent) -> Self: def from_component(cls, component: SectionComponent) -> Self:
from .view import _component_to_item from .view import _component_to_item
# using MISSING as accessory so we can create the new one with the parent set accessory = _component_to_item(component.accessory, None)
self = cls(id=component.id, accessory=MISSING) self = cls(id=component.id, accessory=accessory)
self.accessory = _component_to_item(component.accessory, self)
self.id = component.id self.id = component.id
self._children = [_component_to_item(c, self) for c in component.children] 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.components import TextInput as TextInputPayload
from ..types.interactions import ModalSubmitTextInputInteractionData as ModalSubmitTextInputInteractionDataPayload from ..types.interactions import ModalSubmitTextInputInteractionData as ModalSubmitTextInputInteractionDataPayload
from .view import View from .view import BaseView
from ..interactions import Interaction from ..interactions import Interaction
@ -47,7 +47,7 @@ __all__ = (
) )
# fmt: on # fmt: on
V = TypeVar('V', bound='View', covariant=True) V = TypeVar('V', bound='BaseView', covariant=True)
class TextInput(Item[V]): class TextInput(Item[V]):

28
discord/ui/view.py

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

17
docs/api.rst

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

18
docs/interactions/api.rst

@ -303,6 +303,24 @@ Choice
:members: :members:
UnfurledMediaItem
~~~~~~~~~~~~~~~~~
.. attributetable:: UnfurledMediaItem
.. autoclass:: UnfurledMediaItem
:members:
MediaGalleryItem
~~~~~~~~~~~~~~~~
.. attributetable:: MediaGalleryItem
.. autoclass:: MediaGalleryItem
:members:
Enumerations 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 This page keeps a detailed human friendly rendering of what's new and changed
in specific versions. 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: .. _vp2p6p0:
v2.6.0 v2.6.0

Loading…
Cancel
Save