classonlymethod decorator/descriptor defined below is like the built-in classmethod, but you're not allowed to call the method on instances, only on the class itself.
from typing import Concatenate, Self
from collections.abc import Callable
from functools import partial
class classonlymethod[C, **P, R]:
def __init__(self, func: Callable[Concatenate[type[C], P], R], /):
self._func = func
def __get__(self, instance: None, owner: type[C], /) -> Callable[P, R]:
if instance is not None:
raise AttributeError('class-only method')
return partial(self._func, owner)
class Foo:
@classonlymethod
def bar(cls: type[Self]) -> None:
raise NotImplementedError()
However, pyright complains on the definition of Foo.bar:
Type of parameter "cls" must be a supertype of its class "Foo" (reportGeneralTypeIssues)
Which is odd, since it matches annotations of classmethod in typeshed.
I don't understand this error. Surely a Foo (denoted by Self) is a (non-strict) supertype of itself? I tried other ways to refer to Foo in the cls annotation, but they didn't work either.
What's going on here and how do I fix it? Is it a pyright bug?
Since type checkers special-case classmethod, I settled on reframing the class-only constraint as an additional decorator on top of another descriptor of appropriate type:
from typing import Protocol
from typing import overload
from collections.abc import Callable
class _Descriptor[C, **P, R](Protocol):
@overload
def __get__(self, instance: None | C, owner: type[C], /) -> Callable[P, R]:
...
@overload
def __get__(self, instance: C, /) -> Callable[P, R]:
...
class classonly[C, **P, R]:
def __init__(self, descriptor: _Descriptor[C, P, R], /):
# can check that descriptor.__get__ exists here at runtime if necessary
self._descriptor = descriptor
def __get__(self, instance: None, owner: type[C], /) -> Callable[P, R]:
if instance is not None:
raise AttributeError('class-only method')
return self._descriptor.__get__(None, owner)
class Foo:
@classonly
@classmethod
def bar(cls) -> int: # fine
return 0
a = Foo.bar() # fine: int
b = Foo().bar() # error: Cannot access attribute "bar" for class "Foo"
See pyright. As a bonus, it also words with staticmethod.
@InSync and @STerliakov, thank you for your insights.