lagunatenofelix
lagunatenofelix

Reputation: 353

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.

Upvotes: 9

Views: 5131

Answers (3)

Alex Waygood
Alex Waygood

Reputation: 7569

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))

Upvotes: 5

Nuno Andr&#233;
Nuno Andr&#233;

Reputation: 5377

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

Upvotes: 3

Aashish A
Aashish A

Reputation: 111

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

Upvotes: 6

Related Questions