Creating custom parsers¶
New in version 1.1.0b1
This feature was added in version 1.1.0b1 - pip install niobot>=1.1.0b1
NioBot is loaded with some sane defaults for basic types. If you were to pass the following function as a command:
NioBot would intelligently detectctx
as the context (and realise it's not intended to be something the user provides)
and arg1
as an integer, arg2
as a float, and arg3
as a string.
Then, when my_command is run, the first argument would be converted to an integer from the message, the second as a
float, and so on.
However, these built-in types only go so far - they're limited to a subset of python built-in types, and a couple matrix-specific things (i.e. rooms and events and matrix.to links).
This looks worryingly complicated
Pre 1.1.0b1
, parsers were far too flexible and inconsistent. The old structure only required you had a singular
synchronous function that took three arguments: ctx
, arg
, and user_input
.
This was a problem for a couple reasons:
- The flexibility meant that it was difficult to get a uniform experience across all parsers.
- This was still not very flexible for customising the parsers, and often required wrappers.
However, since 1.1.0b1, the parser structure now uses two new ABC classes, Parser
and StatelessParser
, to ensure
that all parsers are consistent and easy to use, while still being flexible and configurable.
As a result of using classes though, some parsers can still feel a little bit bulky. But that's okay!
Creating a parser¶
Creating a parser is actually really easy. All the library needs from you is a class that subclasses either of the
parser ABCs (see below), and implements the __call__
dunder method!
For example:
from niobot.utils.parsers import StatelessParser
from niobot import CommandParserError
class UserParser(StatelessParser):
def __call__(self, ctx: Context, arg: Argument, value: str):
# Do some stuff here
if "@" not in value:
# Always raise CommandParserError when its an invalid value - this allows for proper error handling.
raise CommandParserError("Invalid user ID. Expected @user:example.com")
return value[1:] # Remove the @ from the user ID
You can then use this parser in your commands like so:
import niobot
import typing
from my_parsers import UserParser
bot = niobot.NioBot(...)
@bot.command()
async def my_command(ctx: niobot.Context, user: typing.Annotated[str, UserParser]):
# typing.Annotated[real_type, parser] is a special type that allows you to specify a parser for a type.
# In your linter, `user` will be `str`, not `UserParser`.
await ctx.respond("User ID: {!s}".format(user))
What if I need to await
in my parser?¶
If you need to use asynchronous functions in your parser, you can simply return the coroutine in __call__, like below:
class MyParser(Parser):
async def internal_caller(self, ctx: Context, arg: Argument, value: str):
# Do some stuff here
await asyncio.sleep(1) # or whatever async function you need to call
return value
def __call__(self, *args, **kwargs):
return self.internal_caller(*args, **kwargs) # this returns a coroutine.
If you want to use a parser like this in your code manually, you can always use niobot.utils.force_await, which will await a coroutine if it needs awaiting, or simply returns the input if it's not a coroutine.
from niobot.utils import force_await
coro = MyParser()(...)
# If you're not sure if coro is a coroutine or not, you can use force_await
parsed = await force_await()
# Otherwise, simply await the result
coro = await MyParser()(...)
What's the difference between Parser and StatelessParser?¶
Great question!
With parsers, there's often a split between complicated/customisable, and fixed parsers. For example, IntegerParser is a customisable parser - You can pass options to it while initialising it, and it will use those options to parse the input. However, on the contrary, BooleanParser is a fixed parser - it does not take any options, and will always convert the input to a boolean.
Basically, StatelessParser
never needs to access self
while parsing. Parser
can.
Which should I choose?¶
If you're writing a parser that needs to be customisable and takes options, then you should use Parser
. Otherwise,
if you don't need self
, then you should use StatelessParser
.