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.
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.
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.
@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:
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.
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.
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:
User A uses
/test_buttonand User A clicks the resulting button,User A uses
/test_buttonand User B clicks the resulting button,User A uses
/test_nested_buttonand User A clicks the resulting button,User A uses
/test_nested_buttonand 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:
The logging wrapper logs the attempted invocation;
The component callback is executed successfully;
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:
The logging wrapper logs the attempted invocation;
The component callback is executed successfully;
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:
The logging wrapper logs the attempted invocation;
The user check wrapper passes because the user clicking the button (User A) is the original command author (User A);
The component callback is executed successfully;
The user check wrapper does not do anything after the component finishes;
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:
The logging wrapper logs the attempted invocation;
The user check wrapper fails because the user clicking the button (User A) is NOT the original command author (User B). An
InvalidUserErroris raised;The component callback is NOT executed;
The user check wrapper does not not get to continue running;
The logging wrapper does not get to continue running;
The exception handler catches the
InvalidUserErrorand 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¶
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"))