Skip to content

Commit 9c739b3

Browse files
committed
Reveal property at class level too
1 parent 3dcf759 commit 9c739b3

File tree

6 files changed

+148
-22
lines changed

6 files changed

+148
-22
lines changed

mypy/checker.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@
170170
ANY_STRATEGY,
171171
MYPYC_NATIVE_INT_NAMES,
172172
OVERLOAD_NAMES,
173+
PROPERTY_DECORATOR_NAMES,
173174
AnyType,
174175
BoolTypeQuery,
175176
CallableType,
@@ -2147,6 +2148,20 @@ def check_method_override_for_base_with_name(
21472148
defn.func,
21482149
code=codes.OVERRIDE,
21492150
)
2151+
elif (
2152+
isinstance(defn, Decorator)
2153+
and isinstance(typ, Instance)
2154+
and typ.type.fullname == "builtins.property"
2155+
):
2156+
self.msg.fail(
2157+
"Property setter/deletter overrides are not supported.",
2158+
defn.func,
2159+
code=codes.MISC,
2160+
)
2161+
self.msg.note(
2162+
"Consider overriding getter explicitly with super() call.", defn.func
2163+
)
2164+
return False
21502165

21512166
if isinstance(original_type, AnyType) or isinstance(typ, AnyType):
21522167
pass
@@ -2215,6 +2230,20 @@ def check_method_override_for_base_with_name(
22152230
)
22162231
return False
22172232

2233+
def get_property_instance(self, method: Decorator) -> Instance | None:
2234+
property_deco_name = next(
2235+
(
2236+
name
2237+
for d in method.original_decorators
2238+
for name in PROPERTY_DECORATOR_NAMES
2239+
if refers_to_fullname(d, name)
2240+
),
2241+
None,
2242+
)
2243+
if property_deco_name is not None:
2244+
return self.named_type(property_deco_name)
2245+
return None
2246+
22182247
def bind_and_map_method(
22192248
self, sym: SymbolTableNode, typ: FunctionLike, sub_info: TypeInfo, super_info: TypeInfo
22202249
) -> FunctionLike:

mypy/checkexpr.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,11 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
409409
# Reference to a module object.
410410
result = self.module_type(node)
411411
elif isinstance(node, Decorator):
412-
result = self.analyze_var_ref(node.var, e)
412+
property_type = self.chk.get_property_instance(node)
413+
if property_type is not None:
414+
result = property_type
415+
else:
416+
result = self.analyze_var_ref(node.var, e)
413417
elif isinstance(node, TypeAlias):
414418
# Something that refers to a type alias appears in runtime context.
415419
# Note that we suppress bogus errors for alias redefinitions,

