Skip to content

Commit bf3977c

Browse files
mharding-hpePierre-Sassoulas
authored andcommitted
Include subclasses of standard property classes as property decorators (#2735)
* Include subclasses of standard property types as property decorators * Modify astroid.bases and tests.test_nodes to reflect that enum.property was added in Python 3.11, not 3.10 * Apply suggestions from code review Co-authored-by: Pierre Sassoulas <[email protected]> --------- Co-authored-by: Pierre Sassoulas <[email protected]> (cherry picked from commit 30128b7)
1 parent 18f9626 commit bf3977c

File tree

3 files changed

+259
-40
lines changed

3 files changed

+259
-40
lines changed

ChangeLog

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ What's New in astroid 3.4.0?
88
Release date: TBA
99

1010

11+
* Include subclasses of standard property classes as `property` decorators
12+
13+
Closes #10377
14+
15+
* Modify ``astroid.bases`` and ``tests.test_nodes`` to reflect that `enum.property` was added in Python 3.11, not 3.10
1116

1217
What's New in astroid 3.3.11?
1318
=============================

astroid/bases.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from typing import TYPE_CHECKING, Any, Literal
1414

1515
from astroid import decorators, nodes
16-
from astroid.const import PY310_PLUS
16+
from astroid.const import PY311_PLUS
1717
from astroid.context import (
1818
CallContext,
1919
InferenceContext,
@@ -38,8 +38,9 @@
3838
from astroid.constraint import Constraint
3939

4040

41-
PROPERTIES = {"builtins.property", "abc.abstractproperty"}
42-
if PY310_PLUS:
41+
PROPERTIES = {"builtins.property", "abc.abstractproperty", "functools.cached_property"}
42+
# enum.property was added in Python 3.11
43+
if PY311_PLUS:
4344
PROPERTIES.add("enum.property")
4445

4546
# List of possible property names. We use this list in order
@@ -79,24 +80,30 @@ def _is_property(
7980
if any(name in stripped for name in POSSIBLE_PROPERTIES):
8081
return True
8182

82-
# Lookup for subclasses of *property*
8383
if not meth.decorators:
8484
return False
85+
# Lookup for subclasses of *property*
8586
for decorator in meth.decorators.nodes or ():
8687
inferred = safe_infer(decorator, context=context)
8788
if inferred is None or isinstance(inferred, UninferableBase):
8889
continue
8990
if isinstance(inferred, nodes.ClassDef):
91+
# Check for a class which inherits from a standard property type
92+
if any(inferred.is_subtype_of(pclass) for pclass in PROPERTIES):
93+
return True
9094
for base_class in inferred.bases:
91-
if not isinstance(base_class, nodes.Name):
95+
# Check for a class which inherits from functools.cached_property
96+
# and includes a subscripted type annotation
97+
if isinstance(base_class, nodes.Subscript):
98+
value = safe_infer(base_class.value, context=context)
99+
if not isinstance(value, nodes.ClassDef):
100+
continue
101+
if value.name != "cached_property":
102+
continue
103+
module, _ = value.lookup(value.name)
104+
if isinstance(module, nodes.Module) and module.name == "functools":
105+
return True
92106
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
100107

101108
return False
102109

tests/test_nodes.py

Lines changed: 235 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
transforms,
2929
util,
3030
)
31-
from astroid.const import IS_PYPY, PY310_PLUS, PY312_PLUS, Context
31+
from astroid.const import IS_PYPY, PY310_PLUS, PY311_PLUS, PY312_PLUS, Context
3232
from astroid.context import InferenceContext
3333
from astroid.exceptions import (
3434
AstroidBuildingError,
@@ -929,67 +929,274 @@ def test(self):
929929

930930

931931
class BoundMethodNodeTest(unittest.TestCase):
932-
def test_is_property(self) -> None:
932+
def _is_property(self, ast: nodes.Module, prop: str) -> None:
933+
inferred = next(ast[prop].infer())
934+
self.assertIsInstance(inferred, nodes.Const, prop)
935+
self.assertEqual(inferred.value, 42, prop)
936+
937+
def test_is_standard_property(self) -> None:
938+
# Test to make sure the Python-provided property decorators
939+
# are properly interpreted as properties
933940
ast = builder.parse(
934941
"""
935942
import abc
943+
import functools
936944
937-
def cached_property():
938-
# Not a real decorator, but we don't care
939-
pass
940-
def reify():
941-
# Same as cached_property
942-
pass
943-
def lazy_property():
944-
pass
945-
def lazyproperty():
946-
pass
947-
def lazy(): pass
948945
class A(object):
949946
@property
950-
def builtin_property(self):
951-
return 42
947+
def builtin_property(self): return 42
948+
952949
@abc.abstractproperty
953-
def abc_property(self):
954-
return 42
950+
def abc_property(self): return 42
951+
952+
@property
953+
@abc.abstractmethod
954+
def abstractmethod_property(self): return 42
955+
956+
@functools.cached_property
957+
def functools_property(self): return 42
958+
959+
cls = A()
960+
builtin_p = cls.builtin_property
961+
abc_p = cls.abc_property
962+
abstractmethod_p = cls.abstractmethod_property
963+
functools_p = cls.functools_property
964+
"""
965+
)
966+
for prop in (
967+
"builtin_p",
968+
"abc_p",
969+
"abstractmethod_p",
970+
"functools_p",
971+
):
972+
self._is_property(ast, prop)
973+
974+
@pytest.mark.skipif(not PY311_PLUS, reason="Uses enum.property introduced in 3.11")
975+
def test_is_standard_property_py311(self) -> None:
976+
# Test to make sure the Python-provided property decorators
977+
# are properly interpreted as properties
978+
ast = builder.parse(
979+
"""
980+
import enum
981+
982+
class A(object):
983+
@enum.property
984+
def enum_property(self): return 42
985+
986+
cls = A()
987+
enum_p = cls.enum_property
988+
"""
989+
)
990+
self._is_property(ast, "enum_p")
991+
992+
def test_is_possible_property(self) -> None:
993+
# Test to make sure that decorators with POSSIBLE_PROPERTIES names
994+
# are properly interpreted as properties
995+
ast = builder.parse(
996+
"""
997+
# Not real decorators, but we don't care
998+
def cachedproperty(): pass
999+
def cached_property(): pass
1000+
def reify(): pass
1001+
def lazy_property(): pass
1002+
def lazyproperty(): pass
1003+
def lazy(): pass
1004+
def lazyattribute(): pass
1005+
def lazy_attribute(): pass
1006+
def LazyProperty(): pass
1007+
def DynamicClassAttribute(): pass
1008+
1009+
class A(object):
1010+
@cachedproperty
1011+
def cachedproperty(self): return 42
1012+
9551013
@cached_property
9561014
def cached_property(self): return 42
1015+
9571016
@reify
9581017
def reified(self): return 42
1018+
9591019
@lazy_property
9601020
def lazy_prop(self): return 42
1021+
9611022
@lazyproperty
9621023
def lazyprop(self): return 42
963-
def not_prop(self): pass
1024+
9641025
@lazy
9651026
def decorated_with_lazy(self): return 42
9661027
1028+
@lazyattribute
1029+
def lazyattribute(self): return 42
1030+
1031+
@lazy_attribute
1032+
def lazy_attribute(self): return 42
1033+
1034+
@LazyProperty
1035+
def LazyProperty(self): return 42
1036+
1037+
@DynamicClassAttribute
1038+
def DynamicClassAttribute(self): return 42
1039+
9671040
cls = A()
968-
builtin_property = cls.builtin_property
969-
abc_property = cls.abc_property
1041+
cachedp = cls.cachedproperty
9701042
cached_p = cls.cached_property
9711043
reified = cls.reified
972-
not_prop = cls.not_prop
9731044
lazy_prop = cls.lazy_prop
9741045
lazyprop = cls.lazyprop
9751046
decorated_with_lazy = cls.decorated_with_lazy
1047+
lazya = cls.lazyattribute
1048+
lazy_a = cls.lazy_attribute
1049+
LazyP = cls.LazyProperty
1050+
DynamicClassA = cls.DynamicClassAttribute
9761051
"""
9771052
)
9781053
for prop in (
979-
"builtin_property",
980-
"abc_property",
1054+
"cachedp",
9811055
"cached_p",
9821056
"reified",
9831057
"lazy_prop",
9841058
"lazyprop",
9851059
"decorated_with_lazy",
1060+
"lazya",
1061+
"lazy_a",
1062+
"LazyP",
1063+
"DynamicClassA",
9861064
):
987-
inferred = next(ast[prop].infer())
988-
self.assertIsInstance(inferred, nodes.Const, prop)
989-
self.assertEqual(inferred.value, 42, prop)
1065+
self._is_property(ast, prop)
1066+
1067+
def test_is_standard_property_subclass(self) -> None:
1068+
# Test to make sure that subclasses of the Python-provided property decorators
1069+
# are properly interpreted as properties
1070+
ast = builder.parse(
1071+
"""
1072+
import abc
1073+
import functools
1074+
from typing import Generic, TypeVar
1075+
1076+
class user_property(property): pass
1077+
class user_abc_property(abc.abstractproperty): pass
1078+
class user_functools_property(functools.cached_property): pass
1079+
T = TypeVar('T')
1080+
class annotated_user_functools_property(functools.cached_property[T], Generic[T]): pass
1081+
1082+
class A(object):
1083+
@user_property
1084+
def user_property(self): return 42
9901085
991-
inferred = next(ast["not_prop"].infer())
992-
self.assertIsInstance(inferred, bases.BoundMethod)
1086+
@user_abc_property
1087+
def user_abc_property(self): return 42
1088+
1089+
@user_functools_property
1090+
def user_functools_property(self): return 42
1091+
1092+
@annotated_user_functools_property
1093+
def annotated_user_functools_property(self): return 42
1094+
1095+
cls = A()
1096+
user_p = cls.user_property
1097+
user_abc_p = cls.user_abc_property
1098+
user_functools_p = cls.user_functools_property
1099+
annotated_user_functools_p = cls.annotated_user_functools_property
1100+
"""
1101+
)
1102+
for prop in (
1103+
"user_p",
1104+
"user_abc_p",
1105+
"user_functools_p",
1106+
"annotated_user_functools_p",
1107+
):
1108+
self._is_property(ast, prop)
1109+
1110+
@pytest.mark.skipif(not PY311_PLUS, reason="Uses enum.property introduced in 3.11")
1111+
def test_is_standard_property_subclass_py311(self) -> None:
1112+
# Test to make sure that subclasses of the Python-provided property decorators
1113+
# are properly interpreted as properties
1114+
ast = builder.parse(
1115+
"""
1116+
import enum
1117+
1118+
class user_enum_property(enum.property): pass
1119+
1120+
class A(object):
1121+
@user_enum_property
1122+
def user_enum_property(self): return 42
1123+
1124+
cls = A()
1125+
user_enum_p = cls.user_enum_property
1126+
"""
1127+
)
1128+
self._is_property(ast, "user_enum_p")
1129+
1130+
@pytest.mark.skipif(not PY312_PLUS, reason="Uses 3.12 generic typing syntax")
1131+
def test_is_standard_property_subclass_py312(self) -> None:
1132+
ast = builder.parse(
1133+
"""
1134+
from functools import cached_property
1135+
1136+
class annotated_user_cached_property[T](cached_property[T]):
1137+
pass
1138+
1139+
class A(object):
1140+
@annotated_user_cached_property
1141+
def annotated_user_cached_property(self): return 42
1142+
1143+
cls = A()
1144+
annotated_user_cached_p = cls.annotated_user_cached_property
1145+
"""
1146+
)
1147+
self._is_property(ast, "annotated_user_cached_p")
1148+
1149+
def test_is_not_property(self) -> None:
1150+
ast = builder.parse(
1151+
"""
1152+
from collections.abc import Iterator
1153+
1154+
class cached_property: pass
1155+
# If a decorator is named cached_property, we will accept it as a property,
1156+
# even if it isn't functools.cached_property.
1157+
# However, do not extend the same leniency to superclasses of decorators.
1158+
class wrong_superclass_type1(cached_property): pass
1159+
class wrong_superclass_type2(cached_property[float]): pass
1160+
cachedproperty = { float: int }
1161+
class wrong_superclass_type3(cachedproperty[float]): pass
1162+
class wrong_superclass_type4(Iterator[float]): pass
1163+
1164+
class A(object):
1165+
def no_decorator(self): return 42
1166+
1167+
def property(self): return 42
1168+
1169+
@wrong_superclass_type1
1170+
def wrong_superclass_type1(self): return 42
1171+
1172+
@wrong_superclass_type2
1173+
def wrong_superclass_type2(self): return 42
1174+
1175+
@wrong_superclass_type3
1176+
def wrong_superclass_type3(self): return 42
1177+
1178+
@wrong_superclass_type4
1179+
def wrong_superclass_type4(self): return 42
1180+
1181+
cls = A()
1182+
no_decorator = cls.no_decorator
1183+
not_prop = cls.property
1184+
bad_superclass1 = cls.wrong_superclass_type1
1185+
bad_superclass2 = cls.wrong_superclass_type2
1186+
bad_superclass3 = cls.wrong_superclass_type3
1187+
bad_superclass4 = cls.wrong_superclass_type4
1188+
"""
1189+
)
1190+
for prop in (
1191+
"no_decorator",
1192+
"not_prop",
1193+
"bad_superclass1",
1194+
"bad_superclass2",
1195+
"bad_superclass3",
1196+
"bad_superclass4",
1197+
):
1198+
inferred = next(ast[prop].infer())
1199+
self.assertIsInstance(inferred, bases.BoundMethod)
9931200

9941201

9951202
class AliasesTest(unittest.TestCase):

0 commit comments

Comments
 (0)