diff --git a/mypy/checker.py b/mypy/checker.py index 7d0b41c516e1..3959e17a0af1 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -174,6 +174,7 @@ ANY_STRATEGY, MYPYC_NATIVE_INT_NAMES, OVERLOAD_NAMES, + PROPERTY_DECORATOR_NAMES, AnyType, BoolTypeQuery, CallableType, @@ -2318,6 +2319,20 @@ def check_method_override_for_base_with_name( ) return False + def get_property_instance(self, method: Decorator) -> Instance | None: + property_deco_name = next( + ( + name + for d in method.original_decorators + for name in PROPERTY_DECORATOR_NAMES + if refers_to_fullname(d, name) + ), + None, + ) + if property_deco_name is not None: + return self.named_type(property_deco_name) + 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: diff --git a/mypy/checker_shared.py b/mypy/checker_shared.py index 6c62af50466c..c2298aea8f1b 100644 --- a/mypy/checker_shared.py +++ b/mypy/checker_shared.py @@ -15,6 +15,7 @@ from mypy.nodes import ( ArgKind, Context, + Decorator, Expression, FuncItem, LambdaExpr, @@ -279,6 +280,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) -> Instance | None: + raise NotImplementedError + class CheckerScope: # We keep two stacks combined, to maintain the relative order diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index e7c2cba3fc55..f902eff6350b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -381,12 +381,15 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: # Reference to a global function. result = function_type(node, self.named_type("builtins.function")) elif isinstance(node, OverloadedFuncDef): + result = node.type if node.type 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 isinstance(node.items[0], Decorator): + property_type = self.chk.get_property_instance(node.items[0]) + if property_type is not None: + result = property_type elif isinstance(node, TypeInfo): # Reference to a type object. if node.typeddict_type: @@ -412,7 +415,11 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: # Reference to a module object. result = self.module_type(node) 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, TypeAlias): # Something that refers to a type alias appears in runtime context. # Note that we suppress bogus errors for alias redefinitions, diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 1a76372d4731..56b09f12cbfe 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -1194,6 +1194,16 @@ def analyze_class_attribute_access( # C[int].x -> int t = erase_typevars(expand_type_by_instance(t, isuper), {tv.id for tv in def_vars}) + if isinstance(node.node, Decorator) and node.node.func.is_property: + property_type = mx.chk.get_property_instance(node.node) + if property_type is not None: + return property_type + if isinstance(node.node, OverloadedFuncDef) and node.node.is_property: + assert isinstance(node.node.items[0], Decorator) + property_type = mx.chk.get_property_instance(node.node.items[0]) + 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 586094b7a6fe..da814656a19d 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -262,6 +262,7 @@ NEVER_NAMES, OVERLOAD_NAMES, OVERRIDE_DECORATOR_NAMES, + PROPERTY_DECORATOR_NAMES, PROTOCOL_NAMES, REVEAL_TYPE_NAMES, TPDICT_NAMES, @@ -1687,16 +1688,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 41a958ae93cc..667620b01444 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -178,6 +178,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", +) + # A placeholder used for Bogus[...] parameters _dummy: Final[Any] = object() diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 65a6a0c9c0a8..6ef0b623fad4 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -1621,8 +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] -[out] +[builtins fixtures/property-full.pyi] [case testDynamicallyTypedProperty] import typing @@ -1632,7 +1631,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 @@ -1649,7 +1648,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 @@ -1663,7 +1662,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 bd59dfbdfd5e..570f78dd9eec 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3298,7 +3298,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/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