Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@
ANY_STRATEGY,
MYPYC_NATIVE_INT_NAMES,
OVERLOAD_NAMES,
PROPERTY_DECORATOR_NAMES,
AnyType,
BoolTypeQuery,
CallableType,
Expand Down Expand Up @@ -2445,6 +2446,41 @@ def check_method_override_for_base_with_name(
)
return False

def get_property_instance(
self, method: Var | Decorator | OverloadedFuncDef
) -> Instance | None:
if method.type is None:
return None

func: SymbolNode | None = method
if isinstance(method, Var):
mt = get_proper_type(method.type)
if isinstance(mt, Overloaded):
func = mt.items[0].definition
elif isinstance(mt, CallableType):
func = mt.definition
if not isinstance(func, (Decorator, OverloadedFuncDef)):
return None
deco = func if isinstance(func, Decorator) else func.items[0]
if not isinstance(deco, Decorator):
return None
property_deco_name = next(
(
name
for d in deco.original_decorators
for name in PROPERTY_DECORATOR_NAMES
if refers_to_fullname(d, name)
),
None,
)
if property_deco_name is not None:
# Extra attr preserves the underlying node to support alias assignments
# (see testPropertyAliasInClassBody)
return self.named_type(property_deco_name).copy_with_extra_attr(
"__mypy-wrapped-property", method.type
)
return None

def get_op_other_domain(self, tp: FunctionLike) -> Type | None:
if isinstance(tp, CallableType):
if tp.arg_kinds and tp.arg_kinds[0] == ARG_POS:
Expand Down Expand Up @@ -4513,6 +4549,13 @@ def set_inferred_type(self, var: Var, lvalue: Lvalue, type: Type) -> None:
refers to the variable (lvalue). If var is None, do nothing.
"""
if var and not self.current_node_deferred:
p_type = get_proper_type(type)
if (
isinstance(p_type, Instance)
and p_type.extra_attrs
and "__mypy-wrapped-property" in p_type.extra_attrs.attrs
):
type = p_type.extra_attrs.attrs["__mypy-wrapped-property"]
var.type = type
var.is_inferred = True
var.is_ready = True
Expand Down
8 changes: 8 additions & 0 deletions mypy/checker_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
from mypy.nodes import (
ArgKind,
Context,
Decorator,
Expression,
FuncItem,
LambdaExpr,
MypyFile,
Node,
OverloadedFuncDef,
RefExpr,
SymbolNode,
TypeInfo,
Expand Down Expand Up @@ -276,6 +278,12 @@ def get_precise_awaitable_type(self, typ: Type, local_errors: ErrorWatcher) -> T
def add_any_attribute_to_type(self, typ: Type, name: str) -> Type:
raise NotImplementedError

@abstractmethod
def get_property_instance(
self, method: Var | Decorator | OverloadedFuncDef
) -> Instance | None:
raise NotImplementedError

@abstractmethod
def is_defined_in_stub(self, typ: Instance, /) -> bool:
raise NotImplementedError
Expand Down
23 changes: 16 additions & 7 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,18 +355,27 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:

if isinstance(node, Var):
# Variable reference.
result = self.analyze_var_ref(node, e)
if isinstance(result, PartialType):
result = self.chk.handle_partial_var_type(result, lvalue, node, e)
property_type = self.chk.get_property_instance(node)
if property_type is not None:
result = property_type
else:
result = self.analyze_var_ref(node, e)
if isinstance(result, PartialType):
result = self.chk.handle_partial_var_type(result, lvalue, node, e)
elif isinstance(node, Decorator):
result = self.analyze_var_ref(node.var, e)
property_type = self.chk.get_property_instance(node)
if property_type is not None:
result = property_type
else:
result = self.analyze_var_ref(node.var, e)
elif isinstance(node, OverloadedFuncDef):
if node.type is None:
result = node.type
if result is None:
if self.chk.in_checked_function() and node.items:
self.chk.handle_cannot_determine_type(node.name, e)
result = AnyType(TypeOfAny.from_error)
else:
result = node.type
elif property_type := self.chk.get_property_instance(node):
result = property_type
elif isinstance(node, (FuncDef, TypeInfo, TypeAlias, MypyFile, TypeVarLikeExpr)):
result = self.analyze_static_reference(node, e, e.is_alias_rvalue or lvalue)
else:
Expand Down
5 changes: 5 additions & 0 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -1266,6 +1266,11 @@ def analyze_class_attribute_access(
erase_vars.add(itype.type.self_type)
t = erase_typevars(t, {tv.id for tv in erase_vars})

if isinstance(node.node, (Var, Decorator, OverloadedFuncDef)):
property_type = mx.chk.get_property_instance(node.node)
if property_type is not None:
return property_type

is_classmethod = (
(is_decorated and cast(Decorator, node.node).func.is_class)
or (isinstance(node.node, SYMBOL_FUNCBASE_TYPES) and node.node.is_class)
Expand Down
12 changes: 2 additions & 10 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@
NEVER_NAMES,
OVERLOAD_NAMES,
OVERRIDE_DECORATOR_NAMES,
PROPERTY_DECORATOR_NAMES,
PROTOCOL_NAMES,
REVEAL_TYPE_NAMES,
TPDICT_NAMES,
Expand Down Expand Up @@ -1693,16 +1694,7 @@ def visit_decorator(self, dec: Decorator) -> None:
removed.append(i)
dec.func.is_explicit_override = True
self.check_decorated_function_is_method("override", dec)
elif refers_to_fullname(
d,
(
"builtins.property",
"abc.abstractproperty",
"functools.cached_property",
"enum.property",
"types.DynamicClassAttribute",
),
):
elif refers_to_fullname(d, PROPERTY_DECORATOR_NAMES):
removed.append(i)
dec.func.is_property = True
dec.var.is_property = True
Expand Down
2 changes: 1 addition & 1 deletion mypy/test/teststubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ class bytes: ...

class list(Sequence[T]): ...

def property(f: T) -> T: ...
class property: ...
def classmethod(f: T) -> T: ...
def staticmethod(f: T) -> T: ...
"""
Expand Down
9 changes: 9 additions & 0 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,15 @@
# Supported @override decorator names.
OVERRIDE_DECORATOR_NAMES: Final = ("typing.override", "typing_extensions.override")

