Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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
like image 481
vasia Avatar asked Oct 28 '25 13:10

vasia


1 Answers

Problem with self.handlers[command_type] = function:

  • register_handler decorator wrapper is called when the function is executed and registers the handler on the instance, so each handler needs to be called for each instance.

Problem with MessageProcessor.cls_handlers[command_type] = function:

  • register_handler decorator is called while the class is being defined, so MessageProcessor is not defined yet.

Quick fix

Reference cls_handlers in register_handler function definition:

# def register_handler(command_type: CommandType) -> typing.Callable:                               # -
def register_handler(command_type: CommandType, handlers: dict = cls_handlers) -> typing.Callable:  # +
    def decorator(function: typing.Callable):
        # MessageProcessor.cls_handlers[command_type] = function  # -
        handlers[command_type] = function                         # +
        return function
    return decorator

Proper way

Create register_handler function using a factory outside the class:

def make_register_handler(handlers: dict):
    def register_handler(command_type: CommandType) -> typing.Callable:
        def decorator(function: typing.Callable):
            handlers[command_type] = function
            return function
        return decorator
    return register_handler

Usage:

class MessageProcessor:
    cls_handlers = {}
    register_handler = make_register_handler(cls_handlers)

The way used in Flask

In Flask, @app.route() is called outside the class definition of app.

Similarly, the handlers may be defined outside of the processor class:

class MessageProcessor:
    cls_handlers = {}

    @classmethod
    def register_handler(cls, command_type: CommandType) -> typing.Callable:
        def decorator(function: typing.Callable):
            cls.cls_handlers[command_type] = function
            return function
        return decorator

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

        self.cls_handlers[command_type](args)


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


@MessageProcessor.register_handler(CommandType.PASS)
def handle_pass(args):
    print("Handler for PASS command. args: ", args)
like image 169
aaron Avatar answered Oct 31 '25 10:10

aaron



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!