5 Tips for Making Better Components in Tanjun

Let's take a quick break from heavy learning for 5 quick tips for better components!

By Asterisk KatieKat

Tanjun and Hikari go through rapid development! With that come a lot of improvements even between articles. Here's a few quick tips that have changed that will streamline your code!

An Example Generic Plugin

Let's look at a simple Tanjun plugin outline. First we define a new component, my_component on line 3. Line 5 we define a generic command handler called my_cmd_handler. This handler accepts ctx: tanjun.abc.Context, so that it can work as a callback for either a Slash command or a Message command. Line 8 starts the Slash command definition with @my_component.with_command, this decorator is what connects your command to the Component() that Tanjun later loads into the Client. Line 14 begins a new Message command definition with the same .with_command decorator. Lines 20 and 24 define the loader and unloader functions respectively. These functions contain the logic Tanjun uses to add and remove this component from the Client.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import tanjun

my_component = tanjun.Component()

async def my_cmd_handler(ctx: tanjun.abc.Context, str_opt: str):
    ...

@my_component.with_command
@tanjun.with_str_slash_option(...)
@tanjun.as_slash_command(...)
async def my_slash_cmd(ctx: tanjun.abc.SlashContext, str_opt: str):
    await my_cmd_handler(ctx, str_opt)

@my_component.with_command
@tanjun.with_argument(...)
@tanjun.as_message_command(...)
async def my_message_cmd(ctx: tanjun.abc.SlashContext, str_opt: str):
    await my_cmd_handler(ctx, str_opt)

@tanjun.as_load
def loader(client: tanjun.Client):
    client.add_component(component.copy())

@tanjun.as_unload
def loader(client: tanjun.Client):
    client.remove_component(component.copy())

A simple plugin so far, right? Well let's look at some new ways we can make this more efficient!

1) Component().load_from_scope()

Instead of adding @component.with_command to every command definition, Tanjun provides a lovely Component().load_from_scope() function. Not only does this reduce some code duplication, you can simplify it even further. Instead of setting up two individual commands we can stack them all on one:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import tanjun

my_component = tanjun.Component()

@tanjun.with_argument(...)
@tanjun.as_message_command(...)
@tanjun.with_str_slash_option(...)
@tanjun.as_slash_command(...)
async def my_cmd_handler(ctx: tanjun.abc.Context, str_opt: str):
    ...

my_component.load_from_scope()

@tanjun.as_load
def loader(client: tanjun.Client):
    client.add_component(component.copy())

@tanjun.as_unload
def loader(client: tanjun.Client):
    client.remove_component(component.copy())

.load_from_scope() works on Slash/Message command groups as well. You just need to define your groups slightly differently. Normally when you define a command group, you have to warp it in a command function of its own. For a slash command that would look like the example below.

1
2
3
my_grp = tanjun.with_command(
    tanjun.slash_command_group(...)
)

You would then use @my_grp as a decorator to setup subcommands in your plugin. .from_local_scope() provides a useful shorthand for this too! Just drop the additional .as_message/slash_command on your command group and setup .from_local_scope() like before:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import tanjun

my_component = tanjun.Component()

my_grp = tanjun.slash_command_group(...)

@my_grp.add_command
@tanjun.with_str_slash_option(...)
@tanjun.as_slash_command(...)
async def my_cmd_handler(ctx: tanjun.abc.Context, str_opt: str):
    ...

@tanjun.as_slash_command(...)
async def other_cmd(ctx: tanjun.abc.Context, str_opt: str):
    ...

my_component.load_from_scope()

@tanjun.as_load
def loader(client: tanjun.Client):
    client.add_component(component.copy())

@tanjun.as_unload
def loader(client: tanjun.Client):
    client.remove_component(component.copy())

This code will now load not only your SlashCommandGroup with its proper subcommand setup, but it will also load the standalone other_cmd separtely from the group and subcommand.

2) Component().make_loader()

To define a plugin in Tanjun, you have to have a loader and unloader function. These two functions provide the logic for how Tanjun adds and removes your Component from the Client. In previous versions of Tanjun you would need to rewrite these two functions at the bottom of every plugin file. A recent release of Tanjun provides a new Component().make_loader() method to do this for you! If your load/unload doesn't require any custom logic try it out! Also notice that we can chain the previous tip with this one: Component().load_from_scope().make_loader(), and this is perfectly valid:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import tanjun

my_component = tanjun.Component()

@tanjun.with_argument(...)
@tanjun.as_message_command(...)
@tanjun.with_str_slash_option(...)
@tanjun.as_slash_command(...)
async def my_cmd_handler(ctx: tanjun.abc.Context, str_opt: str):
    ...

my_component.load_from_scope().make_loader()

3) Stop Restarting your Bot, Write Load/Unload Command's

Restarting your bot every time you make a minor change to your code can be annoying. Restarting your bot can mean losing your in memory cache, a slow startup, or getting your Token throttled if you are just testing! Instead try writing some commands to load and unload plugins via Discord:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import tanjun

@tanjun.with_str_slash_option("module_name", "The module to target.")
@tanjun.as_slash_command("reload_module", "Reloads a module.")
async def reload_module(
    ctx: tanjun.abc.SlashContext, module_name: str, client: tanjun.Client = tanjun.injected(type=tanjun.Client)
):
    """Reload a module in Tanjun"""
    try:
        client.reload_modules(module_name)
    except ValueError:
        client.load_modules(module_name)

    await client.declare_global_commands()
    await ctx.respond("Reloaded!")


