Skip to content

Improve abc module and builtin function decorators (with ParamSpec) #5682

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
17 changes: 9 additions & 8 deletions stdlib/abc.pyi
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
from typing import Any, Callable, Type, TypeVar
from typing import Any, Callable, Generic, Type, TypeVar

_T = TypeVar("_T")
_FuncT = TypeVar("_FuncT", bound=Callable[..., Any])

# These definitions have special processing in mypy
class ABCMeta(type):
def register(cls: ABCMeta, subclass: Type[_T]) -> Type[_T]: ...

def abstractmethod(funcobj: _FuncT) -> _FuncT: ...

class abstractclassmethod(classmethod[_FuncT], Generic[_FuncT]):
def __init__(self, callable: _FuncT) -> None: ...

class abstractstaticmethod(staticmethod[_FuncT], Generic[_FuncT]):
def __init__(self, callable: _FuncT) -> None: ...

class abstractproperty(property): ...

# These two are deprecated and not supported by mypy
def abstractstaticmethod(callable: _FuncT) -> _FuncT: ...
def abstractclassmethod(callable: _FuncT) -> _FuncT: ...
class ABCMeta(type):
def register(cls: ABCMeta, subclass: Type[_T]) -> Type[_T]: ...

class ABC(metaclass=ABCMeta): ...

Expand Down
19 changes: 10 additions & 9 deletions stdlib/builtins.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ from _typeshed import (
SupportsWrite,
)
from ast import AST, mod
from collections.abc import Callable
from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper
from types import CodeType, TracebackType
from typing import (
Expand All @@ -26,7 +27,6 @@ from typing import (
AsyncIterator,
BinaryIO,
ByteString,
Callable,
Dict,
FrozenSet,
Generic,
Expand Down Expand Up @@ -80,6 +80,7 @@ _T4 = TypeVar("_T4")
_T5 = TypeVar("_T5")
_TT = TypeVar("_TT", bound="type")
_TBE = TypeVar("_TBE", bound="BaseException")
_FuncT = TypeVar("_FuncT", bound=Callable[..., Any])

class object:
__doc__: Optional[str]
Expand Down Expand Up @@ -109,19 +110,19 @@ class object:
def __dir__(self) -> Iterable[str]: ...
def __init_subclass__(cls) -> None: ...

class staticmethod(object): # Special, only valid as a decorator.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: These comments should be deleted. They seem misleading and mypy-specific to me.

__func__: Callable[..., Any]
class staticmethod(Generic[_FuncT]): # Special, only valid as a decorator.
__func__: _FuncT
__isabstractmethod__: bool
def __init__(self, f: Callable[..., Any]) -> None: ...
def __init__(self, f: _FuncT) -> None: ...
def __new__(cls: Type[_T], *args: Any, **kwargs: Any) -> _T: ...
def __get__(self, obj: _T, type: Optional[Type[_T]] = ...) -> Callable[..., Any]: ...
def __get__(self, obj: _T, type: Type[_T] | None = ...) -> _FuncT: ...

class classmethod(object): # Special, only valid as a decorator.
__func__: Callable[..., Any]
class classmethod(Generic[_FuncT]): # Special, only valid as a decorator.
__func__: _FuncT
__isabstractmethod__: bool
def __init__(self, f: Callable[..., Any]) -> None: ...
def __init__(self, f: _FuncT) -> None: ...
def __new__(cls: Type[_T], *args: Any, **kwargs: Any) -> _T: ...
def __get__(self, obj: _T, type: Optional[Type[_T]] = ...) -> Callable[..., Any]: ...
def __get__(self, obj: _T, type: Type[_T] | None = ...) -> _FuncT: ...
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem right, because the callable returned from __get__ doesn't take the class as a first argument.

>>> class Foo:
...     @classmethod
...     def bar(cls):
...             print("Bar runs:", cls)
... 
>>> Foo.bar()
Bar runs: <class '__main__.Foo'>
>>> Foo.__dict__['bar'].__func__(Foo)
Bar runs: <class '__main__.Foo'>

Here Foo.__dict__['bar'] is the classmethod object, and for Foo.bar, its __get__ method gets called. The result is a bound method, which basically means that it includes the class similarly to partial:

>>> Foo.bar == Foo.__dict__['bar'].__get__(Foo(), None)
True
>>> Foo.bar
<bound method Foo.bar of <class '__main__.Foo'>>

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another problem: I think it calls __get__(None, Foo) when looking up Foo.bar, so None should be allowed for the first argument to __get__.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I tried to express this using ParamSpec, but I'm not sure this is correct. Also, do you know in what circumstances the owner argument can be None or not set?

Copy link
Collaborator

@Akuli Akuli Jun 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears to be a technicality that doesn't really occur in practice: "Python’s own __getattribute__() implementation always passes in both arguments whether they are required or not." Docs


class type(object):
__base__: type
Expand Down