# Supported property decorators
PROPERTY_DECORATOR_NAMES: Final = (
"builtins.property",
"abc.abstractproperty",
"functools.cached_property",
"enum.property",
"types.DynamicClassAttribute",
)

ELLIPSIS_TYPE_NAMES: Final = ("builtins.ellipsis", "types.EllipsisType")

# A placeholder used for Bogus[...] parameters
Expand Down
1 change: 1 addition & 0 deletions test-data/unit/check-callable.test
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ class A:

g2 = f2

reveal_type(A().f) # N: Revealed type is "builtins.int"
reveal_type(A().g) # N: Revealed type is "builtins.int"
reveal_type(A().g2) # N: Revealed type is "builtins.int"
A().g = 1 # E: Property "g" defined in "A" is read-only
Expand Down
87 changes: 82 additions & 5 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -1642,7 +1642,7 @@ class A:
self.x = 1
self.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
return ''
[builtins fixtures/property.pyi]
[builtins fixtures/property-full.pyi]

[case testPropertyNameIsChecked]
class A:
Expand Down Expand Up @@ -1727,7 +1727,7 @@ class A:
a = A()
a.f.xx
a.f = '' # E: Property "f" defined in "A" is read-only
[builtins fixtures/property.pyi]
[builtins fixtures/property-full.pyi]

[case testPropertyWithSetter]
import typing
Expand All @@ -1744,7 +1744,7 @@ a.f.x # E: "int" has no attribute "x"
a.f = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
a.f = 1
reveal_type(a.f) # N: Revealed type is "builtins.int"
[builtins fixtures/property.pyi]
[builtins fixtures/property-full.pyi]

[case testPropertyWithDeleterButNoSetter]
import typing
Expand All @@ -1758,7 +1758,60 @@ class A:
a = A()
a.f = a.f # E: Property "f" defined in "A" is read-only
a.f.x # E: "int" has no attribute "x"
[builtins fixtures/property.pyi]
[builtins fixtures/property-full.pyi]

[case testPropertyAccessOnClass]
class Foo:
@property
def bar(self) -> bool:
return True

reveal_type(bar) # N: Revealed type is "builtins.property"

reveal_type(Foo.bar) # N: Revealed type is "builtins.property"
reveal_type(Foo.bar(Foo())) # E: "property" not callable \
# N: Revealed type is "Any"
reveal_type(Foo.bar.fget(Foo())) # E: "None" not callable \
# N: Revealed type is "Any"

class Bar:
@property
def bar(self) -> bool:
return True
@bar.setter
def bar(self, bar: bool) -> None:
pass

reveal_type(bar) # N: Revealed type is "builtins.property"

reveal_type(Bar.bar) # N: Revealed type is "builtins.property"
reveal_type(Bar.bar(Bar())) # E: "property" not callable \
# N: Revealed type is "Any"
reveal_type(Bar.bar.fget(Bar())) # E: "None" not callable \
# N: Revealed type is "Any"
[builtins fixtures/property-full.pyi]

[case testPropertyAccessOnClass2]
import functools
from functools import cached_property

class Foo:
@cached_property
def foo(self) -> bool:
return True

@functools.cached_property
def bar(self) -> bool:
return True