mypy/checkmember.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
is_final_node,
3939
)
4040
from mypy.plugin import AttributeContext
41-
from mypy.semanal import refers_to_fullname
4241
from mypy.typeops import (
4342
bind_self,
4443
class_callable,
@@ -51,7 +50,6 @@
5150
type_object_type_from_function,
5251
)
5352
from mypy.types import (
54-
PROPERTY_DECORATOR_NAMES,
5553
AnyType,
5654
CallableType,
5755
DeletedType,
@@ -1112,18 +1110,10 @@ def analyze_class_attribute_access(
11121110
# C[int].x -> int
11131111
t = erase_typevars(expand_type_by_instance(t, isuper), {tv.id for tv in def_vars})
11141112

1115-
if is_decorated:
1116-
property_deco_name = next(
1117-
(
1118-
name
1119-
for d in cast(Decorator, node.node).original_decorators
1120-
for name in PROPERTY_DECORATOR_NAMES
1121-
if refers_to_fullname(d, name)
1122-
),
1123-
None,
1124-
)
1125-
if property_deco_name is not None:
1126-
return mx.named_type(property_deco_name)
1113+
if is_decorated and cast(Decorator, node.node).func.is_property:
1114+
property_type = mx.chk.get_property_instance(cast(Decorator, node.node))
1115+
if property_type is not None:
1116+
return property_type
11271117

11281118
is_classmethod = (is_decorated and cast(Decorator, node.node).func.is_class) or (
11291119
isinstance(node.node, FuncBase) and node.node.is_class

mypy/semanal.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7153,6 +7153,8 @@ def already_defined(
71537153
self.fail(
71547154
f'{noun} "{unmangle(name)}" already defined{extra_msg}', ctx, code=codes.NO_REDEF
71557155
)
7156+
if isinstance(node, Decorator) and node.func.is_property:
7157+
self.note("Property setter and deleter must be adjacent to the getter.", ctx)
71567158

71577159
def name_already_defined(
71587160
self, name: str, ctx: Context, original_ctx: SymbolTableNode | SymbolNode | None = None

test-data/unit/check-classes.test

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1621,8 +1621,7 @@ class A:
16211621
self.x = 1
16221622
self.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
16231623
return ''
1624-
[builtins fixtures/property.pyi]
1625-
[out]
1624+
[builtins fixtures/property-full.pyi]
16261625

16271626
[case testDynamicallyTypedProperty]
16281627
import typing
@@ -1632,7 +1631,7 @@ class A:
16321631
a = A()
16331632
a.f.xx
16341633
a.f = '' # E: Property "f" defined in "A" is read-only
1635-
[builtins fixtures/property.pyi]
1634+
[builtins fixtures/property-full.pyi]
16361635

16371636
[case testPropertyWithSetter]
16381637
import typing
@@ -1649,7 +1648,7 @@ a.f.x # E: "int" has no attribute "x"
16491648
a.f = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
16501649
a.f = 1
16511650
reveal_type(a.f) # N: Revealed type is "builtins.int"
1652-
[builtins fixtures/property.pyi]
1651+
[builtins fixtures/property-full.pyi]
16531652

16541653
[case testPropertyWithDeleterButNoSetter]
16551654
import typing
@@ -1663,18 +1662,21 @@ class A:
16631662
a = A()
16641663
a.f = a.f # E: Property "f" defined in "A" is read-only
16651664
a.f.x # E: "int" has no attribute "x"
1666-
[builtins fixtures/property.pyi]
1665+
[builtins fixtures/property-full.pyi]
16671666

16681667
[case testPropertyAccessOnClass]
16691668
class Foo:
16701669
@property
16711670
def bar(self) -> bool:
16721671
return True
16731672

1673+
reveal_type(bar) # N: Revealed type is "builtins.property"
1674+
16741675
reveal_type(Foo.bar) # N: Revealed type is "builtins.property"
16751676
reveal_type(Foo.bar(Foo())) # E: "property" not callable \
16761677
# N: Revealed type is "Any"
1677-
[builtins fixtures/property.pyi]
1678+
1679+
[builtins fixtures/property-full.pyi]
16781680

16791681
[case testPropertyAccessOnClass2]
16801682
import functools
@@ -1689,11 +1691,67 @@ class Foo:
16891691
def bar(self) -> bool:
16901692
return True
16911693

1694+
reveal_type(foo) # N: Revealed type is "functools.cached_property[Any]"
1695+
reveal_type(bar) # N: Revealed type is "functools.cached_property[Any]"
1696+
16921697
reveal_type(Foo.foo) # N: Revealed type is "functools.cached_property[Any]"
16931698
reveal_type(Foo.bar) # N: Revealed type is "functools.cached_property[Any]"
16941699
Foo.foo(Foo()) # E: "cached_property[Any]" not callable
16951700
Foo.bar(Foo()) # E: "cached_property[Any]" not callable
1696-
[builtins fixtures/property.pyi]
1701+
[builtins fixtures/property-full.pyi]
1702+
1703+
[case testPropertySetterOverrideInCubclass]
1704+
# See https://github.com/python/mypy/issues/5936
1705+
# Ideally we should support this, but at least be explicit that it isn't supported
1706+
class Base:
1707+
def __init__(self, value: int) -> None:
1708+
self._value = value
1709+
1710+
@property
1711+
def value(self) -> int:
1712+
return self._value
1713+
1714+
reveal_type(value) # N: Revealed type is "builtins.property"
1715+
reveal_type(value.setter) # N: Revealed type is "def (def (Any, Any)) -> builtins.property"
1716+
1717+
class Sub(Base):
1718+
@Base.value.setter
1719+
def value(self, value: int) -> None: # E: Property setter/deletter overrides are not supported. \
1720+
# N: Consider overriding getter explicitly with super() call.
1721+
self._value = value
1722+
1723+
reveal_type(Base.value) # N: Revealed type is "builtins.property"
1724+
reveal_type(Base(0).value) # N: Revealed type is "builtins.int"
1725+
Base(0).value = 2 # E: Property "value" defined in "Base" is read-only
1726+
1727+
reveal_type(Sub.value) # N: Revealed type is "Any"
1728+
reveal_type(Sub(0).value) # N: Revealed type is "Any"
1729+
Sub(0).value = 2
1730+
[builtins fixtures/property-full.pyi]
1731+
1732+
[case testPropertySetterNonAdjacent]
1733+
# See https://github.com/python/mypy/issues/1465
1734+
# We want to support this later, but at least fail explicitly for now.
1735+
class A(object):
1736+
@property
1737+
def val(self) -> int:
1738+
return 0
1739+
1740+
reveal_type(val) # N: Revealed type is "builtins.property"
1741+
1742+
def _other(self) -> None: ...
1743+
1744+
@val.setter # E: Name "val" already defined on line 4 \
1745+
# N: Property setter and deleter must be adjacent to the getter.
1746+
def val(self, val: int) -> None:
1747+
pass
1748+
1749+
reveal_type(val) # N: Revealed type is "builtins.property"
1750+
1751+
reveal_type(A.val) # N: Revealed type is "builtins.property"
1752+
reveal_type(A().val) # N: Revealed type is "builtins.int"
1753+
A().val = 1 # E: Property "val" defined in "A" is read-only
1754+
[builtins fixtures/property-full.pyi]
16971755

16981756
-- Descriptors
16991757
-- -----------
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from typing import Any, Callable, Generic, TypeVar
2+
3+
_T = TypeVar('_T')
4+
5+
class object:
6+
def __init__(self) -> None: pass
7+
8+
class type:
9+
def __init__(self, x: Any) -> None: pass
10+
11+
class function: pass
12+
13+
class property:
14+
fget: Callable[[Any], Any] | None
15+
fset: Callable[[Any, Any], None] | None
16+
fdel: Callable[[Any], None] | None
17+
__isabstractmethod__: bool
18+
19+
def __init__(
20+
self,
21+
fget: Callable[[Any], Any] | None = ...,
22+
fset: Callable[[Any, Any], None] | None = ...,
23+
fdel: Callable[[Any], None] | None = ...,
24+
doc: str | None = ...,
25+
) -> None: ...
26+
def getter(self, fget: Callable[[Any], Any], /) -> property: ...
27+
def setter(self, fset: Callable[[Any, Any], None], /) -> property: ...
28+
def deleter(self, fdel: Callable[[Any], None], /) -> property: ...
29+
def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ...
30+
def __set__(self, instance: Any, value: Any, /) -> None: ...
31+
def __delete__(self, instance: Any, /) -> None: ...
32+
class classmethod: pass
33+
34+
class list: pass
35+
class dict: pass
36+
class int: pass
37+
class float: pass
38+
class str: pass
39+
class bytes: pass
40+
class bool: pass
41+
class ellipsis: pass
42+
43+
class tuple(Generic[_T]): pass

0 commit comments

Comments
 (0)