Skip to content

Commit 55411e6

Browse files
committed
Fix cached_property access through classes
1 parent 5bb72b7 commit 55411e6

5 files changed

Lines changed: 53 additions & 2 deletions

File tree

mypy/cache.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
from mypy_extensions import u8
7373

7474
# High-level cache layout format
75-
CACHE_VERSION: Final = 11
75+
CACHE_VERSION: Final = 12
7676

7777
# Type used internally to represent errors:
7878
# (path, line, column, end_line, end_column, severity, message, code)

mypy/checkmember.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,6 +1301,20 @@ def analyze_class_attribute_access(
13011301
t, mx, cast(Decorator, node.node).var, itype, is_class=is_classmethod
13021302
)
13031303

1304+
proper_t = get_proper_type(t)
1305+
if (
1306+
is_decorated
1307+
and cast(Decorator, node.node).is_cached_property
1308+
and isinstance(proper_t, CallableType)
1309+
):
1310+
# ``cached_property.__get__(None, owner)`` returns the descriptor
1311+
# itself, unlike a regular property access. Preserve that type so
1312+
# class-level attributes such as ``A.value.attrname`` are valid.
1313+
cached_property = Instance(
1314+
mx.chk.lookup_typeinfo("functools.cached_property"), [proper_t.ret_type]
1315+
)
1316+
return apply_class_attr_hook(mx, hook, cached_property)
1317+
13041318
result = t
13051319
# __set__ is not called on class objects.
13061320
if not mx.is_lvalue:

mypy/nodes.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1314,7 +1314,14 @@ class Decorator(SymbolNode, Statement):
13141314
A single Decorator object can include any number of function decorators.
13151315
"""
13161316

1317-
__slots__ = ("func", "decorators", "original_decorators", "var", "is_overload")
1317+
__slots__ = (
1318+
"func",
1319+
"decorators",
1320+
"original_decorators",
1321+
"var",
1322+
"is_overload",
1323+
"is_cached_property",
1324+
)
13181325

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

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

13371345
@property
13381346
def name(self) -> str:
@@ -1363,20 +1371,23 @@ def serialize(self) -> JsonDict:
13631371
"func": self.func.serialize(),
13641372
"var": self.var.serialize(),
13651373
"is_overload": self.is_overload,
1374+
"is_cached_property": self.is_cached_property,
13661375
}
13671376

13681377
@classmethod
13691378
def deserialize(cls, data: JsonDict) -> Decorator:
13701379
assert data[".class"] == "Decorator"
13711380
dec = Decorator(FuncDef.deserialize(data["func"]), [], Var.deserialize(data["var"]))
13721381
dec.is_overload = data["is_overload"]
1382+
dec.is_cached_property = data.get("is_cached_property", False)
13731383
return dec
13741384

13751385
def write(self, data: WriteBuffer) -> None:
13761386
write_tag(data, DECORATOR)
13771387
self.func.write(data)
13781388
self.var.write(data)
13791389
write_bool(data, self.is_overload)
1390+
write_bool(data, self.is_cached_property)
13801391
write_tag(data, END_TAG)
13811392

13821393
@classmethod
@@ -1387,6 +1398,7 @@ def read(cls, data: ReadBuffer) -> Decorator:
13871398
var = Var.read(data)
13881399
dec = Decorator(func, [], var)
13891400
dec.is_overload = read_bool(data)
1401+
dec.is_cached_property = read_bool(data)
13901402
assert read_tag(data) == END_TAG
13911403
return dec
13921404

mypy/semanal.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1779,6 +1779,7 @@ def visit_decorator(self, dec: Decorator) -> None:
17791779
dec.func.abstract_status = IS_ABSTRACT
17801780
elif refers_to_fullname(d, "functools.cached_property"):
17811781
dec.var.is_settable_property = True
1782+
dec.is_cached_property = True
17821783
self.check_decorated_function_is_method("property", dec)
17831784
elif refers_to_fullname(d, "typing.no_type_check"):
17841785
dec.var.type = AnyType(TypeOfAny.special_form)

test-data/unit/check-functools.test

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,30 @@ _T = TypeVar('_T')
125125
class cached_property(Generic[_T]): ...
126126
[builtins fixtures/property.pyi]
127127

128+
[case testCachedPropertyClassAccess]
129+
from functools import cached_property
130+
131+
class A:
132+
@cached_property
133+
def value(self) -> int: ...
134+
135+
@classmethod
136+
def name(cls) -> str:
137+
reveal_type(cls.value) # N: Revealed type is "functools.cached_property[builtins.int]"
138+
return cls.value.attrname or ""
139+
140+
reveal_type(A.value) # N: Revealed type is "functools.cached_property[builtins.int]"
141+
[file functools.pyi]
142+
from typing import Any, Generic, TypeVar, overload
143+
_T = TypeVar('_T')
144+
class cached_property(Generic[_T]):
145+
attrname: str | None
146+
@overload
147+
def __get__(self, instance: None, owner: type[Any] | None = ...) -> cached_property[_T]: ...
148+
@overload
149+
def __get__(self, instance: object, owner: type[Any] | None = ...) -> _T: ...
150+
[builtins fixtures/property.pyi]
151+
128152
[case testTotalOrderingWithForwardReference]
129153
from typing import Generic, Any, TypeVar
130154
import functools

0 commit comments

Comments
 (0)