Component Manager

Step-by-Step

A simple example on the use of component managers with disnake-compass.

First, we create a new component manager. A call to get_manager() without arguments returns the root manager. You can think of this in much the same way as logging.getLogger(). A manager handles components registered to itself and any of its children. To make sure a bot can actually interact with the manager, we must register the bot to the manager.

examples/manager.py - create root manager
bot = commands.InteractionBot()
manager = disnake_compass.get_manager()
manager.add_to_client(bot)

We can create a child manager by entering a name into get_manager(), the returned manager will be a child of the root manager. Again similar to logging.getLogger(), we can create a complex parent-child hierarchy by using dot-qualified names.

examples/manager.py - create child manager
foo_manager = disnake_compass.get_manager("foo")
deeply_nested_manager = disnake_compass.get_manager("foo.bar.baz")

Note

Any missing bits will automatically be filled in– the above snippet has implicitly created a manager named “foo.bar”.

To register a component to a manager, we use ComponentManager.register(). For purposes that will become clear in the later on in this example, we will register a component to both our foo_manager and our deeply_nested_manager. To this end, we will use the button example as simple components to work with. Since we will need it later, we immediately add a command to send each button.

examples/manager.py - register components
@foo_manager.register
class FooButton(disnake_compass.RichButton):
    label: str | None = "0"

    count: int

    async def callback(
        self, interaction: disnake.MessageInteraction[disnake.Client]
    ) -> None:
        self.count += 1
        self.label = str(self.count)

        component = await self.as_ui_component()
        await interaction.response.edit_message(components=component)


@deeply_nested_manager.register
class FooBarBazButton(disnake_compass.RichButton):
    label: str | None = "0"

    count: int

    async def callback(
        self, interaction: disnake.MessageInteraction[disnake.Client]
    ) -> None:
        self.count += 1
        return True

    return False


@bot.slash_command()
async def test_button(interaction: disnake.CommandInteraction[disnake.Client]) -> None:
    component = await FooButton(count=0).as_ui_component()
    await interaction.response.send_message(components=component)


@bot.slash_command()
async def test_nested_button(
    interaction: disnake.CommandInteraction[disnake.Client],

Customizing your component manager

For most use cases, the default implementation of the component manager should suffice. Two methods of interest to customise your managers without having to subclass them are ComponentManager.as_callback_wrapper() and ComponentManager.as_exception_handler().

Callback Wrappers

A callback wrapper is essentially a context mananger. In short, this is a function that does some setup, yields to let the “managed” callback run, and then gets to do something again after the callback finished running. Callback wrappers, like context managers, can be nested.

ComponentManager.as_callback_wrapper() wraps the callbacks of all components registered to that manager along with those of its children. Therefore, if we were to add a callback wrapper to the root manager, we would ensure it applies to all components. For example, say we want to log all component interactions:

examples/manager.py - creating a logging wrapper
 1        self.label = str(self.count)
 2
 3        component = await self.as_ui_component()
 4        await interaction.response.edit_message(components=component)
 5
 6
 7@manager.as_callback_wrapper
 8async def wrapper(
 9    manager: disnake_compass.ComponentManager,
10    component: disnake_compass.api.RichComponent,
11    interaction: disnake.Interaction[disnake.Client],
12):
13    print(
14        f"User {interaction.user.name!r} interacted with component {type(component).__name__!r}...",
15    )
16
17    yield

Tip

For actual production code, consider using logging instead of print.

This creates a wrapper that prints who interacted with the component (lines 7-10), lets the component run (line 12), and finally prints that the component interaction was successful (lines 14-17).

Note

Anything after the yield is ignored if the callback raised an error, unless the yield is wrapped in a try-except block. This works in the same way as normal contextmanagers would.

This feature can also be used as a check. By raising an exception before the component callback is invoked, you can prevent it from being invoked entirely. The exception is then also passed to exception handlers. For example, we create a wrapper that allows only the original slash command author to interact with any components on this manager.

examples/manager.py - creating a check
 1        f"User {interaction.user.name!r}s interaction with component"
 2        f" {type(component).__name__!r} was successful!",
 3    )
 4
 5
 6class InvalidUserError(Exception):
 7    def __init__(self, message: str, user: disnake.User | disnake.Member) -> None:
 8        super().__init__(message)
 9        self.message = message
