I couldn't get the example given for the interaction between @singledispatchmethod and @classmethod to work https://docs.python.org/3/library/functools.html?highlight=singledispatch#functools.singledispatchmethod
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
if __name__ == "__main__":
print(Negator.neg(0))
print(Negator.neg(False))
Leads to
$ python -m bad_classmethod_as_documented
Traceback (most recent call last):
File "/usr/lib/python3.8/runpy.py", line 193, in _run_module_as_main
return _run_code(code, main_globals, None,
File "/usr/lib/python3.8/runpy.py", line 86, in _run_code
exec(code, run_globals)
File "/home/viktor/scratch/bad_classmethod_as_documented.py", line 4, in <module>
class Negator:
File "/home/viktor/scratch/bad_classmethod_as_documented.py", line 12, in Negator
def _(cls, arg: int):
File "/usr/lib/python3.8/functools.py", line 906, in register
return self.dispatcher.register(cls, func=method)
File "/usr/lib/python3.8/functools.py", line 848, in register
raise TypeError(
TypeError: Invalid first argument to `register()`: <classmethod object at 0x7f37d1469070>. Use either `@register(some_class)` or plain `@register` on an annotated function.
Curiously, @staticmethod does work, but not as documented (don't decorate the actual implementations):
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
if __name__ == "__main__":
print(Negator.neg(0))
print(Negator.neg(False))
Leads to
$ python -m good_staticmethod
0
True
Removing @classmethod from the implementation methods doesn't work, though
Traceback (most recent call last):
File "/usr/lib/python3.8/runpy.py", line 193, in _run_module_as_main
return _run_code(code, main_globals, None,
File "/usr/lib/python3.8/runpy.py", line 86, in _run_code
exec(code, run_globals)
File "/home/viktor/scratch/bad_classmethod_alternative.py", line 20, in <module>
print(Negator.neg(0))
File "/usr/lib/python3.8/functools.py", line 911, in _method
return method.__get__(obj, cls)(*args, **kwargs)
TypeError: _() missing 1 required positional argument: 'arg'
|
I guess the method checks for annotation on cls [0] which will be classmethod/staticmethod object in the report and won't have annotations. The annotations should be looked up in the function the classmethod/staticmethod decorator wraps around as in cls.__func__ . Something like below so that the right annotations are picked up. In addition to this the registry should store the type annotation as key to cls or cls.__func__ depending on normal method or classmethod/staticmethod.
diff --git Lib/functools.py Lib/functools.py
index 050bec8605..a66711208d 100644
--- Lib/functools.py
+++ Lib/functools.py
@@ -1073,24 +1073,33 @@ def singledispatch(func):
if func is None:
if isinstance(cls, type):
return lambda f: register(cls, f)
- ann = getattr(cls, '__annotations__', {})
+ if isinstance(cls, (classmethod, staticmethod)):
+ ann = getattr(cls.__func__, '__annotations__', {})
+ func = cls.__func__
+ else:
+ ann = getattr(cls, '__annotations__', {})
+ func = cls
[0] https://github.com/python/cpython/blob/ab6423fe2de0ed5f8a0dc86a9c7070229326b0f0/Lib/functools.py#L1076
|
I tried to apply this change but it didn't work, failing with this error
$ ~/.pyenv/versions/3.8.1/bin/python -m bad_classmethod_as_documented
Traceback (most recent call last):
File "/home/viktor/.pyenv/versions/3.8.1/lib/python3.8/runpy.py", line 193, in _run_module_as_main
return _run_code(code, main_globals, None,
File "/home/viktor/.pyenv/versions/3.8.1/lib/python3.8/runpy.py", line 86, in _run_code
exec(code, run_globals)
File "/home/viktor/scratch/bad_classmethod_as_documented.py", line 4, in <module>
class Negator:
File "/home/viktor/scratch/bad_classmethod_as_documented.py", line 12, in Negator
def _(cls, arg: int):
File "/home/viktor/.pyenv/versions/3.8.1/lib/python3.8/functools.py", line 1006, in register
return self.dispatcher.register(cls, func=method)
File "/home/viktor/.pyenv/versions/3.8.1/lib/python3.8/functools.py", line 959, in register
argname, cls = next(iter(get_type_hints(func).items()))
File "/home/viktor/.pyenv/versions/3.8.1/lib/python3.8/typing.py", line 1252, in get_type_hints
raise TypeError('{!r} is not a module, class, method, '
TypeError: <classmethod object at 0x7f84e1e69c40> is not a module, class, method, or function.
After digging around a bit, this diff seems to work (not sure if there's a better way to do it) (also this one doesn't seem to care whether @staticmethod is applied to the implementation methods):
$ diff -u functools-orig.py functools.py
--- functools-orig.py 2020-02-21 16:14:56.141934001 -0500
+++ functools.py 2020-02-21 16:17:19.959905849 -0500
@@ -843,14 +843,18 @@
if func is None:
if isinstance(cls, type):
return lambda f: register(cls, f)
- ann = getattr(cls, '__annotations__', {})
+ if isinstance(cls, (classmethod, staticmethod)):
+ ann = getattr(cls.__func__, '__annotations__', {})
+ func = cls.__func__
+ else:
+ ann = getattr(cls, '__annotations__', {})
+ func = cls
if not ann:
raise TypeError(
f"Invalid first argument to `register()`: {cls!r}. "
f"Use either `@register(some_class)` or plain `@register` "
f"on an annotated function."
)
- func = cls
# only import typing if annotation parsing is necessary
from typing import get_type_hints
@@ -908,6 +912,8 @@
def __get__(self, obj, cls=None):
def _method(*args, **kwargs):
method = self.dispatcher.dispatch(args[0].__class__)
+ if isinstance(self.func, classmethod):
+ return method.__get__(obj, cls)(cls, *args, **kwargs)
return method.__get__(obj, cls)(*args, **kwargs)
_method.__isabstractmethod__ = self.__isabstractmethod__
|
Based on what I've read, workaround:
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
|