diff --git a/mypy/checker.py b/mypy/checker.py index 159569849061..5c02fb8ce5e2 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -182,6 +182,7 @@ ANY_STRATEGY, MYPYC_NATIVE_INT_NAMES, OVERLOAD_NAMES, + PROPERTY_DECORATOR_NAMES, AnyType, BoolTypeQuery, CallableType, @@ -2335,6 +2336,29 @@ def check_method_override_for_base_with_name( ) return False + def get_property_instance(self, method: Decorator | OverloadedFuncDef) -> Instance | None: + if method.type is None: + return None + deco = method if isinstance(method, Decorator) else method.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: @@ -4394,6 +4418,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 diff --git a/mypy/checker_shared.py b/mypy/checker_shared.py index 65cec41d5202..8a5d1de1cece 100644 --- a/mypy/checker_shared.py +++ b/mypy/checker_shared.py @@ -15,11 +15,13 @@ from mypy.nodes import ( ArgKind, Context, + Decorator, Expression, FuncItem, LambdaExpr, MypyFile, Node, + OverloadedFuncDef, RefExpr, SymbolNode, TypeInfo, @@ -277,6 +279,10 @@ def checking_await_set(self) -> Iterator[None]: def get_precise_awaitable_type(self, typ: Type, local_errors: ErrorWatcher) -> Type | None: raise NotImplementedError + @abstractmethod + def get_property_instance(self, method: Decorator | OverloadedFuncDef) -> Instance | None: + raise NotImplementedError + @abstractmethod def is_defined_in_stub(self, typ: Instance, /) -> bool: raise NotImplementedError diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 8223ccfe4ca0..69c5976f7b8b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -384,14 +384,19 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: 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: diff --git a/mypy/checkmember.py b/mypy/checkmember.py index ef38cc3a0dcf..58edb26aca02 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -1226,6 +1226,16 @@ 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, Decorator) + and node.node.func.is_property + or isinstance(node.node, OverloadedFuncDef) + and node.node.is_property + ): + 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) diff --git a/mypy/semanal.py b/mypy/semanal.py index 01b7f4989d80..e265746c68ba 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -260,6 +260,7 @@ NEVER_NAMES, OVERLOAD_NAMES, OVERRIDE_DECORATOR_NAMES, + PROPERTY_DECORATOR_NAMES, PROTOCOL_NAMES, REVEAL_TYPE_NAMES, TPDICT_NAMES, @@ -1691,16 +1692,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 diff --git a/mypy/types.py b/mypy/types.py index 05b02acc68c0..c973990c5088 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -181,6 +181,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 diff --git a/test-data/unit/check-callable.test b/test-data/unit/check-callable.test index 23db0bf50a4e..3727f52a001f 100644 --- a/test-data/unit/check-callable.test +++ b/test-data/unit/check-callable.test @@ -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 diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index ae91815d1e9e..65455d7fbbdd 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -1621,7 +1621,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: @@ -1706,7 +1706,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 @@ -1723,7 +1723,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 @@ -1737,7 +1737,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 -- ----------- diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index 7fa34a398ea0..ba0ebecf3076 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -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] diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 79207c9aad56..76b4877c5be3 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -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" diff --git a/test-data/unit/fixtures/property-full.pyi b/test-data/unit/fixtures/property-full.pyi new file mode 100644 index 000000000000..378a7b91028f --- /dev/null +++ b/test-data/unit/fixtures/property-full.pyi @@ -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 diff --git a/test-data/unit/fixtures/property.pyi b/test-data/unit/fixtures/property.pyi index 933868ac9907..9d3c0005b7fe 100644 --- a/test-data/unit/fixtures/property.pyi +++ b/test-data/unit/fixtures/property.pyi @@ -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