Skip to content

Commit d91c778

Browse files
committed
Include subclasses of standard property types as property decorators
1 parent 3636bc2 commit d91c778

File tree

3 files changed

+253
-37
lines changed

3 files changed

+253
-37
lines changed

ChangeLog

+3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ Release date: TBA
3232

3333
Closes #2513
3434

35+
* Include subclasses of standard property classes as `property` decorators
36+
37+
Closes #10377
3538

3639
What's New in astroid 3.3.11?
3740
=============================

astroid/bases.py

+16-10
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from astroid.constraint import Constraint
3939

4040

41-
PROPERTIES = {"builtins.property", "abc.abstractproperty"}
41+
PROPERTIES = {"builtins.property", "abc.abstractproperty", "functools.cached_property"}
4242
if PY310_PLUS:
4343
PROPERTIES.add("enum.property")
4444

@@ -79,24 +79,30 @@ def _is_property(
7979
if any(name in stripped for name in POSSIBLE_PROPERTIES):
8080
return True
8181

82-
# Lookup for subclasses of *property*
8382
if not meth.decorators:
8483
return False
84+
# Lookup for subclasses of *property*
8585
for decorator in meth.decorators.nodes or ():
8686
inferred = safe_infer(decorator, context=context)
8787
if inferred is None or isinstance(inferred, UninferableBase):
8888
continue
8989
if isinstance(inferred, nodes.ClassDef):
90+
# Check for a class which inherits from a standard property type
91+
if any(inferred.is_subtype_of(pclass) for pclass in PROPERTIES):
92+
return True
9093
for base_class in inferred.bases:
91-
if not isinstance(base_class, nodes.Name):
94+
# Check for a class which inherits from functools.cached_property
95+
# and includes a subscripted type annotation
96+
if isinstance(base_class, nodes.Subscript):
97+
value = safe_infer(base_class.value, context=context)
98+
if not isinstance(value, nodes.ClassDef):
99+
continue
100+
if value.name != "cached_property":
101+
continue
102+
module, _ = value.lookup(value.name)
103+
if isinstance(module, nodes.Module) and module.name == "functools":
104+
return True
92105
continue
93-
module, _ = base_class.lookup(base_class.name)
94-
if (
95-
isinstance(module, nodes.Module)
96-
and module.name == "builtins"
97-
and base_class.name == "property"
98-
):
99-
return True
100106

101107
return False
102108

tests/test_nodes.py

+234-27
Original file line numberDiff line numberDiff line change
@@ -924,67 +924,274 @@ def test(self):
924924

925925

926926
class BoundMethodNodeTest(unittest.TestCase):
927-
def test_is_property(self) -> None:
927+
def _is_property(self, ast: nodes.Module, prop: str) -> None:
928+
inferred = next(ast[prop].infer())
929+
self.assertIsInstance(inferred, nodes.Const, prop)
930+
self.assertEqual(inferred.value, 42, prop)
931+
932+
def test_is_standard_property(self) -> None:
933+
# Test to make sure the Python-provided property decorators
934+
# are properly interpreted as properties
928935
ast = builder.parse(
929936
"""
930937
import abc
938+
import functools
931939
932-
def cached_property():
933-
# Not a real decorator, but we don't care
934-
pass
935-
def reify():
936-
# Same as cached_property
937-
pass
938-
def lazy_property():
939-
pass
940-
def lazyproperty():
941-
pass
942-
def lazy(): pass
943940
class A(object):
944941
@property
945-
def builtin_property(self):
946-
return 42
942+
def builtin_property(self): return 42
943+
947944
@abc.abstractproperty
948-
def abc_property(self):
949-
return 42
945+
def abc_property(self): return 42
946+
947+
@property
948+
@abc.abstractmethod
949+
def abstractmethod_property(self): return 42
950+
951+
@functools.cached_property
952+
def functools_property(self): return 42
953+
954+
cls = A()
955+
builtin_p = cls.builtin_property
956+
abc_p = cls.abc_property
957+
abstractmethod_p = cls.abstractmethod_property
958+
functools_p = cls.functools_property
959+
"""
960+
)
961+
for prop in (
962+
"builtin_p",
963+
"abc_p",
964+
"abstractmethod_p",
965+
"functools_p",
966+
):
967+
self._is_property(ast, prop)
968+
969+
@pytest.mark.skipif(not PY310_PLUS, reason="Uses enum.property")
970+
def test_is_standard_property_py310(self) -> None:
971+
# Test to make sure the Python-provided property decorators
972+
# are properly interpreted as properties
973+
ast = builder.parse(
974+
"""
975+
import enum
976+
977+
class A(object):
978+
@enum.property
979+
def enum_property(self): return 42
980+
981+
cls = A()
982+
enum_p = cls.enum_property
983+
"""
984+
)
985+
self._is_property(ast, "enum_p")
986+
987+
def test_is_possible_property(self) -> None:
988+
# Test to make sure that decorators with POSSIBLE_PROPERTIES names
989+
# are properly interpreted as properties
990+
ast = builder.parse(
991+
"""
992+
# Not real decorators, but we don't care
993+
def cachedproperty(): pass
994+
def cached_property(): pass
995+
def reify(): pass
996+
def lazy_property(): pass
997+
def lazyproperty(): pass
998+
def lazy(): pass
999+
def lazyattribute(): pass
1000+
def lazy_attribute(): pass
1001+
def LazyProperty(): pass
1002+
def DynamicClassAttribute(): pass
1003+
1004+
class A(object):
1005+
@cachedproperty
1006+
def cachedproperty(self): return 42
1007+
9501008
@cached_property
9511009
def cached_property(self): return 42
1010+
9521011
@reify
9531012
def reified(self): return 42
1013+
9541014
@lazy_property
9551015
def lazy_prop(self): return 42
1016+
9561017
@lazyproperty
9571018
def lazyprop(self): return 42
958-
def not_prop(self): pass
1019+
9591020
@lazy
9601021
def decorated_with_lazy(self): return 42
9611022
1023+
@lazyattribute
1024+
def lazyattribute(self): return 42
1025+
1026+
@lazy_attribute
1027+
def lazy_attribute(self): return 42
1028+
1029+
@LazyProperty
1030+
def LazyProperty(self): return 42
1031+
1032+
@DynamicClassAttribute
1033+
def DynamicClassAttribute(self): return 42
1034+
9621035
cls = A()
963-
builtin_property = cls.builtin_property
964-
abc_property = cls.abc_property
1036+
cachedp = cls.cachedproperty
9651037
cached_p = cls.cached_property
9661038
reified = cls.reified
967-
not_prop = cls.not_prop
9681039
lazy_prop = cls.lazy_prop
9691040
lazyprop = cls.lazyprop
9701041
decorated_with_lazy = cls.decorated_with_lazy
1042+
lazya = cls.lazyattribute
1043+
lazy_a = cls.lazy_attribute
1044+
LazyP = cls.LazyProperty
1045+
DynamicClassA = cls.DynamicClassAttribute
9711046
"""
9721047
)
9731048
for prop in (
974-
"builtin_property",
975-
"abc_property",
1049+
"cachedp",
9761050
"cached_p",
9771051
"reified",
9781052
"lazy_prop",
9791053
"lazyprop",
9801054
"decorated_with_lazy",
1055+
"lazya",
1056+
"lazy_a",
1057+
"LazyP",
1058+
"DynamicClassA",
9811059
):
982-
inferred = next(ast[prop].infer())
983-
self.assertIsInstance(inferred, nodes.Const, prop)
984-
self.assertEqual(inferred.value, 42, prop)
1060+
self._is_property(ast, prop)
1061+
1062+
def test_is_standard_property_subclass(self) -> None:
1063+
# Test to make sure that subclasses of the Python-provided property decorators
1064+
# are properly interpreted as properties
1065+
ast = builder.parse(
1066+
"""
1067+
import abc
1068+
import functools
1069+
from typing import Generic, TypeVar
1070+
1071+
class user_property(property): pass
1072+
class user_abc_property(abc.abstractproperty): pass
1073+
class user_functools_property(functools.cached_property): pass
1074+
T = TypeVar('T')
1075+
class annotated_user_functools_property(functools.cached_property[T], Generic[T]): pass
1076+
1077+
class A(object):
1078+
@user_property
1079+
def user_property(self): return 42
9851080
986-
inferred = next(ast["not_prop"].infer())
987-
self.assertIsInstance(inferred, bases.BoundMethod)
1081+
@user_abc_property
1082+
def user_abc_property(self): return 42
1083+
1084+
@user_functools_property
1085+
def user_functools_property(self): return 42
1086+
1087+
@annotated_user_functools_property
1088+
def annotated_user_functools_property(self): return 42
1089+
1090+
cls = A()
1091+
user_p = cls.user_property
1092+
user_abc_p = cls.user_abc_property
1093+
user_functools_p = cls.user_functools_property
1094+
annotated_user_functools_p = cls.annotated_user_functools_property
1095+
"""
1096+
)
1097+
for prop in (
1098+
"user_p",
1099+
"user_abc_p",
1100+
"user_functools_p",
1101+
"annotated_user_functools_p",
1102+
):
1103+
self._is_property(ast, prop)
1104+
1105+
@pytest.mark.skipif(not PY310_PLUS, reason="Uses enum.property")
1106+
def test_is_standard_property_subclass_py310(self) -> None:
1107+
# Test to make sure that subclasses of the Python-provided property decorators
1108+
# are properly interpreted as properties
1109+
ast = builder.parse(
1110+
"""
1111+
import enum
1112+
1113+
class user_enum_property(enum.property): pass
1114+
1115+
class A(object):
1116+
@user_enum_property
1117+
def user_enum_property(self): return 42
1118+
1119+
cls = A()
1120+
user_enum_p = cls.user_enum_property
1121+
"""
1122+
)
1123+
self._is_property(ast, "user_enum_p")
1124+
1125+
@pytest.mark.skipif(not PY312_PLUS, reason="Uses 3.12 generic typing syntax")
1126+
def test_is_standard_property_subclass_py312(self) -> None:
1127+
ast = builder.parse(
1128+
"""
1129+
from functools import cached_property
1130+
1131+
class annotated_user_cached_property[T](cached_property[T]):
1132+
pass
1133+
1134+
class A(object):
1135+
@annotated_user_cached_property
1136+
def annotated_user_cached_property(self): return 42
1137+
1138+
cls = A()
1139+
annotated_user_cached_p = cls.annotated_user_cached_property
1140+
"""
1141+
)
1142+
self._is_property(ast, "annotated_user_cached_p")
1143+
1144+
def test_is_not_property(self) -> None:
1145+
ast = builder.parse(
1146+
"""
1147+
from collections.abc import Iterator
1148+
1149+
class cached_property: pass
1150+
# If a decorator is named cached_property, we will accept it as a property,
1151+
# even if it isn't functools.cached_property.
1152+
# However, do not extend the same leniency to superclasses of decorators.
1153+
class wrong_superclass_type1(cached_property): pass
1154+
class wrong_superclass_type2(cached_property[float]): pass
1155+
cachedproperty = { float: int }
1156+
class wrong_superclass_type3(cachedproperty[float]): pass
1157+
class wrong_superclass_type4(Iterator[float]): pass
1158+
1159+
class A(object):
1160+
def no_decorator(self): return 42
1161+
1162+
def property(self): return 42
1163+
1164+
@wrong_superclass_type1
1165+
def wrong_superclass_type1(self): return 42
1166+
1167+
@wrong_superclass_type2
1168+
def wrong_superclass_type2(self): return 42
1169+
1170+
@wrong_superclass_type3
1171+
def wrong_superclass_type3(self): return 42
1172+
1173+
@wrong_superclass_type4
1174+
def wrong_superclass_type4(self): return 42
1175+
1176+
cls = A()
1177+
no_decorator = cls.no_decorator
1178+
not_prop = cls.property
1179+
bad_superclass1 = cls.wrong_superclass_type1
1180+
bad_superclass2 = cls.wrong_superclass_type2
1181+
bad_superclass3 = cls.wrong_superclass_type3
1182+
bad_superclass4 = cls.wrong_superclass_type4
1183+
"""
1184+
)
1185+
for prop in (
1186+
"no_decorator",
1187+
"not_prop",
1188+
"bad_superclass1",
1189+
"bad_superclass2",
1190+
"bad_superclass3",
1191+
"bad_superclass4",
1192+
):
1193+
inferred = next(ast[prop].infer())
1194+
self.assertIsInstance(inferred, bases.BoundMethod)
9881195

9891196

9901197
class AliasesTest(unittest.TestCase):

0 commit comments

Comments
 (0)