Tanjun uses a more functional approach than other Discord libraries. Functional in this post does not mean, provides more utility, but rather is written in a functional programming style. One good example is how hikari and Tanjun implement a Bot.
In other Discord libraries you create a new class that inherits from a base
the library provides. After you inherit from that base class you can then add any new
methods, attributes or whatever else you need. Common practice in other libraries
is to then define your own
__init__ method where you setup dependencies that your Bot needs.
Common examples would be connections to Redis, Postgres, or any external service.
Once those connections are validated and working you could attach them to your bot via
self.redis = aioredis.Redis(...), or whatever your situation needs. From that point on
all your plugins can access
self.redis for your Redis client!
If you followed along with the last two parts of this series, you might start to see the issue.
If Tanjun doesn't want you to inherit from it's classes, like
Client, to add attributes or methods,
how do you access those needed dependencies? Tanjun provides a wonderful built in solution for
those cases via
tanjun.injected, called Dependency Injection.
How does Injection Work?
Dependency Injection allows you to register specific objects or functions and then access those registered objects in any async Tanjun callback. The only real limit to DI is that last part, "in any async Tanjun Callback." That means DI normally applies to command handlers, listeners, DI callbacks only. There are few exceptions to this for other builtin tanjun elements, like tanjun.conversion, where non-async code can be injected.
Injecting into a command or listener is simple to do. Just setup your component/command as you normally would then add an extra parameter. In the example below we create new command
my_cmd_func and inject
hikari.GatewayBot into that command. In python we are able to define "default parameters" on a function. This basically says "if no value is provided, use this" and that allows the injecting magic to happen. We can define that
inj_bot should default to
... is whatever object you registered. Tanjun will then do all the work to make
inj_bot resolve to that object!
1 2 3 4 5 6 7 8
Note: To inject your dependencies you can use
tanjun.injected2 which is an alias of the previous.
What can be Injected?
Anything really! As mentioned earlier you can register your own objects, functions, instiate other libs like Redis or Postgres and register them. There's also a few ways to inject without registering first!
The first is to use the defaults tanjun preregisters for you. Once you start your
tanjun.Client, tanjun automatically registers these objects via type3:
The second is to inject local function. Below we reuse the slash command from earlier, but instead of injecting the
hikari.GatewayBot directly by it's
type we pass in a callback function
my_injector is just a regular async python function, but because we are using it as a callback that tanjun executes (via
tanjun.inject) we can actually use DI on this callback!
1 2 3 4 5 6 7 8 9 10
You can also cache the result of DI's if they are resource intensive! This is most commonly seen on networking tasks like fetching webpages or getting a bot prefix from a database. It's also very simple to setup! Just change
tanjun.cached_inject4 and provide a
expire_after parameter to manage the cache's time to live. Tanjun will then run the injection normally, but cache it's result for
expire_after and reuse that result instead of re-running the code!
The last option is to register your own types into Tanjun. So let's look at how to do that!
How to Set Dependencies
If you have a dependency that requires some kind of configuration before it can be used, it's probably worth registering your own dependency. Luckily, tanjun provides
tanjun.Client.set_type_dependency5 for this case. Below is a simple example to setup a Redis Client and register it. We start by setting up our bot and client. Then we configure an
aioredis.Redis instance. Finally we register that new instance to the
aioredis.Redis type with tanjun.
1 2 3 4 5 6 7 8 9 10
Now if we were to set
tanjun.inject(type=aioredis.Redis) on any tanjun callbacks, we would be passed this instance that we configured.
It is possible to use
set_type_dependency with functions or even lambda's with
set_type_dependency too. Just like with the previous example you can set the return of these callbacks to any
type you want, even from other libraries like
aioredis.Redis. Functions do not have to be registered though! Like the example in What Can Be Injected showed,
tanjun.inject will also search the local scope for the referenced callback too.
Injecting with Multiple Types
So far we have shown how to inject based on a single type, but tanjun allows you to pass in multiple types too. Tanjun will then check those types in order as well as the literal
Union if no other types resolve. In the example below tanjun will try to find a
MyBotProxy type first, then
hikari.GatewayBot, then finally
| is just shorthand for
Union). Assuming the following two function snippets were decorated properly for tanjun as a Slash command, they would act identical.
Defaults & Optional for Injection
If you are unsure that a dependency is registered it is also possible to provide a default. Assuming again that the following functions were decorated properly, they would function identically:
1 2 3
In all of the above cases tanjun will attempt to find
MyBotProxy, and if it cannot will return
None without any exceptions.
Starting Injected Dependencies that require the Bot Started
Sometimes you will have dependencies that require async methods to start or require your bot already be started up and running. We can handle these kinds of situations by registering callbacks67! Let's look at some code and see how we could set this up:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
This will setup your dependency same as with the Redis example, but we also add the two final lines.
add_client_callback lets us tell tanjun "run this callback when the bot starts/closes". Tanjun provides hooks for a number of useful events, and STARTING/CLOSING can be used to ensure dependencies with async startup's can be automatically started.
Now with a better understanding of what DI is and how it works in Tanjun we can start working on more concrete examples! Next post we will cover setting up a Reaction Role system with slash commands. The end result will be a plugin that allows you to add a Emote/Emoji and Role combination that the Bot will store in Postgres and Redis. When member sends the Role.name or Emoji/Emote in a specific channel, the Bot will toggle the role on the member. This will require setting up custom injectors for
Resources & Link's
hikari-tanjun Documentation, Faster Speeding tanjun.inject, https://tanjun.cursed.solutions/master/tanjun.html#inject ↩
hikari-tanjun Documentation, Faster Speeding tanjun.injecting.injected, https://tanjun.cursed.solutions/master/tanjun/injecting.html#injected ↩
hikari-tanjun Source Code tanjun.client, Faster Speeding, https://github.com/FasterSpeeding/Tanjun/blob/master/tanjun/clients.py#L615 ↩
hikari-tanjun Documentation, Faster Speeding, https://tanjun.cursed.solutions/master/tanjun.html#cached_inject ↩
hikari-tanjun Documentation tanjun.injecting.InjectorClient.set_type_dependency, Faster Speeding, https://tanjun.cursed.solutions/master/tanjun/injecting.html#InjectorClient.set_type_dependency ↩
hikari-tanjun Documentation tanjun.Client.add_client_callback, Faster Speeding, https://tanjun.cursed.solutions/master/tanjun.html#Client.add_client_callback ↩
hikari-tanjun Documentation tanjun.ClientCallbackNames, Faster Speeding, https://tanjun.cursed.solutions/master/tanjun/clients.html#ClientCallbackNames ↩