Thursday, 10 March 2022

How to properly register a handler within a class using a decorator?

I am implementing a message processor class. I'd like to use decorators to determine which class method should be used for processing a particular command type. Flask does something very similar to this with their @app.route('/urlpath') decorator.

I can't figure out a way to actually register the function as a handler, without first executing the function explicitly, which doesn't seem like it should be necessary.

My code:

from enum import Enum, auto, unique
import typing

@unique
class CommandType(Enum):
    DONE = auto()
    PASS = auto()

class MessageProcessor:
    def __init__(self):
        self.handlers = {}

    def register_handler(command_type: CommandType, *args) -> typing.Callable:
        def decorator(function: typing.Callable):
            def wrapper(self, args):
                self.handlers[command_type] = function
                return function
            return wrapper
        return decorator

    @register_handler(CommandType.DONE)
    def handle_done(self, args):
        print("Handler for DONE command. args: ", args)

    @register_handler(CommandType.PASS)
    def handle_pass(self, args):
        print("Handler for PASS command. args: ", args)

    def process(self, message: str):
        tokens = message.split()
        command_type = CommandType[tokens[0]]
        args = tokens[1:]

        self.handlers[command_type](self, args)

Now if I try executing the following code, I get an error:

m = MessageProcessor()
m.process('DONE arg1 arg2')
Traceback (most recent call last):
  File "/Users/......./main.py", line 87, in <module>
    m.process('DONE arg1 arg2')
  File "/Users/......./main.py", line 81, in process
    self.handlers[command_type](self, args)
KeyError: <CommandType.DONE: 1>

However, if I explicitly invoke the methods (with any arguments) first, then I don't get the error:

m = MessageProcessor()
m.handle_done(None)
m.handle_pass(None)
m.process('DONE arg1 arg2')
m.process('PASS arg1 arg2')
Handler for DONE command. args:  ['arg1', 'arg2']
Handler for PASS command. args:  ['arg1', 'arg2']

Is there a way to properly register these handlers without first having to perform an explicit dummy invocation (i.e. m.handle_done(None))?

I don't want to do something like manually invoke each method in the constructor for the MessageProcessor, I am looking for an approach in which the existence of the decorator is sufficient.


Would I be able to get around this by making handlers static and not bound to an object instance? I was trying that earlier but got tangled up while trying to make it work:

class MessageProcessor:
    cls_handlers = {}

    def register_handler(command_type: CommandType, *args) -> typing.Callable:
        def decorator(function: typing.Callable):
            MessageProcessor.cls_handlers[command_type] = function
            return function
        return decorator


from How to properly register a handler within a class using a decorator?

No comments:

Post a Comment