I am writing a simple Telegram bot in Python 3 using the python-telegram-bot
library. A command
is a Telegram message that looks like this: /command_name arg1 arg2 arg3...
. The bot should handle the users' messages by changing its internal state and making responses to the users.
I was not very happy with the 'command handling' provided by the library because this code is not beautiful at all and is very noisy:
def caps(update, context):
text_caps = ' '.join(context.args).upper()
context.bot.send_message(chat_id=update.message.chat_id, text=text_caps)
caps_handler = CommandHandler('caps', caps)
dispatcher.add_handler(caps_handler)
So I wanted to create a more convenient way, and now a command looks like this:
@cmd('hello')
def cmd_hello(msg, a:int, b:[float], c:str):
s = ''
s += 'Number: '+str(a)+'\n'
s += 'Floats: '+str(b)+'\n'
s += 'String: '+c
return s
If i send this command: /hello 1 3.14 5 3.7 world
to the bot, the response will be:
Number: 1
Floats: [3.14, 5.0, 3.7]
String: world
I am using the inspect
module to read the argument annotations of a function and store them in a dict
:
Cmd = namedtuple('Cmd',['func','patterns'])
commands = dict()
def cmd(name):
def deco(func):
args = []
sig = signature(func)
for arg_name, param in sig.parameters.items():
ann = param.annotation
if ann:
args.append(ann)
else:
args.append(str)
args.pop(0)
commands[name] = Cmd(func, tuple(args))
return func
return deco
The process_args
function takes: 1) a list of raw (str
) arguments that a bot receives in a message, 2) a list of patterns to match the arguments; and returns a list of processed (converted) arguments or raises CmdError
if the arguments are incorrect.
def process_args(args, patterns):
patterns = list(patterns)
processed = []
while patterns:
if not args:
raise CmdError("Not enough arguments")
pattern = patterns.pop(0)
if pattern in (int, float, str):
arg = args[0]
try:
converted = pattern(arg)
except Exception:
raise CmdError(f"Cannot parse argument {arg} as {pattern.__name__}")
args.pop(0)
processed.append(converted)
elif isinstance(pattern, list):
pat_single = pattern[0]
collected = []
match = True
while match:
try:
x = process_args([args[0]], [pat_single])[0]
except CmdError: # If the pattern isn't matched anymore
match = False
else:
collected.append(x)
args.pop(0)
processed.append(collected)
return processed
This function looks really bad. Is there a better way of implementing a pattern matching like this?