Search code examples
pythonpython-3.8callabledynamic-dispatch

How to register typing.Callable with Python @singledispatch?


Background

Suppose I am to implement a simple decorator @notifyme that prints a message when the decorated function is invoked. I would like the decorator to accept one argument to print a customized message; the argument (along with the parentheses surrounding the argument) may be omitted, in which case the default message is printed:

@notifyme('Foo is invoked!')
def foo():
    pass

@notifyme  # instead of @notifyme()
def bar():
    pass

To allow the parentheses to be omitted, I have to provide two implementations of @notifyme:

  1. The first implementation allows the user to customize the message, so it accepts a string as argument and returns a decorator:

    def notifyme_customized(message: str) -> Callable[[Callable], Callable]:
        def decorator(func: Callable) -> Callable:
            def decorated_func(*args, **kwargs):
                print(str)
                return func(*args, **kwargs)
            return decorated_func
        return decorator
    
  2. The second implementation is a decorator itself and uses the first implementation to print a default message:

    def notifyme_default(func: Callable) -> Callable:
        return notifyme_customized('The function is invoked.')(func)
    

To make the two implementations above use the same name notifyme, I used functools.singledispatch to dynamically dispatch the call to notifyme to one of the two implementations:

# This is a complete minimal reproducible example

from functools import singledispatch
from typing import Callable

@singledispatch
def notifyme(arg):
    return NotImplemented

@notifyme.register
def notifyme_customized(message: str) -> Callable[[Callable], Callable]:
    def decorator(func: Callable) -> Callable:
        def decorated_func(*args, **kwargs):
            print(str)
            return func(*args, **kwargs)
        return decorated_func
    return decorator

@notifyme.register
def notifyme_default(func: Callable) -> Callable:
    return notifyme_customized('The function is invoked.')(func)

Problem

However, as the code is interpreted by the Python interpreter, it complains that typing.Callable is an invalid type:

Traceback (most recent call last):
  File "demo.py", line 20, in <module>
    def notifyme_default(func: Callable) -> Callable:
  File "C:\Program Files\Python38\lib\functools.py", line 860, in register
    raise TypeError(
TypeError: Invalid annotation for 'func'. typing.Callable is not a class.

I have found this issue on Python bug tracker, according to which it seems to be expected behavior since Python 3.7. Does a solution or workaround exist in Python 3.8 I use currently (or Python 3.9 that has been released recently)?

Thanks in advance.


Solution

  • I was unable to use typing.Callable with functools.singledispatch, but I did find a workaround by using a function class reference instead:

    from functools import singledispatch
    from typing import Callable
    
    function = type(lambda: ())
    
    @singledispatch
    def notifyme(arg):
        return NotImplemented
    
    @notifyme.register
    def notifyme_customized(message: str) -> Callable[[Callable], Callable]:
        def decorator(func: Callable) -> Callable:
            def decorated_func(*args, **kwargs):
                print(str)
                return func(*args, **kwargs)
            return decorated_func
        return decorator
    
    @notifyme.register
    def notifyme_default(func: function) -> Callable:
        return notifyme_customized('The function is invoked.')(func)