@debug.with_command
@tanjun.with_str_slash_option("module_name", "The module to target.")
@tanjun.as_slash_command("unload_module", "Removes a module.")
async def unload_module(
    ctx: tanjun.abc.SlashContext, module_name: str, client: tanjun.Client = tanjun.injected(type=tanjun.Client)
):
    """Unload a module in Tanjun"""
    try:
        client.unload_modules(module_name)
    except ValueError:
        await ctx.respond("Couldn't unload module...")
        return

    await client.declare_global_commands()
    await ctx.respond("Unloaded!")

    @tanjun.with_str_slash_option("module_name", "The module to reload.")
@tanjun.as_slash_command("load_module", "Loads a module.")
async def load_module(
    ctx: tanjun.abc.SlashContext, module_name: str, client: tanjun.Client = tanjun.injected(type=tanjun.Client)
):
    """Load a module in Tanjun"""
    try:
        client.load_modules(module_name)
    except ValueError:
        await ctx.respond("Can't find that module!")
        return

    await client.declare_global_commands()
    await ctx.respond("Loaded!")


component = tanjun.Component().load_from_scope().make_loader()

This short 50 line plugin provides all the functionality you need to load, unload, and reload any plugins. Just run /reloadpath.to.plugin` and just that plugin will be reloaded.

Note: This will not reload the plugins dependencies. If you change other files via pip or other non-component files, a restart will be required!

Note 2: Using this method will not cause newly created commands or renamed commands to appear! Tanjun only declares commands when the bot starts up.

4) Learn how Dependency Injection works

We already wrote a full post on Dependency Injection and how useful it can be in Tanjun. To give a tl;dr, DI allows you to dynamically add preconfigured objects to a callback via it's type:

1
2
3
4
5
6
@tanjun.with_str_slash_option("module_name", "The module to target.")
@tanjun.as_slash_command("reload_module", "Reloads a module.")
async def reload_module(
    ctx: tanjun.abc.SlashContext, module_name: str, client: tanjun.Client = tanjun.injected(type=tanjun.Client)
):
    ...

With tanjun.injected we are able to tell Tanjun we want the preconfigured tanjun.Client. Tanjun then checks an internal list to see if any objects are registered for that type. By default Tanjun registers tanjun.Client and a few others for you. We won't go too much deeper into DI here, but it is an essential concept to understand in Tanjun.

5) Please use Logging

A lot of beginners will end up using print to help debug their code. print is one of the first things you learn to get information out to the console, but it is not a good method to use long term. print doesn't have any built in way to filter information and there's no way to toggle all printing for a file or project. That's just a few reasonsing why logging is better! So let's look at how to utilize this module better.

Firstly Hikari preconfiures logging for you! By default, running the following code in a Hikari file will be valid. Hikari will automatically add its specific formatting type and add color to your log! info gets logged as yellow and error gets logged as red, neat!

1
2
3
4
5
6
7
import logging

logging.debug(...)
logging.info(...)
logging.warn(...)
logging.error(...)
logging.critical(...)

There's a few more things you can do to better use logging too. One good habit is to use logging.getLogger so you're not using your root logger. By default import logging will climb up your python files to find a previous working logger. If you have not setup any other loggers, by default you will get the root logger hikari setup. You can avoid that with the single line below:

1
2
3
4
5
6
7
import logging

logger = logging.getLogger(__name__)

# Now use logger! Works same as above!

logger.info(...)

Lastly, log your exceptions!! The logging plugin provides a way to log exceptions with the full traceback too:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import logging

logger = logging.getLogger(__name__)

try:
    x = 5 / 0
except ZeroDivisionError:
    logger.exception("Oh no!", exc_info=True)

# Output

ERROR:root:test
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero

hikari.GatewayBot also provides a way to set a project wide LogLeve3. This provides a fast and easy way to toggle what information shows when your bot runs. Just passing in the LogLevel you need to see and Hikari will preconfigure the logs to only show that Level and below!


Next Time

We took a break today to review a couple quick tips! Next time we will be diving back into more advanced topics. More about Yuyo, the hikari utility library made by FasterSpeeding, the wonderful owner of Tanjun. More about setting up Dependency Injection properly. Check back soon!

Resources & Links


  1. hikari-tanjun Documentation tanjun.Component.load_from_scope, Faster Speeding, https://tanjun.cursed.solutions/master/tanjun.html#Component.load_from_scope 

  2. hikari-tanjun Documentation tanjun.Component.make_loader, Faster Speeding, https://tanjun.cursed.solutions/master/tanjun.html#Component.make_loader 

  3. hikari Documentation, Davfsa, https://www.hikari-py.dev/hikari/impl/bot.html#hikari.impl.bot.GatewayBot 

We appreciate Donations & Tips!

We would much rather be making cool tutorials, streaming code, and making accessibility tools. Unfortunately we have bills and a job. Hopefully one day, with enough Patrons and donations this can be fulltime!

Authors

Asterisk

Role: Host Apparently Normal Part

Asters's current pfp

Pre-2020 Aster consider themself a very neurotypical male. As the COVID pandemic hit it magnified many issues Aster had been able to hide or compensate for. Since diagnosis Aster now tries to use their previous skills with teaching and speaking to help spread education and awareness about DID/OSDD. In their spare time Aster enjoys programming, teaching, and helping build the Official Game Theory Discord.

>> Learn More about Aster and The Patchwork Collective

Katie is the Social Protector for the Patchwork Collective and the first Part to start communicating with Aster in early 2021. Most of Katie's System Role revolves around handling social situations, work, and dealing with external strangers. Once the internal walls came down Katie quickly found her interests in research, programming, and writing.

>> Learn More about Katie and The Patchwork Collective