From 55411e67fd19de3f33bf19f05868e8daeff0d470 Mon Sep 17 00:00:00 2001 From: "Aryan Singh K." <70511529+aryansk@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:49:53 +0530 Subject: [PATCH] Fix cached_property access through classes --- mypy/cache.py | 2 +- mypy/checkmember.py | 14 ++++++++++++++ mypy/nodes.py | 14 +++++++++++++- mypy/semanal.py | 1 + test-data/unit/check-functools.test | 24 ++++++++++++++++++++++++ 5 files changed, 53 insertions(+), 2 deletions(-) diff --git a/mypy/cache.py b/mypy/cache.py index 013a286fae2c8..9a9adbb419764 100644 --- a/mypy/cache.py +++ b/mypy/cache.py @@ -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) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 3ba99d8e8c6b2..cc879ebcb14f3 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -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: diff --git a/mypy/nodes.py b/mypy/nodes.py index cbeda29ec74a2..6285dd3c3c8c0 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -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") @@ -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: @@ -1363,6 +1371,7 @@ 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 @@ -1370,6 +1379,7 @@ 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: @@ -1377,6 +1387,7 @@ def write(self, data: WriteBuffer) -> None: 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 @@ -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 diff --git a/mypy/semanal.py b/mypy/semanal.py index 7f961687a8aee..ef6d72e25b327 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -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) diff --git a/test-data/unit/check-functools.test b/test-data/unit/check-functools.test index 77070d61a013c..1a9279996c718 100644 --- a/test-data/unit/check-functools.test +++ b/test-data/unit/check-functools.test @@ -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