typing icon indicating copy to clipboard operation
typing copied to clipboard

Add typing.Property

Open luckydonald opened this issue 6 years ago • 5 comments

Currently there is no type for the values @property returns.

a) the returned value of property has no typing equivalent

def create_property() -> ???:
    some_really_bad_code = True
    def setter(value):
         some_really_bad_code = value
    def getter():
        return some_really_bad_code
    def deller():
        some_really_bad_code = None
    return property(getter, setter, deller)

b) There is no consistency in how properties in classes work in comparison to normal variables or functions:

class Example:
    foo: str
    
    def bar(self, value: str) -> str:
        return value
    # end def

    @property
    def batz(self) -> str:
        return 'test2'
    # end def

    @batz.setter
    def batz(self, value: str):
        pass
    # end def
# end class


# variable

>>> typing.get_type_hints(Example)
{'foo': <class 'str'>}

>>> Example.__annotations__
{'foo': <class 'str'>}


# function

>>> typing.get_type_hints(Example.bar)
{'value': <class 'str'>, 'return': <class 'str'>}

>>> Example.bar.__annotations__
{'value': <class 'str'>, 'return': <class 'str'>}


# property
# not in the class's hints
>>> 'batz' in  typing.get_type_hints(Example)
False

>>> typing.get_type_hints(Example.batz)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "/usr/local/Cellar/python/3.6.4_3/Frameworks/Python.framework/Versions/3.6/lib/python3.6/typing.py", line 1527, in get_type_hints
    'or function.'.format(obj))
TypeError: <property object at 0x10e81b868> is not a module, class, method, or function.

>>> Example.batz.__annotations__
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: 'property' object has no attribute '__annotations__'

luckydonald avatar Nov 20 '18 13:11 luckydonald

Regarding a) this is not really about typing, technically property is a class, so you can annotate your decorator as just -> property. Some type checkers may be not happy about this, but then it should be discussed per type checker. About b) we can add some special casing for property but this is a more general problem, unless decorator copies the __annotations__ attribute, we can't do much.

ilevkivskyi avatar Nov 20 '18 17:11 ilevkivskyi

FYI: if you wind up here, you can do this for now:

>>> Example.batz.fget.__annotations__
{'return': <class 'str'>}

earonesty avatar May 08 '20 08:05 earonesty

@earonesty Thank you for putting me on the right path... I was looking for this trick for so long, I think I should share it here. Before reaching fget or fset attributes from a property, we need to access descriptor statically either from inspect.getattr_static() or type.__getattribute__(), otherwise Python would raise AttributeError: 'FooBar' object has no attribute "fget" since it is trying to access 'fget' on property return value instead of the object itself.

from typing import Optional, Union
from inspect import getattr_static
import unittest


class FooBar:
    @property
    def foobar(self) -> Optional[str]:
        pass

    @foobar.setter
    def foobar(self, foobar_: str) -> None:
        pass


class AnnotationsTest(unittest.TestCase):

    def setUp(self):
        self.foobar = FooBar()

    def test_access_getter_annotations_with_inspect(self):
        """==> object_.fget.__annotations__ should return expected annotations"""
        property_object = getattr_static(self.foobar, 'foobar')
        self.assertEqual(property_object.fget.__annotations__, {'return': Union[str, None]})

    def test_access_setter_annotations_with_inspect(self):
        """==> object_.fset.__annotations__ should return expected annotations"""
        property_object = getattr_static(self.foobar, 'foobar')
        self.assertEqual(property_object.fset.__annotations__, {'foobar_': str, 'return': None})

    def test_access_getter_annotations_with_type_getattribute(self):
        """==> object_.fget.__annotations__ should return expected annotations"""
        property_object = type.__getattribute__(type(self.foobar), 'foobar')
        self.assertEqual(property_object.fget.__annotations__, {'return': Union[str, None]})

    def test_access_setter_annotations_with_type_getattribute(self):
        """==> object_.fset.__annotations__ should return expected annotations"""
        property_object = type.__getattribute__(type(self.foobar), 'foobar')
        self.assertEqual(property_object.fset.__annotations__, {'foobar_': str, 'return': None})


if __name__ == '__main__':
    unittest.main(verbosity=2)

TangoMan75 avatar May 13 '20 01:05 TangoMan75

Could we have a generic Property class added to typing? PEP 614 would allow something like this:

from typing import Property

class C:
    def __init__(self, prop: int):
        self._prop = prop

    @Property[int]
    def prop(self):
        return self._prop

Here's the gist of the class I'm proposing (details will likely need to be refined):

T_co = TypeVar('T_co', covariant=True)

class Property(property, Generic[T_co]):
    def fget(self) -> T_co:
        return cast(T_co, super().fget())

    def fset(self, value: T_co) -> None:
        super().fset(value)

    def fdel(self) -> None:
        super().fdel()

    def getter(self, fget: Callable[[Any], T_co]) -> 'Property[T_co]':
        return cast(Property[T_co], super().getter(fget))

    def setter(self, fset: Callable[[Any, T_co], None]) -> 'Property[T_co]':
        return cast(Property[T_co], super().setter(fset))

    def deleter(self, fdel: Callable[[Any], None]) -> 'Property[T_co]':
        return cast(Property[T_co], super().deleter(fdel))

jp-larose avatar Sep 03 '20 02:09 jp-larose

I mean for me it would make sense the most to actually annotate the return type for the function the property calls, and maybe the property could automatically adapt to that?

@property
def foo(...) -> int:

to

Union[property, int]

or that a new type idea

Property[int]

Like as long as you annotate the function under the @property that could automatically be used?

luckydonald avatar Sep 03 '20 14:09 luckydonald