diff --git a/discord/__init__.py b/discord/__init__.py index f4d7af42e..3279f8b8c 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -75,6 +75,7 @@ from .subscription import * from .presences import * from .primary_guild import * from .onboarding import * +from .collectible import * class VersionInfo(NamedTuple): diff --git a/discord/app_commands/commands.py b/discord/app_commands/commands.py index a23682f8b..36d07d41c 100644 --- a/discord/app_commands/commands.py +++ b/discord/app_commands/commands.py @@ -2523,10 +2523,7 @@ def guild_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext() f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment - # Ensure that only Guild context is allowed - allowed_contexts.guild = True # Enable guild context - allowed_contexts.private_channel = False # Disable private channel context - allowed_contexts.dm_channel = False # Disable DM context + allowed_contexts.guild = True return f @@ -2578,10 +2575,7 @@ def private_channel_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]] allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext() f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment - # Ensure that only Private Channel context is allowed - allowed_contexts.guild = False # Disable guild context - allowed_contexts.private_channel = True # Enable private channel context - allowed_contexts.dm_channel = False # Disable DM context + allowed_contexts.private_channel = True return f @@ -2631,11 +2625,7 @@ def dm_only(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: allowed_contexts = getattr(f, '__discord_app_commands_contexts__', None) or AppCommandContext() f.__discord_app_commands_contexts__ = allowed_contexts # type: ignore # Runtime attribute assignment - # Ensure that only DM context is allowed - allowed_contexts.guild = False # Disable guild context - allowed_contexts.private_channel = False # Disable private channel context - allowed_contexts.dm_channel = True # Enable DM context - + allowed_contexts.dm_channel = True return f # Check if called with parentheses or not @@ -2727,7 +2717,6 @@ def guild_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: f.__discord_app_commands_installation_types__ = allowed_installs # type: ignore # Runtime attribute assignment allowed_installs.guild = True - allowed_installs.user = False return f @@ -2776,7 +2765,6 @@ def user_install(func: Optional[T] = None) -> Union[T, Callable[[T], T]]: f.__discord_app_commands_installation_types__ = allowed_installs # type: ignore # Runtime attribute assignment allowed_installs.user = True - allowed_installs.guild = False return f diff --git a/discord/asset.py b/discord/asset.py index a3ed53c6b..41bcba3cf 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -355,6 +355,16 @@ class Asset(AssetMixin): animated=False, ) + @classmethod + def _from_user_collectible(cls, state: _State, asset: str, animated: bool = False) -> Self: + name = 'static.png' if not animated else 'asset.webm' + return cls( + state, + url=f'{cls.BASE}/assets/collectibles/{asset}{name}', + key=asset, + animated=animated, + ) + def __str__(self) -> str: return self._url diff --git a/discord/audit_logs.py b/discord/audit_logs.py index c27a793c3..e56f0fb3d 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -630,11 +630,6 @@ class _AuditLogProxyAutoModAction(_AuditLogProxy): channel: Optional[Union[abc.GuildChannel, Thread]] -class _AuditLogProxyAutoModActionQuarantineUser(_AuditLogProxy): - automod_rule_name: str - automod_rule_trigger_type: str - - class _AuditLogProxyMemberKickOrMemberRoleUpdate(_AuditLogProxy): integration_type: Optional[str] @@ -725,7 +720,6 @@ class AuditLogEntry(Hashable): _AuditLogProxyStageInstanceAction, _AuditLogProxyMessageBulkDelete, _AuditLogProxyAutoModAction, - _AuditLogProxyAutoModActionQuarantineUser, _AuditLogProxyMemberKickOrMemberRoleUpdate, Member, User, None, PartialIntegration, Role, Object @@ -766,6 +760,7 @@ class AuditLogEntry(Hashable): self.action is enums.AuditLogAction.automod_block_message or self.action is enums.AuditLogAction.automod_flag_message or self.action is enums.AuditLogAction.automod_timeout_member + or self.action is enums.AuditLogAction.automod_quarantine_user ): channel_id = utils._get_as_snowflake(extra, 'channel_id') channel = None @@ -781,13 +776,6 @@ class AuditLogEntry(Hashable): ), channel=channel, ) - elif self.action is enums.AuditLogAction.automod_quarantine_user: - self.extra = _AuditLogProxyAutoModActionQuarantineUser( - automod_rule_name=extra['auto_moderation_rule_name'], - automod_rule_trigger_type=enums.try_enum( - enums.AutoModRuleTriggerType, int(extra['auto_moderation_rule_trigger_type']) - ), - ) elif self.action.name.startswith('overwrite_'): # the overwrite_ actions have a dict with some information diff --git a/discord/collectible.py b/discord/collectible.py new file mode 100644 index 000000000..b2ad7e4e0 --- /dev/null +++ b/discord/collectible.py @@ -0,0 +1,109 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +from typing import Optional, TYPE_CHECKING + + +from .asset import Asset +from .enums import NameplatePalette, CollectibleType, try_enum +from .utils import parse_time + + +if TYPE_CHECKING: + from datetime import datetime + + from .state import ConnectionState + from .types.user import ( + Collectible as CollectiblePayload, + ) + + +__all__ = ('Collectible',) + + +class Collectible: + """Represents a user's collectible. + + .. versionadded:: 2.7 + + Attributes + ---------- + label: :class:`str` + The label of the collectible. + palette: Optional[:class:`NameplatePalette`] + The palette of the collectible. + This is only available if ``type`` is + :class:`CollectibleType.nameplate`. + sku_id: :class:`int` + The SKU ID of the collectible. + type: :class:`CollectibleType` + The type of the collectible. + expires_at: Optional[:class:`datetime.datetime`] + The expiration date of the collectible. If applicable. + """ + + __slots__ = ( + 'type', + 'sku_id', + 'label', + 'expires_at', + 'palette', + '_state', + '_asset', + ) + + def __init__(self, *, state: ConnectionState, type: str, data: CollectiblePayload) -> None: + self._state: ConnectionState = state + self.type: CollectibleType = try_enum(CollectibleType, type) + self._asset: str = data['asset'] + self.sku_id: int = int(data['sku_id']) + self.label: str = data['label'] + self.expires_at: Optional[datetime] = parse_time(data.get('expires_at')) + + # nameplate + self.palette: Optional[NameplatePalette] + try: + self.palette = try_enum(NameplatePalette, data['palette']) # type: ignore + except KeyError: + self.palette = None + + @property + def static(self) -> Asset: + """:class:`Asset`: The static asset of the collectible.""" + return Asset._from_user_collectible(self._state, self._asset) + + @property + def animated(self) -> Asset: + """:class:`Asset`: The animated asset of the collectible.""" + return Asset._from_user_collectible(self._state, self._asset, animated=True) + + def __repr__(self) -> str: + attrs = ['sku_id'] + if self.palette: + attrs.append('palette') + + joined_attrs = ' '.join(f'{attr}={getattr(self, attr)!r}' for attr in attrs) + return f'<{self.type.name.title()} {joined_attrs}>' diff --git a/discord/components.py b/discord/components.py index 0986680fc..08ae4f277 100644 --- a/discord/components.py +++ b/discord/components.py @@ -396,6 +396,7 @@ class SelectMenu(Component): 'min_values': self.min_values, 'max_values': self.max_values, 'disabled': self.disabled, + 'required': self.required, } if self.id is not None: payload['id'] = self.id @@ -1354,11 +1355,11 @@ class LabelComponent(Component): __slots__ = ( 'label', 'description', - 'commponent', + 'component', 'id', ) - __repr_info__ = ('label', 'description', 'commponent', 'id,') + __repr_info__ = ('label', 'description', 'component') def __init__(self, data: LabelComponentPayload, state: Optional[ConnectionState]) -> None: self.component: Component = _component_factory(data['component'], state) # type: ignore diff --git a/discord/enums.py b/discord/enums.py index 7dc4bccd0..172f736a9 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -83,6 +83,8 @@ __all__ = ( 'OnboardingMode', 'SeparatorSpacing', 'MediaItemLoadingState', + 'CollectibleType', + 'NameplatePalette', ) @@ -273,6 +275,7 @@ class MessageType(Enum): guild_incident_report_false_alarm = 39 purchase_notification = 44 poll_result = 46 + emoji_added = 63 class SpeakingState(Enum): @@ -968,6 +971,24 @@ class MediaItemLoadingState(Enum): not_found = 3 +class CollectibleType(Enum): + nameplate = 'nameplate' + + +class NameplatePalette(Enum): + crimson = 'crimson' + berry = 'berry' + sky = 'sky' + teal = 'teal' + forest = 'forest' + bubble_gum = 'bubble_gum' + violet = 'violet' + cobalt = 'cobalt' + clover = 'clover' + lemon = 'lemon' + white = 'white' + + def create_unknown_value(cls: Type[E], val: Any) -> E: value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below name = f'unknown_{val}' diff --git a/discord/flags.py b/discord/flags.py index 5105a4156..a4878368c 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -371,6 +371,15 @@ class SystemChannelFlags(BaseFlags): """ return 32 + @flag_value + def emoji_added(self): + """:class:`bool`: Returns ``True`` if the system channel is used for + emoji added notifications. + + .. versionadded:: 2.7 + """ + return 256 + @fill_with_flags() class MessageFlags(BaseFlags): diff --git a/discord/member.py b/discord/member.py index 9f6b9daf2..fd2cf7edb 100644 --- a/discord/member.py +++ b/discord/member.py @@ -75,6 +75,7 @@ if TYPE_CHECKING: VoiceState as VoiceStatePayload, ) from .primary_guild import PrimaryGuild + from .collectible import Collectible VocalGuildChannel = Union[VoiceChannel, StageChannel] @@ -311,6 +312,7 @@ class Member(discord.abc.Messageable, _UserTag): avatar_decoration: Optional[Asset] avatar_decoration_sku_id: Optional[int] primary_guild: PrimaryGuild + collectibles: List[Collectible] def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: ConnectionState): self._state: ConnectionState = state diff --git a/discord/message.py b/discord/message.py index 02f31198d..9db351d54 100644 --- a/discord/message.py +++ b/discord/message.py @@ -2843,14 +2843,14 @@ class Message(PartialMessage, Hashable): if call_ended: duration = utils._format_call_duration(self.call.duration) # type: ignore # call can't be None here if missed: - return 'You missed a call from {0.author.name} that lasted {1}.'.format(self, duration) + return f'You missed a call from {self.author.name} that lasted {duration}.' else: - return '{0.author.name} started a call that lasted {1}.'.format(self, duration) + return f'{self.author.name} started a call that lasted {duration}.' else: if missed: - return '{0.author.name} started a call. \N{EM DASH} Join the call'.format(self) + return f'{self.author.name} started a call. \N{EM DASH} Join the call' else: - return '{0.author.name} started a call.'.format(self) + return f'{self.author.name} started a call.' if self.type is MessageType.purchase_notification and self.purchase_notification is not None: guild_product_purchase = self.purchase_notification.guild_product_purchase @@ -2865,6 +2865,9 @@ class Message(PartialMessage, Hashable): ) return f"{self.author.display_name}'s poll {poll_title.value} has closed." # type: ignore + if self.type is MessageType.emoji_added: + return f'{self.author.name} added a new emoji, {self.content}' + # Fallback for unknown message types return '' diff --git a/discord/primary_guild.py b/discord/primary_guild.py index 85e40159a..940cb77c8 100644 --- a/discord/primary_guild.py +++ b/discord/primary_guild.py @@ -35,6 +35,12 @@ if TYPE_CHECKING: from .types.user import PrimaryGuild as PrimaryGuildPayload from typing_extensions import Self +# fmt: off +__all__ = ( + 'PrimaryGuild', +) +# fmt: on + class PrimaryGuild: """Represents the primary guild identity of a :class:`User` diff --git a/discord/types/message.py b/discord/types/message.py index dfb251f28..c7631ffc3 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -175,6 +175,7 @@ MessageType = Literal[ 39, 44, 46, + 63, ] diff --git a/discord/types/user.py b/discord/types/user.py index b2b213ecf..639384a56 100644 --- a/discord/types/user.py +++ b/discord/types/user.py @@ -27,11 +27,38 @@ from typing import Literal, Optional, TypedDict from typing_extensions import NotRequired -class AvatarDecorationData(TypedDict): +PremiumType = Literal[0, 1, 2, 3] +NameplatePallete = Literal['crimson', 'berry', 'sky', 'teal', 'forest', 'bubble_gum', 'violet', 'cobalt', 'clover'] + + +class _UserSKU(TypedDict): asset: str sku_id: Snowflake +AvatarDecorationData = _UserSKU + + +class PrimaryGuild(TypedDict): + identity_guild_id: Optional[int] + identity_enabled: Optional[bool] + tag: Optional[str] + badge: Optional[str] + + +class Collectible(_UserSKU): + label: str + expires_at: Optional[str] + + +class NameplateCollectible(Collectible): + palette: str + + +class UserCollectibles(TypedDict): + nameplate: NameplateCollectible + + class PartialUser(TypedDict): id: Snowflake username: str @@ -39,9 +66,8 @@ class PartialUser(TypedDict): avatar: Optional[str] global_name: Optional[str] avatar_decoration_data: NotRequired[AvatarDecorationData] - - -PremiumType = Literal[0, 1, 2, 3] + primary_guild: NotRequired[PrimaryGuild] + collectibles: NotRequired[UserCollectibles] class User(PartialUser, total=False): @@ -54,10 +80,3 @@ class User(PartialUser, total=False): flags: int premium_type: PremiumType public_flags: int - - -class PrimaryGuild(TypedDict): - identity_guild_id: Optional[int] - identity_enabled: Optional[bool] - tag: Optional[str] - badge: Optional[str] diff --git a/discord/ui/action_row.py b/discord/ui/action_row.py index b0598db20..c7f7a2b7b 100644 --- a/discord/ui/action_row.py +++ b/discord/ui/action_row.py @@ -227,8 +227,7 @@ class ActionRow(Item[V]): An item in the action row. """ - for child in self.children: - yield child + yield from self.children def content_length(self) -> int: """:class:`int`: Returns the total length of all text content in this action row.""" diff --git a/discord/ui/modal.py b/discord/ui/modal.py index c85719a9b..86c09da30 100644 --- a/discord/ui/modal.py +++ b/discord/ui/modal.py @@ -70,11 +70,11 @@ class Modal(BaseView): from discord import ui class Questionnaire(ui.Modal, title='Questionnaire Response'): - name = ui.TextInput(label='Name') - answer = ui.TextInput(label='Answer', style=discord.TextStyle.paragraph) + name = ui.Label(text='Name', component=ui.TextInput()) + answer = ui.Label(text='Answer', component=ui.TextInput(style=discord.TextStyle.paragraph)) async def on_submit(self, interaction: discord.Interaction): - await interaction.response.send_message(f'Thanks for your response, {self.name}!', ephemeral=True) + await interaction.response.send_message(f'Thanks for your response, {self.name.component.value}!', ephemeral=True) Parameters ----------- diff --git a/discord/ui/section.py b/discord/ui/section.py index 6a4026e22..67d35e001 100644 --- a/discord/ui/section.py +++ b/discord/ui/section.py @@ -87,7 +87,7 @@ class Section(Item[V]): self.id = id def __repr__(self) -> str: - return f'<{self.__class__.__name__} children={len(self._children)}>' + return f'<{self.__class__.__name__} children={len(self._children)} accessory={self._accessory!r}>' @property def type(self) -> Literal[ComponentType.section]: @@ -137,8 +137,7 @@ class Section(Item[V]): An item in this section. """ - for child in self.children: - yield child + yield from self.children yield self.accessory def _update_view(self, view) -> None: diff --git a/discord/ui/view.py b/discord/ui/view.py index cbf40a14d..9c7547e60 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -426,9 +426,6 @@ class BaseView: if not isinstance(item, Item): raise TypeError(f'expected Item not {item.__class__.__name__}') - if item._is_v2() and not self._is_layout(): - raise ValueError('v2 items cannot be added to this view') - item._update_view(self) self._add_count(item._total_count) self._children.append(item) @@ -737,6 +734,9 @@ class View(BaseView): if len(self._children) >= 25: raise ValueError('maximum number of children exceeded') + if item._is_v2(): + raise ValueError('v2 items cannot be added to this view') + super().add_item(item) try: self.__weights.add_item(item) diff --git a/discord/user.py b/discord/user.py index 751437532..32edb1dc7 100644 --- a/discord/user.py +++ b/discord/user.py @@ -33,6 +33,7 @@ from .enums import DefaultAvatar from .flags import PublicUserFlags from .utils import snowflake_time, _bytes_to_base64_data, MISSING, _get_as_snowflake from .primary_guild import PrimaryGuild +from .collectible import Collectible if TYPE_CHECKING: from typing_extensions import Self @@ -49,6 +50,7 @@ if TYPE_CHECKING: User as UserPayload, AvatarDecorationData, PrimaryGuild as PrimaryGuildPayload, + UserCollectibles as UserCollectiblesPayload, ) @@ -78,6 +80,7 @@ class BaseUser(_UserTag): '_state', '_avatar_decoration_data', '_primary_guild', + '_collectibles', ) if TYPE_CHECKING: @@ -94,6 +97,7 @@ class BaseUser(_UserTag): _public_flags: int _avatar_decoration_data: Optional[AvatarDecorationData] _primary_guild: Optional[PrimaryGuildPayload] + _collectibles: Optional[UserCollectiblesPayload] def __init__(self, *, state: ConnectionState, data: Union[UserPayload, PartialUserPayload]) -> None: self._state = state @@ -132,6 +136,7 @@ class BaseUser(_UserTag): self.system = data.get('system', False) self._avatar_decoration_data = data.get('avatar_decoration_data') self._primary_guild = data.get('primary_guild', None) + self._collectibles = data.get('collectibles', None) @classmethod def _copy(cls, user: Self) -> Self: @@ -149,6 +154,7 @@ class BaseUser(_UserTag): self._public_flags = user._public_flags self._avatar_decoration_data = user._avatar_decoration_data self._primary_guild = user._primary_guild + self._collectibles = user._collectibles return self @@ -324,6 +330,16 @@ class BaseUser(_UserTag): return PrimaryGuild(state=self._state, data=self._primary_guild) return PrimaryGuild._default(self._state) + @property + def collectibles(self) -> List[Collectible]: + """List[:class:`Collectible`]: Returns a list of the user's collectibles. + + .. versionadded:: 2.7 + """ + if self._collectibles is None: + return [] + return [Collectible(state=self._state, type=key, data=value) for key, value in self._collectibles.items() if value] # type: ignore + def mentioned_in(self, message: Message) -> bool: """Checks if the user is mentioned in the specified message. diff --git a/docs/api.rst b/docs/api.rst index 53ddec06e..ab8f4f5ca 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -874,6 +874,7 @@ Members - avatar - username - discriminator + - primary guild This requires :attr:`Intents.members` to be enabled. @@ -1924,6 +1925,14 @@ of :class:`enum.Enum`. The system message sent when a poll has closed. + .. versionadded:: 2.5 + + .. attribute:: emoji_added + + The system message sent when a custom emoji is added to the guild. + + .. versionadded:: 2.7 + .. class:: UserFlags Represents Discord User flags. @@ -3063,10 +3072,11 @@ of :class:`enum.Enum`. a :class:`Member` with the ID of the person who triggered the automod rule. When this is the action, the type of :attr:`~AuditLogEntry.extra` is - set to an unspecified proxy object with 2 attributes: + set to an unspecified proxy object with 3 attributes: - ``automod_rule_name``: The name of the automod rule that was triggered. - ``automod_rule_trigger_type``: A :class:`AutoModRuleTriggerType` representation of the rule type that was triggered. + - ``channel``: The channel of the message sent by the member when they were flagged. `None` if the member was quarantined when they just joined the guild. When this is the action, :attr:`AuditLogEntry.changes` is empty. @@ -4052,8 +4062,6 @@ of :class:`enum.Enum`. Default channels and questions count towards onboarding constraints. - - .. class:: MediaItemLoadingState Represents a :class:`UnfurledMediaItem` load state. @@ -4074,6 +4082,66 @@ of :class:`enum.Enum`. The media item was not found. +.. class:: CollectibleType + + Represents the type of a :class:`Collectible`. + + .. versionadded:: 2.7 + + .. attribute:: nameplate + + The collectible is a nameplate. + +.. class:: NameplatePalette + + Represents the available palettes for a nameplate. + + .. versionadded:: 2.7 + + .. attribute:: crimson + + The collectible nameplate palette is crimson. + + .. attribute:: berry + + The collectible nameplate palette is berry. + + .. attribute:: sky + + The collectible nameplate palette is sky. + + .. attribute:: teal + + The collectible nameplate palette is teal. + + .. attribute:: forest + + The collectible nameplate palette is forest. + + .. attribute:: bubble_gum + + The collectible nameplate palette is bubble gum. + + .. attribute:: violet + + The collectible nameplate palette is violet. + + .. attribute:: cobalt + + The collectible nameplate palette is cobalt. + + .. attribute:: clover + + The collectible nameplate palette is clover. + + .. attribute:: lemon + + The collectible nameplate palette is lemon. + + .. attribute:: white + + The collectible nameplate palette is white. + .. _discord-api-audit-logs: Audit Log Data @@ -5770,6 +5838,14 @@ PrimaryGuild .. autoclass:: PrimaryGuild() :members: +Collectible +~~~~~~~~~~~ + +.. attributetable:: Collectible + +.. autoclass:: Collectible() + :members: + CallMessage ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/whats_new.rst b/docs/whats_new.rst index 25011bb4b..c78c44435 100644 --- a/docs/whats_new.rst +++ b/docs/whats_new.rst @@ -11,6 +11,28 @@ Changelog This page keeps a detailed human friendly rendering of what's new and changed in specific versions. +.. _vp2p6p3: + +v2.6.3 +------- + +Bug Fixes +~~~~~~~~~~ + +- Fix :attr:`ui.Select.required` not being applied properly +- Fix potential attribute error when accessing :class:`LabelComponent` +- Fix issue when stacking decorators such as :func:`app_commands.guild_install` and :func:`app_commands.user_install` + +.. _vp2p6p2: + +v2.6.2 +------- + +Bug Fixes +~~~~~~~~~~ + +- Fix a bug with :class:`ui.DynamicItem` causing it to fail at runtime when passing a generic. + .. _vp2p6p1: v2.6.1