diff --git a/mypy/checker.py b/mypy/checker.py index 3bee7b633339..c97407032dcd 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -198,6 +198,7 @@ ANY_STRATEGY, MYPYC_NATIVE_INT_NAMES, OVERLOAD_NAMES, + PROPERTY_DECORATOR_NAMES, AnyType, BoolTypeQuery, CallableType, @@ -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: @@ -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 diff --git a/mypy/checker_shared.py b/mypy/checker_shared.py index 0014d2c6fc88..748ba3c35541 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, @@ -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 diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index b8f9bf087467..e0d8be4df1ef 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -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: diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 719b48b14e07..30551f95c03d 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -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) diff --git a/mypy/semanal.py b/mypy/semanal.py index 08f9eb03c9d7..bc83fb5c990b 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -261,6 +261,7 @@ NEVER_NAMES, OVERLOAD_NAMES, OVERRIDE_DECORATOR_NAMES, + PROPERTY_DECORATOR_NAMES, PROTOCOL_NAMES, REVEAL_TYPE_NAMES, TPDICT_NAMES, @@ -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 diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index dfbde217e82f..a3a4b4ce797f 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -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: ... """ diff --git a/mypy/types.py b/mypy/types.py index 426d560c2bf7..dd64c7b96e71 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -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 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 c0b1114db512..ed6d1f634350 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -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: @@ -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 @@ -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 @@ -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 -- ----------- @@ -9255,7 +9308,7 @@ 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]" @@ -9263,6 +9316,30 @@ 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 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 0f19b404082e..7f511f8e6b8d 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