1
\$\begingroup\$

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?

\$\endgroup\$
2
  • \$\begingroup\$ Hi, I don't have much time to skim through the code right now, but I think you may find this SO question of some interest. There is also this answer that have some pre-thoughts to the one I posted on SO. \$\endgroup\$ Commented Jun 28, 2019 at 12:14
  • \$\begingroup\$ @MathiasEttinger Hi. I'm not trying to validate the type of the function arguments. I am parsing a user input according to a list of patterns. \$\endgroup\$ Commented Jun 28, 2019 at 12:34

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.