Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

singledispatchmethod and class method decorators in python 3.8

I am trying to use one of the new capabilities of python 3.8 (currently using 3.8.3). Following the documentation I tried the example provided in the docs:

from functools import singledispatchmethod
class Negator:
    @singledispatchmethod
    @classmethod
    def neg(cls, arg):
        raise NotImplementedError("Cannot negate a")

    @neg.register
    @classmethod
    def _(cls, arg: int):
        return -arg

    @neg.register
    @classmethod
    def _(cls, arg: bool):
        return not arg

Negator.neg(1)

This, however, yields the following error:

...
TypeError: Invalid first argument to `register()`: <classmethod object at 0x7fb9d31b2460>. Use either `@register(some_class)` or plain `@register` on an annotated function.

How can I create a generic class method? Is there something I am missing in my example?

Update:

I have read Aashish A's answer and it seems lik an on-going issue. I have managed to solve my problem the following way.

from functools import singledispatchmethod
class Negator:
    @singledispatchmethod
    @staticmethod
    def neg(arg):
        raise NotImplementedError("Cannot negate a")

    @neg.register
    def _(arg: int):
        return -arg

    @neg.register
    def _(arg: bool):
        return not arg

print(Negator.neg(False))
print(Negator.neg(-1))

This seems to work in version 3.8.1 and 3.8.3, however it seems it shouldn't as I am not using the staticmethod decorator on neither the undescore functions. This DOES work with classmethods, even tho the issue seems to indicate the opposite.

Keep in mind if you are using an IDE that the linter won't be happy with this approach, throwing a lot of errors.

like image 487
lagunatenofelix Avatar asked Jul 02 '20 12:07

lagunatenofelix


3 Answers

This seems to be a bug in the functools library documented in this issue.

like image 156
Aashish A Avatar answered Nov 14 '22 11:11

Aashish A


A workaround, while the bugfix is not merged, is to patch singledispatchmethod.register():

from functools import singledispatchmethod

def _register(self, cls, method=None):
    if hasattr(cls, '__func__'):
        setattr(cls, '__annotations__', cls.__func__.__annotations__)
    return self.dispatcher.register(cls, func=method)

singledispatchmethod.register = _register
like image 40
Nuno André Avatar answered Nov 14 '22 13:11

Nuno André


This bug is no longer present in Python >= 3.9.8. In Python 3.9.8, the code for singledispatchmethod has been tweaked to ensure it works with type annotations and classmethods/staticmethods. In Python 3.10+, however, the bug was solved as a byproduct of changing the way classmethods and staticmethods behave with respect to the __annotations__ attribute of the function they're wrapping.

In Python 3.9:

>>> x = lambda y: y
>>> x.__annotations__ = {'y': int}
>>> c = classmethod(x)
>>> c.__annotations__
Traceback (most recent call last):
  File "<pyshell#37>", line 1, in <module>
    c.__annotations__
AttributeError: 'classmethod' object has no attribute '__annotations__'

In Python 3.10+:

>>> x = lambda y: y
>>> x.__annotations__ = {'y': int}
>>> c = classmethod(x)
>>> c.__annotations__
{'y': <class 'int'>}

This change appears to have solved the issue with singledispatchmethod, meaning that in Python 3.9.8 and Python 3.10+, the following code works fine:

from functools import singledispatchmethod

class Negator:
    @singledispatchmethod
    @classmethod
    def neg(cls, arg):
        raise NotImplementedError(f"Cannot negate object of type '{type(arg).__name__}'")

    @neg.register
    @classmethod
    def _(cls, arg: int) -> int:
        return -arg

    @neg.register
    @classmethod
    def _(cls, arg: bool) -> bool:
        return not arg

print(Negator.neg(1))
print(Negator.neg(False))
like image 27
Alex Waygood Avatar answered Nov 14 '22 12:11

Alex Waygood