python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.43k stars 2.82k forks source link

Wrong type inference for class with metaclass that acts as descriptor #10964

Open Jackenmen opened 3 years ago

Jackenmen commented 3 years ago

Bug Report Descriptor protocol does not work on metaclasses - defining __get__ method in a metaclass does not make classes using that metaclass work as descriptors.

To Reproduce

  1. Put this code in a py file:
    
    from typing import Any, TYPE_CHECKING, overload

class _IntDescriptorMeta(type): def get(self, instance: Any, owner: Any) -> int: return 123

class IntDescriptorClass(metaclass=_IntDescriptorMeta): ...

class IntDescriptor: def get(self, instance: Any, owner: Any) -> int: return 123

class X: number_cls = IntDescriptorClass number = IntDescriptor()

print(X.number_cls) print(X().number_cls) print(X.number) print(X().number)

if TYPE_CHECKING: reveal_type(X.number_cls) reveal_type(X().number_cls) reveal_type(X.number) reveal_type(X().number)

2. Run it, you should see:

123 123 123 123

3. Type check it with mypy and see the incorrect output.

**Expected Behavior**
I expected mypy to infer type of `X.number_cls`/`X().number_cls` correctly:

main.py:24: note: Revealed type is "builtins.int" main.py:25: note: Revealed type is "builtins.int" main.py:26: note: Revealed type is "builtins.int" main.py:27: note: Revealed type is "builtins.int"

Note:
To simplify this example, `__get__` returns an `int` no matter if `instance` is `None` or not but I assume a fix would also make it work properly with overloads as it does for instances of classes.

**Actual Behavior**

Mypy does not infer type of `X.number_cls`/`X().number_cls` correctly:

main.py:24: note: Revealed type is "def () -> main.IntDescriptorClass" main.py:25: note: Revealed type is "def () -> main.IntDescriptorClass" main.py:26: note: Revealed type is "builtins.int" main.py:27: note: Revealed type is "builtins.int"



**Your Environment**

- Mypy version used: 0.910 as well as the latest (master branch)
- mypy-play.net URL: https://mypy-play.net/?mypy=latest&python=3.8&gist=de23d753f19cf0149ed9cd6919e84791

**Additional notes**
This works properly on pyright 1.1.161+, see the issue: https://github.com/microsoft/pyright/issues/2164
Jackenmen commented 3 years ago

Another worthy test case checking for proper inference of types when self is annotated as Type[T]:

from typing import Any, Optional, Union, Type, TypeVar, TYPE_CHECKING, overload

T = TypeVar("T")

class _IntDescriptorMeta(type):
    @overload
    def __get__(self: Type[T], instance: None, owner: Any) -> Type[T]: ...

    @overload
    def __get__(self: Type[T], instance: object, owner: Any) -> T: ...

    def __get__(self: Type[T], instance: Optional[object], owner: Any) -> Union[Type[T], T]:
        if instance is None:
            return self
        return self()

class IntDescriptorClass(metaclass=_IntDescriptorMeta):
    ...

class X:
    number_cls = IntDescriptorClass

print(X.number_cls)
print(X().number_cls)

if TYPE_CHECKING:
    reveal_type(X.number_cls)
    reveal_type(X().number_cls)

Taken from a pyright bug that was reported after this functionality was initially implemented: https://github.com/microsoft/pyright/issues/2177

finite-state-machine commented 9 months ago

This provides a very useful workaround to the lack of support for arbitrary class decorators; a metaclass and __get__() could accomplish almost anything a decorator could.