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 Bot
class
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 tanjun.inject(type=...)
1, where ...
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.inject
, ortanjun.injected
2 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:
tanjun.Client
hikari.GatewayBot
hikari.api.RESTClient
hikari.api.Cache
hikari.api.EventManager
hikari.api.InteractionServer
hikari.api.ShardAware
hikari.api.VoiceComponent
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
. 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 |
|
Caching
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.inject
to tanjun.cached_inject
4 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_dependency
5 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 Union
(|
is just shorthand for Union
). Assuming the following two function snippets were decorated properly for tanjun as a Slash command, they would act identical.
1 2 |
|
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.
Next Time!
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 aiopg
and aioredis
!
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 ↩