mypy icon indicating copy to clipboard operation
mypy copied to clipboard

Descriptor protocol is executed for properties returning descriptors

Open denballakh opened this issue 2 years ago • 1 comments

Consider this example:

from __future__ import annotations

class X: ...

class Y:
    @property
    def some_property(self) -> Descriptor: # return Descriptor instance
        return Descriptor()

class Descriptor:
    def __get__(self, obj: X, cls: type[X]) -> int: # can work only with X instances
        print(f'Descriptor.__get__({self}, {obj}, {cls}) called')
        return 0

y = Y()
print(y.some_property)
# arg-type: Argument 1 to "__get__" of "Descriptor" has incompatible type "Y"; expected "X"
# arg-type: Argument 2 to "__get__" of "Descriptor" has incompatible type "Type[Y]"; expected "Type[X]"

# stdout: <__main__.Descriptor object at 0x0000020BF8A5BE20>
# note that Descriptor.__get__ isnt called

I am defining descriptor that can work only with X instances, and i am returning this descriptor from property of Y instances. At runtime Descriptor.__get__ isnt called, but mypy thinks that Y.some_property is Descriptor (not a property returning Descriptor instance), tries to execute descriptor protocol and fails.

At runtime i am getting no errors, descriptor isnt called and this code is safe.

To reproduce:

  • just run mypy with this code

Expected Behavior

  • no errors

Actual Behavior

  • error

Your Environment

  • Mypy version used: mypy 0.971 (compiled: yes)
  • Mypy command-line flags: none
  • Mypy configuration options from mypy.ini (and other config files): none
  • Python version used: 3.10.6
  • Operating system and version: Win10

denballakh avatar Sep 04 '22 10:09 denballakh

from __future__ import annotations

class X:
    ...

class Y:
    # some_descriptor: Descriptor
    def __init__(self) -> None:
        self.some_descriptor = Descriptor()

class Descriptor:
    def __get__(self, obj: X, cls: type[X]) -> int:
        print(f'Descriptor.__get__({self}, {obj}, {cls}) called')
        return 0

y = Y()
print(y.some_descriptor)

This code is fine. But if i uncomment # some_descriptor: Descriptor line i am getting errors:

at `self.some_descriptor = Descriptor()` line:
Argument 1 to "__get__" of "Descriptor" has incompatible type "Y"; expected "X"                                                                       
Argument 2 to "__get__" of "Descriptor" has incompatible type "Type[Y]"; expected "Type[X]"                                                           
Incompatible types in assignment (expression has type "Descriptor", variable has type "int")                                                          

at `print(y.some_descriptor)` line:
Argument 1 to "__get__" of "Descriptor" has incompatible type "Y"; expected "X"                                                                       
Argument 2 to "__get__" of "Descriptor" has incompatible type "Type[Y]"; expected "Type[X]"  

I think this is related

denballakh avatar Sep 04 '22 10:09 denballakh

A similar example with the recent mypy version: https://mypy-play.net/?mypy=latest&python=3.11&gist=069b99fd10df82bef0769c52209ce00e

from typing import assert_type

class Desc:
    def __get__(self, obj, owner=None) -> int:
        return 1

class C:
    a = Desc()
    @property
    def b(self) -> Desc:
        return Desc()

obj = C()
assert_type(obj.a, int)
assert_type(obj.b, Desc)

generates

main.py:15: error: Expression is of type "int", not "Desc"  [assert-type]

but works fine at runtime.

eltoder avatar Aug 23 '23 02:08 eltoder