From 28e5d7406df69f7af029c0db93bde7eb5133ac85 Mon Sep 17 00:00:00 2001 From: "Aryan Singh K." <70511529+aryansk@users.noreply.github.com> Date: Sun, 16 Aug 2026 09:48:54 +0530 Subject: [PATCH] Fix class access to cached_property attributes (#21825) Accessing a functools.cached_property through the class object (e.g. `cls.value.attrname` inside a classmethod) previously exposed the getter as a bare callable, so descriptor attributes like `attrname` and `func` were reported as missing. At runtime the value is the `cached_property` instance itself, so type it as such and let the descriptor machinery (`__get__(None, owner) -> Self`) apply. --- mypy/checkmember.py | 16 ++++++++++++++++ test-data/unit/check-functools.test | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 3ba99d8e8c6b2..e6a728c1aca16 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -33,6 +33,7 @@ MypyFile, NameExpr, OverloadedFuncDef, + RefExpr, SymbolTable, TempNode, TypeAlias, @@ -1304,6 +1305,21 @@ def analyze_class_attribute_access( result = t # __set__ is not called on class objects. if not mx.is_lvalue: + if is_decorated and any( + isinstance(d, RefExpr) and d.fullname == "functools.cached_property" + for d in cast(Decorator, node.node).original_decorators + ): + # Accessing a functools.cached_property through the class object + # returns the descriptor itself, not the getter. At runtime the + # value is a ``cached_property`` instance (with attributes like + # ``attrname`` and ``func``), which typeshed models via + # ``__get__(self, instance: None, ...) -> Self``. (#21825) + getter_type = get_proper_type(t) + if isinstance(getter_type, CallableType): + cached_property = lookup_stdlib_typeinfo( + "functools.cached_property", modules_state.modules + ) + result = Instance(cached_property, [getter_type.ret_type]) result = analyze_descriptor_access(result, mx) return apply_class_attr_hook(mx, hook, result) diff --git a/test-data/unit/check-functools.test b/test-data/unit/check-functools.test index 77070d61a013c..5831b6aabc165 100644 --- a/test-data/unit/check-functools.test +++ b/test-data/unit/check-functools.test @@ -125,6 +125,22 @@ _T = TypeVar('_T') class cached_property(Generic[_T]): ... [builtins fixtures/property.pyi] +[case testCachedPropertyClassAccess] +# https://github.com/python/mypy/issues/21825 +from functools import cached_property + +class A: + @cached_property + def value(self) -> int: + return 1 + +reveal_type(A.value) # N: Revealed type is "functools.cached_property[builtins.int]" + +# The descriptor instance is returned, so its attributes are accessible. +x: str | None = A.value.attrname +y: int = A.value.func(A()) +[builtins fixtures/property.pyi] + [case testTotalOrderingWithForwardReference] from typing import Generic, Any, TypeVar import functools