Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 215
Yang Hanlin Avatar asked Oct 07 '20 17:10

Yang Hanlin


People also ask

What are the limitations of singledispatch in Python?

The singledispatch function is useful, but it is not without limitations. Its main limitation is also apparent in its name: it can only dispatch based on a single function parameter, and then only the first. If you require multiple dispatching you will need a third-party library as Python does not come with that built in.

How do I convert a regular function to a single dispatch?

They did this by adding a neat little decorator to the functools module called singledispatch. This decorator will transform your regular function into a single dispatch generic function. Note however that singledispatch only happens based on the first argument's type. Let's take a look at an example to see how this works!

Do I need to register a single dispatch for a class?

You still have to register your Vector implementation for the single dispatch after the class has been created, because only then can you register a dispatch for the class:

What is the purpose of the method singledispatch method in Python?

The functools.singledispatchmethod () option that Python 3.8 adds uses a class as the decorator which implements the descriptor protocol, just like methods do. This lets it then handle dispatch before binding (so before self would be prepended to the argument list) and then bind the registered function that the singledispatch dispatcher returns.


2 Answers

https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable

from collections import abc

@notifyme.register
def notifyme_default(func: abc.Callable) -> Callable:
    return notifyme_customized('The function is invoked.')(func)
like image 142
lot Avatar answered Oct 23 '22 09:10

lot


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)
like image 40
Maximilian Burszley Avatar answered Oct 23 '22 07:10

Maximilian Burszley