10        self.user = user
11
12
13@deeply_nested_manager.as_callback_wrapper
14async def check_wrapper(
15    manager: disnake_compass.api.ComponentManager,
16    component: disnake_compass.api.RichComponent,
17    interaction: disnake.Interaction[disnake.Client],
18):
19    if (
20        isinstance(interaction, disnake.MessageInteraction)
21        and interaction.message.interaction
22        and interaction.user != interaction.message.interaction.user

This callback wrapper contains a check that only fires for message interactions (line 15), where the message must have been sent as interaction response (line 16), and the component user is NOT the same as the original interaction user (line 17). If all the conditions are satisfied we raise a custom error for convenience (lines 19-20), otherwise, we yield to the wrapped callback (line 22).

Note

All component wrappers receive the component instance as-is. This means that any modifications done to the component are reflected inside other wrappers and the wrapped callback itself.

Exception handlers

Similarly, we can create an exception handler for our components using ComponentManager.as_exception_handler(). An exception handler function should return True if the error was handled, and False or None otherwise.

The default implementation hands the exception down to the next handler until it either is handled or reaches the root manager. If the root manager is reached (and does not have a custom exception handler), the exception is logged.

To demonstrate this, we will make a custom error handler only for the deeply_nested_manager. Consider the previously established user check wrapper. We raised a custom exception there, and we wish to handle it in this exception handler.

examples/manager.py - creating an exception handler
        raise InvalidUserError(message, interaction.user)

    yield


@deeply_nested_manager.as_exception_handler
async def error_handler(
    manager: disnake_compass.ComponentManager,
    component: disnake_compass.api.RichComponent,
    interaction: disnake.Interaction[disnake.Client],
    exception: Exception,
):
    if isinstance(exception, InvalidUserError):

Note

You do not need to explicitly return False. Returning None – and thus a blank return statement – is sufficient. Explicitly returning False is simply preferred for clarity.

Using the components and commands registered in the register components codeblock combined with the logging wrapper and the user check wrapper, we can assess the following four cases:

  1. User A uses /test_button and User A clicks the resulting button,

  2. User A uses /test_button and User B clicks the resulting button,

  3. User A uses /test_nested_button and User A clicks the resulting button,

  4. User A uses /test_nested_button and User B clicks the resulting button.

The invoked command is /test_button, so the button in question is a FooButton, which is registered to the foo_manager. This manager does not have a callback wrapper registered to it. However, the root manager has the logging wrapper.

As the button is invoked, the following mechanisms are triggered in order:

  1. The logging wrapper logs the attempted invocation;

  2. The component callback is executed successfully;

  3. The user check wrapper does not do anything after the component finishes;

The invoked command is /test_button, so the button in question is a FooButton, which is registered to the foo_manager. This manager does not have a callback wrapper registered to it. However, the root manager has the logging wrapper.

As the button is invoked, the following mechanisms are triggered in order:

  1. The logging wrapper logs the attempted invocation;

  2. The component callback is executed successfully;

  3. The user check wrapper does not do anything after the component finishes;

Tip

As foo_manager does not see the callback wrapper registered to deeply_nested_manager, it’s irrelevant who clicked the button, as the check simply doesn’t apply.

The invoked command is /test_nested_button, so the button in question is a FooBarBazButton, which is registered to the deeply_nested_manager. This manager has the user check wrapper registered to it. Furthermore, the root manager has the logging wrapper.

As the button is invoked, the following mechanisms are triggered in order:

  1. The logging wrapper logs the attempted invocation;

  2. The user check wrapper passes because the user clicking the button (User A) is the original command author (User A);

  3. The component callback is executed successfully;

  4. The user check wrapper does not do anything after the component finishes;

  5. The logging wrapper logs the successful invocation of the component.

The invoked command is /test_nested_button, so the button in question is a FooBarBazButton, which is registered to the deeply_nested_manager. This manager has the user check wrapper registered to it. Furthermore, the root manager has the logging wrapper.

As the button is invoked, the following mechanisms are triggered in order:

  1. The logging wrapper logs the attempted invocation;

  2. The user check wrapper fails because the user clicking the button (User A) is NOT the original command author (User B). An InvalidUserError is raised;

  3. The component callback is NOT executed;

  4. The user check wrapper does not not get to continue running;

  5. The logging wrapper does not get to continue running;

  6. The exception handler catches the InvalidUserError and returns True. The exception is deemed successfully handled, and no further handlers are triggered.

Important

Callback wrappers are traversed from the root manager down to the child that invokes the component.

Exception handlers are traversed in reverse order: from the child down to the root manager.

Source Code

View on GitHub: manager.py

examples/manager.py
  1"""A simple example on the use of component managers with disnake-compass."""
  2
  3import os
  4
  5import disnake
  6import disnake_compass
  7from disnake.ext import commands
  8
  9bot = commands.InteractionBot()
 10
 11manager = disnake_compass.get_manager()
 12manager.add_to_client(bot)
 13
 14foo_manager = disnake_compass.get_manager("foo")
 15deeply_nested_manager = disnake_compass.get_manager("foo.bar.baz")
 16
 17
 18@foo_manager.register
 19class FooButton(disnake_compass.RichButton):
 20    label: str | None = "0"
 21
 22    count: int
 23
 24    async def callback(
 25        self, interaction: disnake.MessageInteraction[disnake.Client]
 26    ) -> None:
 27        self.count += 1
 28        self.label = str(self.count)
 29
 30        component = await self.as_ui_component()
 31        await interaction.response.edit_message(components=component)
 32
 33
 34@deeply_nested_manager.register
 35class FooBarBazButton(disnake_compass.RichButton):
 36    label: str | None = "0"
 37
 38    count: int
 39
 40    async def callback(
 41        self, interaction: disnake.MessageInteraction[disnake.Client]
 42    ) -> None:
 43        self.count += 1
 44        self.label = str(self.count)
 45
 46        component = await self.as_ui_component()
 47        await interaction.response.edit_message(components=component)
 48
 49
 50@manager.as_callback_wrapper
 51async def wrapper(
 52    manager: disnake_compass.ComponentManager,
 53    component: disnake_compass.api.RichComponent,
 54    interaction: disnake.Interaction[disnake.Client],
 55):
 56    print(
 57        f"User {interaction.user.name!r} interacted with component {type(component).__name__!r}...",
 58    )
 59
 60    yield
 61
 62    print(
 63        f"User {interaction.user.name!r}s interaction with component"
 64        f" {type(component).__name__!r} was successful!",
 65    )
 66
 67
 68class InvalidUserError(Exception):
 69    def __init__(self, message: str, user: disnake.User | disnake.Member) -> None:
 70        super().__init__(message)
 71        self.message = message
 72        self.user = user
 73
 74
 75@deeply_nested_manager.as_callback_wrapper
 76async def check_wrapper(
 77    manager: disnake_compass.api.ComponentManager,
 78    component: disnake_compass.api.RichComponent,
 79    interaction: disnake.Interaction[disnake.Client],
 80):
 81    if (
 82        isinstance(interaction, disnake.MessageInteraction)
 83        and interaction.message.interaction
 84        and interaction.user != interaction.message.interaction.user
 85    ):
 86        message = "You are not allowed to use this component."
 87        raise InvalidUserError(message, interaction.user)
 88
 89    yield
 90
 91
 92@deeply_nested_manager.as_exception_handler
 93async def error_handler(
 94    manager: disnake_compass.ComponentManager,
 95    component: disnake_compass.api.RichComponent,
 96    interaction: disnake.Interaction[disnake.Client],
 97    exception: Exception,
 98):
 99    if isinstance(exception, InvalidUserError):
100        message = f"{exception.user.mention}, {exception.message}"
101        await interaction.response.send_message(message, ephemeral=True)
102        return True
103
104    return False
105
106
107@bot.slash_command()
108async def test_button(interaction: disnake.CommandInteraction[disnake.Client]) -> None:
109    component = await FooButton(count=0).as_ui_component()
110    await interaction.response.send_message(components=component)
111
112
113@bot.slash_command()
114async def test_nested_button(
115    interaction: disnake.CommandInteraction[disnake.Client],
116) -> None:
117    component = await FooBarBazButton(count=0).as_ui_component()
118    await interaction.response.send_message(components=component)
119
120
121bot.run(os.getenv("EXAMPLE_TOKEN"))