reveal_type(foo) # N: Revealed type is "functools.cached_property[Any]"
reveal_type(bar) # N: Revealed type is "functools.cached_property[Any]"

reveal_type(Foo.foo) # N: Revealed type is "functools.cached_property[Any]"
reveal_type(Foo.bar) # N: Revealed type is "functools.cached_property[Any]"
Foo.foo(Foo()) # E: "cached_property[Any]" not callable
Foo.bar(Foo()) # E: "cached_property[Any]" not callable
[builtins fixtures/property-full.pyi]

-- Descriptors
-- -----------
Expand Down Expand Up @@ -9255,14 +9308,38 @@ class B:

class C(B):
prop = A.prop
prop_t = AT.prop # E: Incompatible types in assignment (expression has type "C", base class "B" defined the type as "str")
prop_t = AT.prop

reveal_type(C().prop) # N: Revealed type is "builtins.str"
C().prop = "no" # E: Invalid self argument "C" to attribute function "prop" with type "Callable[[A, str], None]"
reveal_type(C().prop_t) # N: Revealed type is "__main__.C"
C().prop_t = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "list[C]")
[builtins fixtures/property.pyi]

[case testPropertyAliasClassAccess]
class A:
@property
def ro(self) -> int: ...

@property
def rw(self) -> int: ...
@rw.setter
def rw(self, val: int) -> None: ...

class B:
ro = A.ro
rw = A.rw

reveal_type(A.ro) # N: Revealed type is "builtins.property"
reveal_type(A.rw) # N: Revealed type is "builtins.property"
reveal_type(B.ro) # N: Revealed type is "builtins.property"
reveal_type(B.rw) # N: Revealed type is "builtins.property"
reveal_type(B().ro) # E: Invalid self argument "B" to attribute function "ro" with type "Callable[[A], int]" \
# N: Revealed type is "builtins.int"
reveal_type(B().rw) # E: Invalid self argument "B" to attribute function "rw" with type "Callable[[A], int]" \
# N: Revealed type is "builtins.int"
[builtins fixtures/property.pyi]

[case testClassEqDecoratedAbstractNote]
from abc import abstractmethod

Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-functions.test
Original file line number Diff line number Diff line change
Expand Up @@ -3321,7 +3321,7 @@ class A:
@decorator
def f(self) -> int: ...

reveal_type(A.f) # N: Revealed type is "__main__.something_callable"
reveal_type(A.f) # N: Revealed type is "builtins.property"
reveal_type(A().f) # N: Revealed type is "builtins.str"
[builtins fixtures/property.pyi]

Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-protocols.test
Original file line number Diff line number Diff line change
Expand Up @@ -3346,7 +3346,7 @@ def test(arg: P) -> None: ...
# TODO: skip type mismatch diagnostics in this case.
test(B) # E: Argument 1 to "test" has incompatible type "type[B]"; expected "P" \
# N: Following member(s) of "B" have conflicts: \
# N: foo: expected "int", got "Callable[[B], int]" \
# N: foo: expected "int", got "property" \
# N: Only class variables allowed for class object access on protocols, foo is an instance variable of "B"
test(C) # E: Argument 1 to "test" has incompatible type "type[C]"; expected "P" \
# N: Only class variables allowed for class object access on protocols, foo is an instance variable of "C"
Expand Down
43 changes: 43 additions & 0 deletions test-data/unit/fixtures/property-full.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from typing import Any, Callable, Generic, TypeVar

_T = TypeVar('_T')

class object:
def __init__(self) -> None: pass

class type:
def __init__(self, x: Any) -> None: pass

class function: pass

class property:
fget: Callable[[Any], Any] | None
fset: Callable[[Any, Any], None] | None
fdel: Callable[[Any], None] | None
__isabstractmethod__: bool

def __init__(
self,
fget: Callable[[Any], Any] | None = ...,
fset: Callable[[Any, Any], None] | None = ...,
fdel: Callable[[Any], None] | None = ...,
doc: str | None = ...,
) -> None: ...
def getter(self, fget: Callable[[Any], Any], /) -> property: ...
def setter(self, fset: Callable[[Any, Any], None], /) -> property: ...
def deleter(self, fdel: Callable[[Any], None], /) -> property: ...
def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ...
def __set__(self, instance: Any, value: Any, /) -> None: ...
def __delete__(self, instance: Any, /) -> None: ...
class classmethod: pass

class list: pass
class dict: pass
class int: pass
class float: pass
class str: pass
class bytes: pass
class bool: pass
class ellipsis: pass

class tuple(Generic[_T]): pass
2 changes: 1 addition & 1 deletion test-data/unit/fixtures/property.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class type:

class function: pass

property = object() # Dummy definition
class property: pass # Dummy definition
class classmethod: pass

class list(typing.Generic[_T]): pass
Expand Down