From e64ab5fd7c00d5549885f5ec38ce04f0abdaab0c Mon Sep 17 00:00:00 2001 From: Milan Smeets Date: Tue, 28 Jan 2025 11:37:41 +0100 Subject: [PATCH] feat!: remove wrapped interactions (#32) * feat: remove wrapped interactions * feat: update examples --- examples/attrs.py | 8 +- examples/button.py | 13 +- examples/interaction.py | 45 -- examples/manager.py | 26 +- examples/row.py | 14 +- examples/select.py | 20 +- src/disnake/ext/components/__init__.py | 1 - .../ext/components/impl/component/base.py | 9 + .../ext/components/impl/component/button.py | 11 +- .../ext/components/impl/component/select.py | 11 +- src/disnake/ext/components/impl/manager.py | 91 +++- src/disnake/ext/components/interaction.py | 498 ------------------ 12 files changed, 121 insertions(+), 626 deletions(-) delete mode 100644 examples/interaction.py delete mode 100644 src/disnake/ext/components/interaction.py diff --git a/examples/attrs.py b/examples/attrs.py index a0f0ee3..dbfd236 100644 --- a/examples/attrs.py +++ b/examples/attrs.py @@ -16,7 +16,7 @@ class CustomisableSelect(components.RichStringSelect): def __attrs_post_init__(self) -> None: self.max_values = len(self.options) - async def callback(self, interaction: components.MessageInteraction) -> None: + async def callback(self, interaction: disnake.MessageInteraction) -> None: selection = ( "\n".join(f"- {value}" for value in interaction.values) if interaction.values @@ -43,9 +43,9 @@ async def make_select(interaction: disnake.CommandInteraction, options: str) -> await interaction.response.send_message("You must specify at most 25 options!") return - wrapped = components.wrap_interaction(interaction) - await wrapped.response.send_message( - components=CustomisableSelect(options=actual_options), + component = await CustomisableSelect(options=actual_options).as_ui_component() + await interaction.response.send_message( + components=component, ) diff --git a/examples/button.py b/examples/button.py index f3442c2..ef9df9e 100644 --- a/examples/button.py +++ b/examples/button.py @@ -18,19 +18,18 @@ class MyButton(components.RichButton): count: int = 0 - async def callback(self, interaction: components.MessageInteraction) -> None: + async def callback(self, interaction: disnake.MessageInteraction) -> None: self.count += 1 self.label = str(self.count) - await interaction.response.edit_message(components=self) + component = await self.as_ui_component() + await interaction.response.edit_message(components=component) @bot.slash_command() # pyright: ignore # still some unknowns in disnake -async def test_button(inter: disnake.CommandInteraction) -> None: - wrapped = components.wrap_interaction(inter) - component = MyButton() - - await wrapped.response.send_message(components=component) +async def test_button(interaction: disnake.CommandInteraction) -> None: + component = await MyButton().as_ui_component() + await interaction.response.send_message(components=component) bot.run(os.getenv("EXAMPLE_TOKEN")) diff --git a/examples/interaction.py b/examples/interaction.py deleted file mode 100644 index 2ef6f7b..0000000 --- a/examples/interaction.py +++ /dev/null @@ -1,45 +0,0 @@ -"""A simple example on the use of interaction wrappers.""" - -import os -import typing - -import disnake -from disnake.ext import commands, components - -bot = commands.InteractionBot() - -manager = components.get_manager() -manager.add_to_bot(bot) - - -@manager.register -class MyButton(components.RichButton): - label: typing.Optional[str] = "\N{PROHIBITED SIGN} 클릭" - style: disnake.ButtonStyle = disnake.ButtonStyle.red - - async def callback(self, interaction: components.MessageInteraction) -> None: - await interaction.response.send_message("Don't touch me!") - - -@bot.slash_command() # pyright: ignore # still some unknowns in disnake -@components.wrap_interaction_for -async def with_wrapped_callback(interaction: components.CommandInteraction): - print(MyButton().label) - - return await interaction.response.send_message(components=MyButton()) - - -@bot.slash_command() # pyright: ignore # still some unknowns in disnake -async def with_manual_wrap(interaction: disnake.CommandInteraction): - wrapped = components.wrap_interaction(interaction) - return await wrapped.response.send_message(components=MyButton()) - - -@bot.slash_command() # pyright: ignore # still some unknowns in disnake -async def without_wrap(interaction: disnake.CommandInteraction): - return await interaction.response.send_message( - components=await MyButton().as_ui_component() - ) - - -bot.run(os.getenv("EXAMPLE_TOKEN")) diff --git a/examples/manager.py b/examples/manager.py index 3e2381f..79d84af 100644 --- a/examples/manager.py +++ b/examples/manager.py @@ -21,11 +21,12 @@ class FooButton(components.RichButton): count: int - async def callback(self, interaction: components.MessageInteraction) -> None: + async def callback(self, interaction: disnake.MessageInteraction) -> None: self.count += 1 self.label = str(self.count) - await interaction.response.edit_message(components=self) + component = await self.as_ui_component() + await interaction.response.edit_message(components=component) @deeply_nested_manager.register @@ -34,11 +35,12 @@ class FooBarBazButton(components.RichButton): count: int - async def callback(self, interaction: components.MessageInteraction) -> None: + async def callback(self, interaction: disnake.MessageInteraction) -> None: self.count += 1 self.label = str(self.count) - await interaction.response.edit_message(components=self) + component = await self.as_ui_component() + await interaction.response.edit_message(components=component) @manager.as_callback_wrapper @@ -100,19 +102,15 @@ async def error_handler( @bot.slash_command() # pyright: ignore # still some unknowns in disnake -async def test_button(inter: disnake.CommandInteraction) -> None: - wrapped = components.wrap_interaction(inter) - component = FooButton(count=0) - - await wrapped.response.send_message(components=component) +async def test_button(interaction: disnake.CommandInteraction) -> None: + component = await FooButton(count=0).as_ui_component() + await interaction.response.send_message(components=component) @bot.slash_command() # pyright: ignore # still some unknowns in disnake -async def test_nested_button(inter: disnake.CommandInteraction) -> None: - wrapped = components.wrap_interaction(inter) - component = FooBarBazButton(count=0) - - await wrapped.response.send_message(components=component) +async def test_nested_button(interaction: disnake.CommandInteraction) -> None: + component = await FooBarBazButton(count=0).as_ui_component() + await interaction.response.send_message(components=component) bot.run(os.getenv("EXAMPLE_TOKEN")) diff --git a/examples/row.py b/examples/row.py index 7a05603..f848ecb 100644 --- a/examples/row.py +++ b/examples/row.py @@ -50,7 +50,7 @@ def update_select(self, components: typing.Sequence[components.api.RichComponent select.set_options(options) - async def callback(self, interaction: components.MessageInteraction): + async def callback(self, interaction: disnake.MessageInteraction): # Get all components on the message for easier re-sending. # Both of these lists will automagically contain self so that any # changes immediately reflect without extra effort. @@ -68,7 +68,8 @@ async def callback(self, interaction: components.MessageInteraction): self.update_select(components) # Re-send and update all components. - await interaction.response.send_message(components=rows) + finalised = await manager.finalise_components(rows) + await interaction.response.edit_message(components=finalised) @manager.register() @@ -89,7 +90,7 @@ def set_options(self, options: typing.List[disnake.SelectOption]): self.max_values = 1 self.disabled = True - async def callback(self, interaction: components.MessageInteraction) -> None: + async def callback(self, interaction: disnake.MessageInteraction) -> None: selection = ( "\n".join(f"- {value}" for value in interaction.values) if interaction.values @@ -101,9 +102,8 @@ async def callback(self, interaction: components.MessageInteraction) -> None: @bot.slash_command() # pyright: ignore async def test_components(interaction: disnake.CommandInteraction) -> None: - wrapped = components.wrap_interaction(interaction) - await wrapped.response.send_message( - components=[ + layout = await manager.finalise_components( + [ [ OptionsToggleButton(label="numbers", options=["1", "2", "3", "4", "5"]), OptionsToggleButton(label="letters", options=["a", "b", "c", "d", "e"]), @@ -113,5 +113,7 @@ async def test_components(interaction: disnake.CommandInteraction) -> None: ] ) + await interaction.response.send_message(components=layout) + bot.run(os.environ["EXAMPLE_TOKEN"]) diff --git a/examples/select.py b/examples/select.py index 832f8d4..cb50ee8 100644 --- a/examples/select.py +++ b/examples/select.py @@ -58,9 +58,9 @@ class MySelect(components.RichStringSelect): colour_middle: str = BLACK_SQUARE colour_right: str = BLACK_SQUARE - async def callback(self, inter: components.MessageInteraction) -> None: - assert inter.values is not None - selected = inter.values[0] + async def callback(self, interaction: disnake.MessageInteraction) -> None: + assert interaction.values is not None + selected = interaction.values[0] if self.state == "slot": self.handle_slots(selected) @@ -69,7 +69,8 @@ async def callback(self, inter: components.MessageInteraction) -> None: self.handle_colours(selected) msg = self.render_colours() - await inter.response.edit_message(msg, components=self) + component = await self.as_ui_component() + await interaction.response.edit_message(msg, components=component) def handle_slots(self, selected: str) -> None: if selected == "Finalise": @@ -94,12 +95,11 @@ def render_colours(self) -> str: @bot.slash_command() # pyright: ignore # still some unknowns in disnake -async def test_select(inter: disnake.CommandInteraction) -> None: - wrapped = components.wrap_interaction(inter) - - component = MySelect() - await wrapped.response.send_message( - component.render_colours(), components=component +async def test_select(interaction: disnake.CommandInteraction) -> None: + my_select = MySelect() + await interaction.response.send_message( + my_select.render_colours(), + components=await my_select.as_ui_component(), ) diff --git a/src/disnake/ext/components/__init__.py b/src/disnake/ext/components/__init__.py index 66f0ff5..6dc375e 100644 --- a/src/disnake/ext/components/__init__.py +++ b/src/disnake/ext/components/__init__.py @@ -12,4 +12,3 @@ from disnake.ext.components import internal as internal from disnake.ext.components.fields import * from disnake.ext.components.impl import * -from disnake.ext.components.interaction import * diff --git a/src/disnake/ext/components/impl/component/base.py b/src/disnake/ext/components/impl/component/base.py index 03e7fdf..44034d9 100644 --- a/src/disnake/ext/components/impl/component/base.py +++ b/src/disnake/ext/components/impl/component/base.py @@ -267,3 +267,12 @@ async def make_custom_id(self) -> str: raise RuntimeError(message) return await self.manager.make_custom_id(self) + + async def callback( # pyright: ignore[reportIncompatibleMethodOverride] # noqa: D102 + self, inter: disnake.MessageInteraction, / + ) -> None: + # <> + + # NOTE: We narrow the interaction type down to a disnake.MessageInteraction + # here. This isn't typesafe, but it's just cleaner for the user. + ... diff --git a/src/disnake/ext/components/impl/component/button.py b/src/disnake/ext/components/impl/component/button.py index e2d0e5e..bcf24bd 100644 --- a/src/disnake/ext/components/impl/component/button.py +++ b/src/disnake/ext/components/impl/component/button.py @@ -5,7 +5,7 @@ import typing import disnake -from disnake.ext.components import fields, interaction +from disnake.ext.components import fields from disnake.ext.components.api import component as component_api from disnake.ext.components.impl.component import base as component_base @@ -64,12 +64,3 @@ async def as_ui_component(self) -> disnake.ui.Button[None]: # noqa: D102 emoji=self.emoji, custom_id=await self.manager.make_custom_id(self), ) - - async def callback( # pyright: ignore[reportIncompatibleMethodOverride] # noqa: D102 - self, inter: interaction.MessageInteraction, / - ) -> None: - # <> - - # NOTE: We narrow the interaction type down to a disnake.MessageInteraction - # here. This isn't typesafe, but it's just cleaner for the user. - ... diff --git a/src/disnake/ext/components/impl/component/select.py b/src/disnake/ext/components/impl/component/select.py index 38c6c00..9c881a3 100644 --- a/src/disnake/ext/components/impl/component/select.py +++ b/src/disnake/ext/components/impl/component/select.py @@ -6,7 +6,7 @@ import attr import disnake -from disnake.ext.components import fields, interaction +from disnake.ext.components import fields from disnake.ext.components.api import component as component_api from disnake.ext.components.impl.component import base as component_base @@ -41,15 +41,6 @@ class BaseSelect( max_values: int = fields.internal(default=1) disabled: bool = fields.internal(default=False) - async def callback( # pyright: ignore[reportIncompatibleMethodOverride] - self, inter: interaction.MessageInteraction, / - ) -> None: - # <> - - # NOTE: We narrow the interaction type down to a disnake.MessageInteraction - # here. This isn't typesafe, but it's just cleaner for the user. - ... - class RichStringSelect(BaseSelect, typing.Protocol): """The default implementation of a disnake-ext-components string select. diff --git a/src/disnake/ext/components/impl/manager.py b/src/disnake/ext/components/impl/manager.py index 7720c13..dda286b 100644 --- a/src/disnake/ext/components/impl/manager.py +++ b/src/disnake/ext/components/impl/manager.py @@ -13,7 +13,6 @@ import disnake from disnake.ext import commands from disnake.ext.components import fields -from disnake.ext.components import interaction as interaction_impl from disnake.ext.components.api import component as component_api from disnake.ext.components.internal import omit, reference @@ -57,8 +56,19 @@ "ExceptionHandlerFuncT", bound=ExceptionHandlerFunc ) -ComponentT = typing.TypeVar("ComponentT", bound=component_api.RichComponent) -ComponentType = typing.Type[component_api.RichComponent] +RichComponentT = typing.TypeVar("RichComponentT", bound=component_api.RichComponent) +RichComponentType = typing.Type[component_api.RichComponent] + +MessageComponents = typing.Union[ + component_api.RichButton, + disnake.ui.Button[typing.Any], + component_api.RichSelect, + disnake.ui.StringSelect[typing.Any], + disnake.ui.ChannelSelect[typing.Any], + disnake.ui.RoleSelect[typing.Any], + disnake.ui.UserSelect[typing.Any], + disnake.ui.MentionableSelect[typing.Any], +] def _to_ui_component( @@ -238,7 +248,7 @@ class ComponentManager(component_api.ComponentManager): _bot: typing.Optional[AnyBot] _children: typing.Set[ComponentManager] - _components: weakref.WeakValueDictionary[str, ComponentType] + _components: weakref.WeakValueDictionary[str, RichComponentType] _count: typing.Optional[bool] _counter: int _identifiers: dict[str, str] @@ -306,7 +316,7 @@ def children(self) -> typing.Set[ComponentManager]: # noqa: D102 return self._children @property - def components(self) -> typing.Mapping[str, ComponentType]: # noqa: D102 + def components(self) -> typing.Mapping[str, RichComponentType]: # noqa: D102 # <> return self._components @@ -377,7 +387,7 @@ def config( if not omit.is_omitted(sep): self._sep = sep - def make_identifier(self, component_type: ComponentType) -> str: # noqa: D102 + def make_identifier(self, component_type: RichComponentType) -> str: # noqa: D102 # <> return component_type.__name__ @@ -508,7 +518,7 @@ async def parse_message_components( message: disnake.Message, *reference_objects: object, ) -> typing.Tuple[ - typing.Sequence[typing.Sequence[interaction_impl.MessageComponents]], + typing.Sequence[typing.Sequence[MessageComponents]], typing.Sequence[component_api.RichComponent], ]: """Parse all components on a message into rich components or ui components. @@ -555,7 +565,7 @@ async def parse_message_components( on the nested structure. """ # noqa: E501 - new_rows: typing.List[typing.List[interaction_impl.MessageComponents]] = [] + new_rows: typing.List[typing.List[MessageComponents]] = [] rich_components: typing.List[component_api.RichComponent] = [] reference_obj = reference.create_reference( @@ -566,7 +576,7 @@ async def parse_message_components( should_test = current_component is not None for row in message.components: - new_row: typing.List[interaction_impl.MessageComponents] = [] + new_row: typing.List[MessageComponents] = [] new_rows.append(new_row) for component in row.children: @@ -593,14 +603,50 @@ async def parse_message_components( return new_rows, rich_components + async def finalise_components( + self, + components: typing.Sequence[typing.Sequence[MessageComponents]], + ) -> disnake.ui.Components[disnake.ui.MessageUIComponent]: + """Finalise the output of :meth:`parse_message_components` back into disnake ui components. + + Parameters + ---------- + components: + A sequence of rows of components, which can be any combination of + disnake ui components and rich components. The rich components are + automatically cast to their equivalent disnake ui components so + that they can be sent as an interaction response. + + Returns + ------- + disnake.ui.Components[disnake.ui.MessageUIComponent]: + A disnake-compatible structure of sendable components. + + """ # noqa: E501 + finalised: typing.List[typing.List[disnake.ui.MessageUIComponent]] = [] + + for row in components: + new_row: typing.List[disnake.ui.MessageUIComponent] = [] + finalised.append(new_row) + + for component in row: + if isinstance( + component, (component_api.RichButton, component_api.RichSelect) + ): + new_row.append(await component.as_ui_component()) # type: ignore + else: + new_row.append(component) + + return finalised + # Identifier and component: function call, return component @typing.overload def register( self, - component_type: typing.Type[ComponentT], + component_type: typing.Type[RichComponentT], *, identifier: typing.Optional[str] = None, - ) -> typing.Type[ComponentT]: + ) -> typing.Type[RichComponentT]: ... # Only identifier: nested decorator, return callable that registers and @@ -608,17 +654,17 @@ def register( @typing.overload def register( self, *, identifier: typing.Optional[str] = None - ) -> typing.Callable[[typing.Type[ComponentT]], typing.Type[ComponentT]]: + ) -> typing.Callable[[typing.Type[RichComponentT]], typing.Type[RichComponentT]]: ... def register( self, - component_type: typing.Optional[typing.Type[ComponentT]] = None, + component_type: typing.Optional[typing.Type[RichComponentT]] = None, *, identifier: typing.Optional[str] = None, ) -> typing.Union[ - typing.Type[ComponentT], - typing.Callable[[typing.Type[ComponentT]], typing.Type[ComponentT]], + typing.Type[RichComponentT], + typing.Callable[[typing.Type[RichComponentT]], typing.Type[RichComponentT]], ]: """Register a component to this component manager. @@ -627,17 +673,19 @@ def register( if component_type is not None: return self.register_component(component_type, identifier=identifier) - def wrapper(component_type: typing.Type[ComponentT]) -> typing.Type[ComponentT]: + def wrapper( + component_type: typing.Type[RichComponentT], + ) -> typing.Type[RichComponentT]: return self.register_component(component_type, identifier=identifier) return wrapper def register_component( # noqa: D102 self, - component_type: typing.Type[ComponentT], + component_type: typing.Type[RichComponentT], *, identifier: typing.Optional[str] = None, - ) -> typing.Type[ComponentT]: + ) -> typing.Type[RichComponentT]: # <> resolved_identifier = identifier or self.make_identifier(component_type) module_data = _ModuleData.from_object(component_type) @@ -675,7 +723,9 @@ def register_component( # noqa: D102 return component_type - def deregister_component(self, component_type: ComponentType) -> None: # noqa: D102 + def deregister_component( # noqa: D102 + self, component_type: RichComponentType + ) -> None: # <> identifier = self.make_identifier(component_type) @@ -881,8 +931,7 @@ async def invoke_component( # noqa: D102 ) # If none raised, we run the callback. - wrapped = interaction_impl.wrap_interaction(interaction) - await component.callback(wrapped) + await component.callback(interaction) except Exception as exception: # noqa: BLE001 # Blanket exception catching is desired here as it's meant to diff --git a/src/disnake/ext/components/interaction.py b/src/disnake/ext/components/interaction.py deleted file mode 100644 index ea4b478..0000000 --- a/src/disnake/ext/components/interaction.py +++ /dev/null @@ -1,498 +0,0 @@ -"""Interaction implementations extending disnake interactions.""" - -from __future__ import annotations - -import functools -import typing - -import disnake -import typing_extensions -from disnake.ext.components.api import component as component_api - -__all__: typing.Sequence[str] = ( - "WrappedInteraction", - "MessageInteraction", - "CommandInteraction", - "wrap_interaction", - "wrap_interaction_for", -) - - -ComponentT = typing.TypeVar( - "ComponentT", - bound=typing.Union[component_api.RichComponent, disnake.ui.WrappedComponent], -) - -Components = typing.Union[ - # ActionRow[ComponentT], - ComponentT, - typing.Sequence[ - typing.Union[ - # "ActionRow[ComponentT]", - ComponentT, - typing.Sequence[ComponentT], - ] - ], -] -# TODO: Custom action rows? - -MessageComponents = typing.Union[ - component_api.RichButton, - disnake.ui.Button[typing.Any], - component_api.RichSelect, - disnake.ui.StringSelect[typing.Any], - disnake.ui.ChannelSelect[typing.Any], - disnake.ui.RoleSelect[typing.Any], - disnake.ui.UserSelect[typing.Any], - disnake.ui.MentionableSelect[typing.Any], -] - -P = typing_extensions.ParamSpec("P") -InteractionT = typing.TypeVar("InteractionT", bound=disnake.Interaction) -ReturnT = typing.TypeVar("ReturnT") - -InteractionCallback = typing.Callable[ - typing_extensions.Concatenate[InteractionT, P], - typing.Coroutine[None, None, ReturnT], -] -InteractionCallbackMethod = typing.Callable[ - typing_extensions.Concatenate[typing.Any, InteractionT, P], - typing.Coroutine[None, None, ReturnT], -] - -MISSING = disnake.utils.MISSING - - -async def _prepare(component: MessageComponents) -> disnake.ui.MessageUIComponent: - if isinstance( - component, (component_api.RichButton, component_api.RichSelect) - ): # TODO: add select - return await component.as_ui_component() # pyright: ignore[reportReturnType] - - return component - - -@typing.overload -async def _to_ui_components( - components: Components[MessageComponents] = MISSING, - *, - allow_none: typing.Literal[False] = False, -) -> disnake.ui.Components[disnake.ui.MessageUIComponent]: - ... - - -@typing.overload -async def _to_ui_components( - components: typing.Optional[Components[MessageComponents]] = MISSING, - *, - allow_none: typing.Literal[True], -) -> typing.Optional[disnake.ui.Components[disnake.ui.MessageUIComponent]]: - ... - - -async def _to_ui_components( - components: typing.Optional[Components[MessageComponents]] = MISSING, - *, - allow_none: bool = False, -) -> typing.Optional[disnake.ui.Components[disnake.ui.MessageUIComponent]]: - if components is None: - if not allow_none: - msg = "Components cannot be None in this method." - raise TypeError(msg) - return components - - if components is MISSING: - return MISSING - - if not isinstance(components, typing.Sequence): - return await _prepare(components) - - finalised: disnake.ui.Components[disnake.ui.MessageUIComponent] = [] - for component in components: - if not isinstance(component, typing.Sequence): - finalised.append(await _prepare(component)) - continue - - finalised.append([await _prepare(nested) for nested in component]) - - return finalised - - -class WrappedInteraction(disnake.Interaction): - """Interaction implementation that wraps :class:`disnake.Interaction`. - - This wrapped interaction class natively supports disnake-ext-components' - specialised components classes and -- unlike vanilla disnake interactions -- - can send them without manually having to convert them to native disnake - components first. - - Attribute access is simply proxied to the wrapped interaction object by - means of a custom :meth:`__getattr__` implementation. - """ - - __slots__ = ("_wrapped",) - - def __init__(self, wrapped: disnake.Interaction): - self._wrapped = wrapped - - def __getattr__(self, name: str) -> typing.Any: # noqa: ANN401 - return getattr(self._wrapped, name) - - @disnake.utils.cached_slot_property("_cs_response") - def response(self) -> WrappedInteractionResponse: # noqa: D102 - # <> - - return WrappedInteractionResponse(super().response) - - @disnake.utils.cached_slot_property("_cs_followup") - def followup(self) -> disnake.Webhook: - """Returns the follow up webhook for follow up interactions.""" # noqa: D401 - return self._wrapped.followup # TODO: custom followup object - - async def edit_original_response( # pyright: ignore[reportIncompatibleMethodOverride] # noqa: PLR0913, D102 - self, - content: typing.Optional[str] = MISSING, - *, - embed: typing.Optional[disnake.Embed] = MISSING, - embeds: typing.List[disnake.Embed] = MISSING, - file: disnake.File = MISSING, - files: typing.List[disnake.File] = MISSING, - attachments: typing.Optional[typing.List[disnake.Attachment]] = MISSING, - view: typing.Optional[disnake.ui.View] = MISSING, - components: typing.Optional[Components[MessageComponents]] = MISSING, - suppress_embeds: bool = MISSING, - allowed_mentions: typing.Optional[disnake.AllowedMentions] = None, - ) -> disnake.InteractionMessage: - # <> - - return await self._wrapped.edit_original_response( - content=content, - embed=embed, - embeds=embeds, - file=file, - files=files, - attachments=attachments, - view=view, - components=await _to_ui_components(components, allow_none=True), - suppress_embeds=suppress_embeds, - allowed_mentions=allowed_mentions, - ) - - edit_original_message = edit_original_response - - async def send( # pyright: ignore[reportIncompatibleMethodOverride] # noqa: PLR0913, D102 - self, - content: typing.Optional[str] = None, - *, - embed: disnake.Embed = MISSING, - embeds: typing.List[disnake.Embed] = MISSING, - file: disnake.File = MISSING, - files: typing.List[disnake.File] = MISSING, - allowed_mentions: disnake.AllowedMentions = MISSING, - view: disnake.ui.View = MISSING, - components: Components[MessageComponents] = MISSING, - tts: bool = False, - ephemeral: bool = False, - suppress_embeds: bool = False, - delete_after: float = MISSING, - ) -> None: - # <> - - return await self._wrapped.send( - content=content, - embed=embed, - embeds=embeds, - file=file, - files=files, - allowed_mentions=allowed_mentions, - view=view, - components=await _to_ui_components(components), - tts=tts, - ephemeral=ephemeral, - suppress_embeds=suppress_embeds, - delete_after=delete_after, - ) - - -class WrappedInteractionResponse(disnake.InteractionResponse): - """Interaction response implementation that wraps :class:`disnake.InteractionResponse`. - - This wrapped interaction response class natively supports - disnake-ext-components' specialised components classes and -- unlike - vanilla disnake interactions -- can send them without manually having to - convert them to native disnake components first. - - Attribute access is simply proxied to the wrapped interaction response - object by means of a custom :meth:`__getattr__` implementation. - """ # noqa: E501 - - __slots__ = ("_wrapped",) - - def __init__(self, wrapped: disnake.InteractionResponse): - self._wrapped = wrapped - - def __getattr__(self, name: str) -> typing.Any: # noqa: ANN401 - """Get an attribute of this class or the wrapped interaction.""" - return getattr(self._wrapped, name) - - async def send_message( # pyright: ignore[reportIncompatibleMethodOverride] # noqa: PLR0913 - self, - content: typing.Optional[str] = None, - *, - embed: disnake.Embed = MISSING, - embeds: typing.List[disnake.Embed] = MISSING, - file: disnake.File = MISSING, - files: typing.List[disnake.File] = MISSING, - allowed_mentions: disnake.AllowedMentions = MISSING, - view: disnake.ui.View = MISSING, - components: Components[MessageComponents] = MISSING, - tts: bool = False, - ephemeral: bool = False, - suppress_embeds: bool = False, - delete_after: float = MISSING, - ) -> None: - # <> - - return await self._wrapped.send_message( - content=content, - embed=embed, - embeds=embeds, - file=file, - files=files, - allowed_mentions=allowed_mentions, - view=view, - components=await _to_ui_components(components), - tts=tts, - ephemeral=ephemeral, - suppress_embeds=suppress_embeds, - delete_after=delete_after, - ) - - async def edit_message( # pyright: ignore[reportIncompatibleMethodOverride] # noqa: PLR0913 - self, - content: typing.Optional[str] = None, - *, - embed: disnake.Embed = MISSING, - embeds: typing.List[disnake.Embed] = MISSING, - file: disnake.File = MISSING, - files: typing.List[disnake.File] = MISSING, - attachments: typing.Optional[typing.List[disnake.Attachment]] = MISSING, - allowed_mentions: disnake.AllowedMentions = MISSING, - view: disnake.ui.View = MISSING, - components: typing.Optional[Components[MessageComponents]] = MISSING, - ) -> None: - # <> - - return await self._wrapped.edit_message( - content=content, - embed=embed, - embeds=embeds, - file=file, - files=files, - attachments=attachments, - allowed_mentions=allowed_mentions, - view=view, - components=await _to_ui_components(components, allow_none=True), - ) - - -class MessageInteraction( # pyright: ignore[reportIncompatibleMethodOverride, reportIncompatibleVariableOverride] - WrappedInteraction, disnake.MessageInteraction -): - """Message interaction implementation that wraps :class:`disnake.MessageInteraction`. - - This wrapped message interaction class natively supports - disnake-ext-components' specialised components classes and -- unlike - vanilla disnake interactions -- can send them without manually having to - convert them to native disnake components first. - - Attribute access is simply proxied to the wrapped message interaction - object by means of a custom :meth:`__getattr__` implementation. - """ # noqa: E501 - - # __slots__ = () # No slots on disnake.MessageInteraction... - - def __init__(self, wrapped: disnake.MessageInteraction): - self._wrapped = wrapped - - # message = proxy.ProxiedProperty("_wrapped") - - -class CommandInteraction( # pyright: ignore[reportIncompatibleMethodOverride, reportIncompatibleVariableOverride] - WrappedInteraction, disnake.CommandInteraction -): - """Message interaction implementation that wraps :class:`disnake.CommandInteraction`. - - This wrapped command interaction class natively supports - disnake-ext-components' specialised components classes and -- unlike - vanilla disnake interactions -- can send them without manually having to - convert them to native disnake components first. - - Attribute access is simply proxied to the wrapped command interaction - object by means of a custom :meth:`__getattr__` implementation. - """ # noqa: E501 - - def __init__(self, wrapped: disnake.CommandInteraction): - self._wrapped = wrapped - - -@typing.overload -def wrap_interaction( - interaction: disnake.CommandInteraction, -) -> CommandInteraction: - ... - - -@typing.overload -def wrap_interaction( - interaction: disnake.MessageInteraction, -) -> MessageInteraction: - ... - - -@typing.overload -def wrap_interaction(interaction: disnake.Interaction) -> WrappedInteraction: - ... - - -def wrap_interaction(interaction: disnake.Interaction) -> WrappedInteraction: - """Wrap a disnake interaction type for disnake-ext-components compatibility. - - Interactions wrapped in this way can send disnake-ext-components' - specialised components directly, without having to first convert them to - native disnake components. - - Parameters - ---------- - interaction: - The interaction to wrap. - - Returns - ------- - WrappedInteraction: - The wrapped interaction. Note that this can be any subclass of - :class:`WrappedInteraction`: - - - Wrapping a (subclass of) :class:`disnake.MessageInteraction` returns - a :class:`MessageInteraction`, - - Wrapping a (subclass of) :class:`disnake.ApplicationCommandInteraction` - returns a :class:`CommandInteraction`, - - Wrapping any other interaction class returns a - :class:`WrappedInteraction`. - - """ - if isinstance(interaction, disnake.MessageInteraction): - return MessageInteraction(interaction) - - # TODO: ModalInteraction - - return WrappedInteraction(interaction) - - -def wrap_args_kwargs( - args: typing.Tuple[object, ...], kwargs: typing.Dict[str, object] -) -> typing.Tuple[typing.Tuple[object, ...], typing.Dict[str, object]]: - args_iter = iter(args) - new_args: typing.List[object] = [] - - # We assume there's only ever going to be one interaction that needs to be - # wrapped. We check args first, and if no interaction was found, we check - # kwargs. Note that we only check at most two args. - for arg, _ in zip(args_iter, range(2)): - if isinstance(arg, disnake.Interaction): - new_args.append(wrap_interaction(arg)) - break - else: - new_args.append(arg) - - else: - for kw, arg in kwargs.items(): - if isinstance(arg, disnake.Interaction): - kwargs[kw] = wrap_interaction(arg) - break - - else: - msg = "No wrappable interaction was found!" - raise TypeError(msg) - - new_args.extend(args_iter) - - return tuple(new_args), kwargs - - -@typing.overload -def wrap_interaction_for( - callback: InteractionCallbackMethod[WrappedInteraction, P, ReturnT] -) -> InteractionCallbackMethod[disnake.Interaction, P, ReturnT]: - ... - - -@typing.overload -def wrap_interaction_for( - callback: InteractionCallbackMethod[MessageInteraction, P, ReturnT] -) -> InteractionCallbackMethod[disnake.MessageInteraction, P, ReturnT]: - ... - - -@typing.overload -def wrap_interaction_for( - callback: InteractionCallbackMethod[CommandInteraction, P, ReturnT] -) -> InteractionCallbackMethod[disnake.CommandInteraction, P, ReturnT]: - ... - - -@typing.overload -def wrap_interaction_for( - callback: InteractionCallback[WrappedInteraction, P, ReturnT] -) -> InteractionCallback[disnake.Interaction, P, ReturnT]: - ... - - -@typing.overload -def wrap_interaction_for( - callback: InteractionCallback[MessageInteraction, P, ReturnT] -) -> InteractionCallback[disnake.MessageInteraction, P, ReturnT]: - ... - - -@typing.overload -def wrap_interaction_for( - callback: InteractionCallback[CommandInteraction, P, ReturnT] -) -> InteractionCallback[disnake.CommandInteraction, P, ReturnT]: - ... - - -def wrap_interaction_for( - callback: typing.Callable[..., typing.Coroutine[None, None, ReturnT]] -) -> typing.Callable[..., typing.Coroutine[None, None, ReturnT]]: - r"""Wrap a callback that takes an interaction for disnake-ext-components compatibility. - - Interactions wrapped in this way can send disnake-ext-components' - specialised components directly, without having to first convert them to - native disnake components. - - .. seealso:: - This uses :func:`wrap_interaction` under the hood. - - Parameters - ---------- - callback: - The callback to wrap. - - This can be either a function or a method. In case of a function, the - interaction must be the first argument. Otherwise, it must be the - second argument after ``self``. - - Returns - ------- - :obj:`~typing.Callable`\[:obj:`... `, :obj:`~typing.Coroutine`\[:obj:`None`, :obj:`None`, ``ReturnT``]] - The callback that had its interaction wrapped. - - """ # noqa: E501 - - @functools.wraps(callback) - async def wrapper(*args: object, **kwargs: object) -> ReturnT: - args, kwargs = wrap_args_kwargs(args, kwargs) - return await callback(*args, **kwargs) - - return wrapper