Skip to content

Commit

Permalink
fix: remove dataclasses from all context classes to fix weird super()…
Browse files Browse the repository at this point in the history
… behaviour
  • Loading branch information
tandemdude committed Aug 13, 2024
1 parent 654cb71 commit f5d1bb7
Show file tree
Hide file tree
Showing 8 changed files with 313 additions and 68 deletions.
14 changes: 14 additions & 0 deletions docs/source/by-examples/100_appendix.md
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
# Appendix

---

## Components and Modals

Lightbulb includes a component handler (and modal handler) that you can use to make processing component and modal
interactions easier than it otherwise would be using raw Hikari code. For a usage guide you should see the
documentation for the {obj}`components subpackage <lightbulb.components>`.

---

## Scheduled and Repeating Tasks

TODO
2 changes: 1 addition & 1 deletion lightbulb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# 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.
"""A simple-to-use command handler for Hikari."""
"""A simple, elegant and powerful command handler for Hikari."""

from lightbulb import components
from lightbulb import di
Expand Down
10 changes: 8 additions & 2 deletions lightbulb/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,7 @@ def build_autocomplete_context(
Returns:
:obj:`~lightbulb.context.AutocompleteContext`: The built context.
"""
return context_.AutocompleteContext(self, interaction, options, command_cls)
return context_.AutocompleteContext(client=self, interaction=interaction, options=options, command=command_cls)

async def _execute_autocomplete_context(
self, context: context_.AutocompleteContext[t.Any], autocomplete_provider: options_.AutocompleteProvider[t.Any]
Expand Down Expand Up @@ -1134,7 +1134,13 @@ def build_rest_autocomplete_context(
command_cls: type[commands.CommandBase],
response_callback: Callable[[hikari.api.InteractionResponseBuilder], None],
) -> context_.AutocompleteContext[t.Any]:
return context_.RestAutocompleteContext(self, interaction, options, command_cls, response_callback)
return context_.RestAutocompleteContext(
client=self,
interaction=interaction,
options=options,
command=command_cls,
_initial_response_callback=response_callback,
)

async def handle_rest_autocomplete_interaction(
self, interaction: hikari.AutocompleteInteraction
Expand Down
177 changes: 177 additions & 0 deletions lightbulb/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,183 @@
# 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.
"""
This package contains a framework for creating your own component and modal handlers, without having to go
through the common issues when trying to do it using raw Hikari.
----
Component Handling
------------------
Creating a Menu
^^^^^^^^^^^^^^^
Creating your own component handler is as easy as creating a subclass of the :obj:`~lightbulb.components.menus.Menu`
class.
.. dropdown:: Example
Creating a menu.
.. code-block:: python
import lightbulb
class MyMenu(lightbulb.components.Menu):
def __init__(self) -> None:
...
A single menu class encapsulates the components and state that will be used when handling interactions for any
of the attached components.
Adding Components to Menus
^^^^^^^^^^^^^^^^^^^^^^^^^^
You can add components to a menu using any of the appropriate methods:
- :meth:`~lightbulb.components.menus.Menu.add_interactive_button`
- :meth:`~lightbulb.components.menus.Menu.add_link_button`
- :meth:`~lightbulb.components.menus.Menu.add_text_select`
- :meth:`~lightbulb.components.menus.Menu.add_user_select`
- :meth:`~lightbulb.components.menus.Menu.add_role_select`
- :meth:`~lightbulb.components.menus.Menu.add_mentionable_select`
- :meth:`~lightbulb.components.menus.Menu.add_channel_select`
The menu will lay out the added components into rows automatically. If you wish to customise the layout, you
can use the methods :meth:`~lightbulb.components.base.BuildableComponentContainer.next_row` and
:meth:`~lightbulb.components.base.BuildableComponentContainer.previous_row` to move between rows while adding
components. If a row becomes full (either through having five buttons, or one select), then the menu will
**always** move to the next row if you add another component to it.
When adding a component to a menu, the methods return an object representing the created component. It is recommended
that you store this component within an instance variable so that you can modify it later if you wish to update
the menu's appearance.
.. dropdown:: Example
Adding a component to a menu.
.. code-block:: python
import lightbulb
class MyMenu(lightbulb.components.Menu):
def __init__(self) -> None:
self.btn = self.add_interactive_button(
hikari.ButtonStyle.PRIMARY,
self.on_button_press,
label="Test Button",
)
async def on_button_press(self, ctx: lightbulb.components.MenuContext) -> None:
await ctx.respond("Button pressed!")
Running Menus
^^^^^^^^^^^^^
To send a menu with a message, you can pass the menu instance to the ``components`` argument of the method you
are using (i.e. ``Context.respond``, ``RESTClient.create_message``) - it will be automatically built and sent
with the message.
Menus require the Lightbulb :obj:`~lightbulb.client.Client` in order to listen for the appropriate interactions. You
can run a menu by calling the :meth:`~lightbulb.components.menus.Menu.attach` method. When calling this method,
you can optionally choose to wait until the menu completes before continuing, and pass a timeout after which
time an :obj:`asyncio.TimeoutError` will be raised.
If you do not pass ``wait=True`` to the ``attach()`` method, then it is recommended that you pass your own known
custom IDs when you are adding components to the menu - otherwise they will be randomly generated and the menu will
probably not work as you intended.
To get your ``Client`` instance within a command, you can use dependency injection as seen in the following example.
Check the "Dependencies" guide within the by-example section of the documentation for more details about dependency
injection.
.. dropdown:: Example
Attaching the menu to a client instance within a command.
.. code-block:: python
import lightbulb
class MyMenu(lightbulb.components.Menu):
def __init__(self) -> None:
self.btn = self.add_interactive_button(
hikari.ButtonStyle.PRIMARY,
self.on_button_press,
label="Test Button",
)
async def on_button_press(self, ctx: lightbulb.components.MenuContext) -> None:
# Edit the message containing the buttons with the new content, and
# remove all the attached components.
await ctx.respond("Button pressed!", edit=True, components=[])
# Stop listening for additional interactions for this menu
ctx.stop_interacting()
class MyCommand(lightbulb.SlashCommand, name="test, description="test"):
@lightbulb.invoke
async def invoke(self, ctx: lightbulb.Context, client: lightbulb.Client) -> None:
menu = MyMenu()
resp = await ctx.respond("Menu testing", components=menu)
# Run the menu, and catch a timeout if one occurs
try:
await menu.attach(client, wait=True, timeout=30)
except asyncio.TimeoutError:
await ctx.edit_respond(resp, "Timed out!", components=[])
.. warning::
You should **always** pass a timeout, unless you wish the menu to be persistent. If you do not set a timeout,
then the number of active menus will grow forever, along with the memory usage of your program.
.. warning::
There are no checks added to menus by default to ensure that only one user can interact with any menu. If you
wish to restrict a menu to only a single user (or add other checks) you should pass any state to the menu
constructor and run your check at the top of each component callback.
.. important::
It is recommended that you create a new instance of your menu every time you send it for the first time - otherwise
multiple invocations could potentially interact with each other in unexpected ways.
Once you have sent your menu, and it is processing interactions, you can safely modify the menu from within your
component callbacks in any way - change attributes of the components, add components, remove components, etc. If,
within a component callback, you wish to resend the menu with a response (after changing anything) - you can pass
``rebuild_menu=True``, or ``components=self`` to the context respond call .
A Note on Select Components
^^^^^^^^^^^^^^^^^^^^^^^^^^^
When adding a select menu to a component menu you **must** store it as an instance variable. If you do not do this
then getting the selected values for it will not be typed correctly.
You can get the selected values for a select menu using the
:meth:`~lightbulb.components.menus.MenuContext.selected_values_for` method.
.. dropdown:: Example
.. code-block:: python
import lightbulb
class MyMenu(lightbulb.components.Menu):
def __init__(self) -> None:
self.select = self.add_text_select(["foo", "bar", "baz"], self.on_select)
async def on_select(self, ctx: lightbulb.components.MenuContext) -> None:
await ctx.respond(f"Selected: {ctx.selected_values_for(self.select)}")
----
Modal Handling
--------------
bar
----
"""

from lightbulb.components.base import *
from lightbulb.components.menus import *
from lightbulb.components.modals import *
Expand Down
38 changes: 25 additions & 13 deletions lightbulb/components/menus.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@

import abc
import asyncio
import dataclasses
import typing as t
import uuid
from collections.abc import Sequence
Expand Down Expand Up @@ -273,7 +272,7 @@ def add_to_row(self, row: special_endpoints.MessageActionRowBuilder) -> special_
)


class MentionableSelect(Select[hikari.Snowflake]):
class MentionableSelect(Select[hikari.Unique]):
"""Class representing a select menu with snowflake options."""

__slots__ = ()
Expand Down Expand Up @@ -320,21 +319,34 @@ def add_to_row(self, row: special_endpoints.MessageActionRowBuilder) -> special_
)


@dataclasses.dataclass(slots=True, kw_only=True)
class MenuContext(base.MessageResponseMixinWithEdit[hikari.ComponentInteraction]):
"""Dataclass representing the context for an invocation of a component that belongs to a menu."""
"""Class representing the context for an invocation of a component that belongs to a menu."""

menu: Menu
"""The menu that this context is for."""
interaction: hikari.ComponentInteraction
"""The interaction that this context is for."""
component: base.BaseComponent[special_endpoints.MessageActionRowBuilder]
"""The component that triggered the interaction for this context."""
__slots__ = ("_interaction", "_should_re_resolve_custom_ids", "_should_stop_menu", "_timeout", "component", "menu")

_timeout: async_timeout.Timeout = dataclasses.field(repr=False)
def __init__(
self,
menu: Menu,
interaction: hikari.ComponentInteraction,
component: base.BaseComponent[special_endpoints.MessageActionRowBuilder],
_timeout: async_timeout.Timeout,
) -> None:
super().__init__()

_should_stop_menu: bool = dataclasses.field(init=False, default=False, repr=False)
_should_re_resolve_custom_ids: bool = dataclasses.field(init=False, default=False, repr=False)
self.menu: Menu = menu
"""The menu that this context is for."""
self._interaction: hikari.ComponentInteraction = interaction
self.component: base.BaseComponent[special_endpoints.MessageActionRowBuilder] = component
"""The component that triggered the interaction for this context."""

self._timeout: async_timeout.Timeout = _timeout
self._should_stop_menu: bool = False
self._should_re_resolve_custom_ids: bool = False

@property
def interaction(self) -> hikari.ComponentInteraction:
"""The interaction that this context is for."""
return self._interaction

@property
def guild_id(self) -> hikari.Snowflake | None:
Expand Down
21 changes: 14 additions & 7 deletions lightbulb/components/modals.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@

import abc
import asyncio
import dataclasses
import typing as t
import uuid

Expand Down Expand Up @@ -95,14 +94,22 @@ def add_to_row(self, row: special_endpoints.ModalActionRowBuilder) -> special_en
)


@dataclasses.dataclass(slots=True, kw_only=True)
class ModalContext(base.MessageResponseMixinWithEdit[hikari.ModalInteraction]):
"""Dataclass representing the context for a modal interaction."""
"""Class representing the context for a modal interaction."""

modal: Modal
"""The modal this context is for."""
interaction: hikari.ModalInteraction
"""The interaction this context is for."""
__slots__ = ("_interaction", "modal")

def __init__(self, modal: Modal, interaction: hikari.ModalInteraction) -> None:
super().__init__()

self.modal: Modal = modal
"""The modal this context is for."""
self._interaction: hikari.ModalInteraction = interaction

@property
def interaction(self) -> hikari.ModalInteraction:
"""The interaction this context is for."""
return self._interaction

@property
def guild_id(self) -> hikari.Snowflake | None:
Expand Down
Loading

0 comments on commit f5d1bb7

Please sign in to comment.