Skip to content

Commit 30128b7

Browse files
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]>
1 parent 2be278d commit 30128b7

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
@@ -32,6 +32,11 @@ Release date: TBA
3232

3333
Closes #2513
3434

35+
* Include subclasses of standard property classes as `property` decorators
36+
37+
Closes #10377
38+
39+
* Modify ``astroid.bases`` and ``tests.test_nodes`` to reflect that `enum.property` was added in Python 3.11, not 3.10
3540

3641
What's New in astroid 3.3.11?
3742
=============================

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,
@@ -927,67 +927,274 @@ def test(self):
927927

928928

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

9921199

9931200
class AliasesTest(unittest.TestCase):

0 commit comments

Comments
 (0)