Skip to content
Draft
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
2 changes: 1 addition & 1 deletion mypy/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
from mypy_extensions import u8

# High-level cache layout format
CACHE_VERSION: Final = 11
CACHE_VERSION: Final = 12

# Type used internally to represent errors:
# (path, line, column, end_line, end_column, severity, message, code)
Expand Down
14 changes: 14 additions & 0 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -1301,6 +1301,20 @@ def analyze_class_attribute_access(
t, mx, cast(Decorator, node.node).var, itype, is_class=is_classmethod
)

proper_t = get_proper_type(t)
if (
is_decorated
and cast(Decorator, node.node).is_cached_property
and isinstance(proper_t, CallableType)
):
# ``cached_property.__get__(None, owner)`` returns the descriptor
# itself, unlike a regular property access. Preserve that type so
# class-level attributes such as ``A.value.attrname`` are valid.
cached_property = Instance(
mx.chk.lookup_typeinfo("functools.cached_property"), [proper_t.ret_type]
)
return apply_class_attr_hook(mx, hook, cached_property)

result = t
# __set__ is not called on class objects.
if not mx.is_lvalue:
Expand Down
14 changes: 13 additions & 1 deletion mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1314,7 +1314,14 @@ class Decorator(SymbolNode, Statement):
A single Decorator object can include any number of function decorators.
"""

__slots__ = ("func", "decorators", "original_decorators", "var", "is_overload")
__slots__ = (
"func",
"decorators",
"original_decorators",
"var",
"is_overload",
"is_cached_property",
)

__match_args__ = ("decorators", "var", "func")

Expand All @@ -1333,6 +1340,7 @@ def __init__(self, func: FuncDef, decorators: list[Expression], var: Var) -> Non
self.original_decorators = decorators.copy()
self.var = var
self.is_overload = False
self.is_cached_property = False

@property
def name(self) -> str:
Expand Down Expand Up @@ -1363,20 +1371,23 @@ def serialize(self) -> JsonDict:
"func": self.func.serialize(),
"var": self.var.serialize(),
"is_overload": self.is_overload,
"is_cached_property": self.is_cached_property,
}

@classmethod
def deserialize(cls, data: JsonDict) -> Decorator:
assert data[".class"] == "Decorator"
dec = Decorator(FuncDef.deserialize(data["func"]), [], Var.deserialize(data["var"]))
dec.is_overload = data["is_overload"]
dec.is_cached_property = data.get("is_cached_property", False)
return dec

def write(self, data: WriteBuffer) -> None:
write_tag(data, DECORATOR)
self.func.write(data)
self.var.write(data)
write_bool(data, self.is_overload)
write_bool(data, self.is_cached_property)
write_tag(data, END_TAG)

@classmethod
Expand All @@ -1387,6 +1398,7 @@ def read(cls, data: ReadBuffer) -> Decorator:
var = Var.read(data)
dec = Decorator(func, [], var)
dec.is_overload = read_bool(data)
dec.is_cached_property = read_bool(data)
assert read_tag(data) == END_TAG
return dec

Expand Down
1 change: 1 addition & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1779,6 +1779,7 @@ def visit_decorator(self, dec: Decorator) -> None:
dec.func.abstract_status = IS_ABSTRACT
elif refers_to_fullname(d, "functools.cached_property"):
dec.var.is_settable_property = True
dec.is_cached_property = True
self.check_decorated_function_is_method("property", dec)
elif refers_to_fullname(d, "typing.no_type_check"):
dec.var.type = AnyType(TypeOfAny.special_form)
Expand Down
24 changes: 24 additions & 0 deletions test-data/unit/check-functools.test
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,30 @@ _T = TypeVar('_T')
class cached_property(Generic[_T]): ...
[builtins fixtures/property.pyi]

[case testCachedPropertyClassAccess]
from functools import cached_property

class A:
@cached_property
def value(self) -> int: ...

@classmethod
def name(cls) -> str:
reveal_type(cls.value) # N: Revealed type is "functools.cached_property[builtins.int]"
return cls.value.attrname or ""

reveal_type(A.value) # N: Revealed type is "functools.cached_property[builtins.int]"
[file functools.pyi]
from typing import Any, Generic, TypeVar, overload
_T = TypeVar('_T')
class cached_property(Generic[_T]):
attrname: str | None
@overload
def __get__(self, instance: None, owner: type[Any] | None = ...) -> cached_property[_T]: ...
@overload
def __get__(self, instance: object, owner: type[Any] | None = ...) -> _T: ...
[builtins fixtures/property.pyi]

[case testTotalOrderingWithForwardReference]
from typing import Generic, Any, TypeVar
import functools
Expand Down
Loading