Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ Release date: TBA

Closes #2513

* Include subclasses of standard property classes as `property` decorators

Closes #10377

* Modify ``astroid.bases`` and ``tests.test_nodes`` to reflect that `enum.property` was added in Python 3.11, not 3.10

What's New in astroid 3.3.11?
=============================
Expand Down
31 changes: 19 additions & 12 deletions astroid/bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from typing import TYPE_CHECKING, Any, Literal

from astroid import decorators, nodes
from astroid.const import PY310_PLUS
from astroid.const import PY311_PLUS
from astroid.context import (
CallContext,
InferenceContext,
Expand All @@ -38,8 +38,9 @@
from astroid.constraint import Constraint


PROPERTIES = {"builtins.property", "abc.abstractproperty"}
if PY310_PLUS:
PROPERTIES = {"builtins.property", "abc.abstractproperty", "functools.cached_property"}
# enum.property was added in Python 3.11
if PY311_PLUS:
PROPERTIES.add("enum.property")

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

# Lookup for subclasses of *property*
if not meth.decorators:
return False
# Lookup for subclasses of *property*
for decorator in meth.decorators.nodes or ():
inferred = safe_infer(decorator, context=context)
if inferred is None or isinstance(inferred, UninferableBase):
continue
if isinstance(inferred, nodes.ClassDef):
# Check for a class which inherits from a standard property type
if any(inferred.is_subtype_of(pclass) for pclass in PROPERTIES):
return True
for base_class in inferred.bases:
if not isinstance(base_class, nodes.Name):
# Check for a class which inherits from functools.cached_property
# and includes a subscripted type annotation
if isinstance(base_class, nodes.Subscript):
value = safe_infer(base_class.value, context=context)
if not isinstance(value, nodes.ClassDef):
continue
if value.name != "cached_property":
continue
module, _ = value.lookup(value.name)
if isinstance(module, nodes.Module) and module.name == "functools":
return True
continue
module, _ = base_class.lookup(base_class.name)
if (
isinstance(module, nodes.Module)
and module.name == "builtins"
and base_class.name == "property"
):
return True

return False

Expand Down
263 changes: 235 additions & 28 deletions tests/test_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
transforms,
util,
)
from astroid.const import IS_PYPY, PY310_PLUS, PY312_PLUS, Context
from astroid.const import IS_PYPY, PY310_PLUS, PY311_PLUS, PY312_PLUS, Context
from astroid.context import InferenceContext
from astroid.exceptions import (
AstroidBuildingError,
Expand Down Expand Up @@ -924,67 +924,274 @@ def test(self):


class BoundMethodNodeTest(unittest.TestCase):
def test_is_property(self) -> None:
def _is_property(self, ast: nodes.Module, prop: str) -> None:
inferred = next(ast[prop].infer())
self.assertIsInstance(inferred, nodes.Const, prop)
self.assertEqual(inferred.value, 42, prop)

def test_is_standard_property(self) -> None:
# Test to make sure the Python-provided property decorators
# are properly interpreted as properties
ast = builder.parse(
"""
import abc
import functools

def cached_property():
# Not a real decorator, but we don't care
pass
def reify():
# Same as cached_property
pass
def lazy_property():
pass
def lazyproperty():
pass
def lazy(): pass
class A(object):
@property
def builtin_property(self):
return 42
def builtin_property(self): return 42

@abc.abstractproperty
def abc_property(self):
return 42
def abc_property(self): return 42

@property
@abc.abstractmethod
def abstractmethod_property(self): return 42

@functools.cached_property
def functools_property(self): return 42

cls = A()
builtin_p = cls.builtin_property
abc_p = cls.abc_property
abstractmethod_p = cls.abstractmethod_property
functools_p = cls.functools_property
"""
)
for prop in (
"builtin_p",
"abc_p",
"abstractmethod_p",
"functools_p",
):
self._is_property(ast, prop)

@pytest.mark.skipif(not PY311_PLUS, reason="Uses enum.property")
def test_is_standard_property_py311(self) -> None:
# Test to make sure the Python-provided property decorators
# are properly interpreted as properties
ast = builder.parse(
"""
import enum

class A(object):
@enum.property
def enum_property(self): return 42

cls = A()
enum_p = cls.enum_property
"""
)
self._is_property(ast, "enum_p")

def test_is_possible_property(self) -> None:
# Test to make sure that decorators with POSSIBLE_PROPERTIES names
# are properly interpreted as properties
ast = builder.parse(
"""
# Not real decorators, but we don't care
def cachedproperty(): pass
def cached_property(): pass
def reify(): pass
def lazy_property(): pass
def lazyproperty(): pass
def lazy(): pass
def lazyattribute(): pass
def lazy_attribute(): pass
def LazyProperty(): pass
def DynamicClassAttribute(): pass

class A(object):
@cachedproperty
def cachedproperty(self): return 42

@cached_property
def cached_property(self): return 42

@reify
def reified(self): return 42

@lazy_property
def lazy_prop(self): return 42

@lazyproperty
def lazyprop(self): return 42
def not_prop(self): pass

@lazy
def decorated_with_lazy(self): return 42

@lazyattribute
def lazyattribute(self): return 42

@lazy_attribute
def lazy_attribute(self): return 42

@LazyProperty
def LazyProperty(self): return 42

@DynamicClassAttribute
def DynamicClassAttribute(self): return 42

cls = A()
builtin_property = cls.builtin_property
abc_property = cls.abc_property
cachedp = cls.cachedproperty
cached_p = cls.cached_property
reified = cls.reified
not_prop = cls.not_prop
lazy_prop = cls.lazy_prop
lazyprop = cls.lazyprop
decorated_with_lazy = cls.decorated_with_lazy
lazya = cls.lazyattribute
lazy_a = cls.lazy_attribute
LazyP = cls.LazyProperty
DynamicClassA = cls.DynamicClassAttribute
"""
)
for prop in (
"builtin_property",
"abc_property",
"cachedp",
"cached_p",
"reified",
"lazy_prop",
"lazyprop",
"decorated_with_lazy",
"lazya",
"lazy_a",
"LazyP",
"DynamicClassA",
):
inferred = next(ast[prop].infer())
self.assertIsInstance(inferred, nodes.Const, prop)
self.assertEqual(inferred.value, 42, prop)
self._is_property(ast, prop)

def test_is_standard_property_subclass(self) -> None:
# Test to make sure that subclasses of the Python-provided property decorators
# are properly interpreted as properties
ast = builder.parse(
"""
import abc
import functools
from typing import Generic, TypeVar

class user_property(property): pass
class user_abc_property(abc.abstractproperty): pass
class user_functools_property(functools.cached_property): pass
T = TypeVar('T')
class annotated_user_functools_property(functools.cached_property[T], Generic[T]): pass

class A(object):
@user_property
def user_property(self): return 42

inferred = next(ast["not_prop"].infer())
self.assertIsInstance(inferred, bases.BoundMethod)
@user_abc_property
def user_abc_property(self): return 42

@user_functools_property
def user_functools_property(self): return 42

@annotated_user_functools_property
def annotated_user_functools_property(self): return 42

cls = A()
user_p = cls.user_property
user_abc_p = cls.user_abc_property
user_functools_p = cls.user_functools_property
annotated_user_functools_p = cls.annotated_user_functools_property
"""
)
for prop in (
"user_p",
"user_abc_p",
"user_functools_p",
"annotated_user_functools_p",
):
self._is_property(ast, prop)

@pytest.mark.skipif(not PY311_PLUS, reason="Uses enum.property")
def test_is_standard_property_subclass_py311(self) -> None:
# Test to make sure that subclasses of the Python-provided property decorators
# are properly interpreted as properties
ast = builder.parse(
"""
import enum

class user_enum_property(enum.property): pass

class A(object):
@user_enum_property
def user_enum_property(self): return 42

cls = A()
user_enum_p = cls.user_enum_property
"""
)
self._is_property(ast, "user_enum_p")

@pytest.mark.skipif(not PY312_PLUS, reason="Uses 3.12 generic typing syntax")
def test_is_standard_property_subclass_py312(self) -> None:
ast = builder.parse(
"""
from functools import cached_property

class annotated_user_cached_property[T](cached_property[T]):
pass

class A(object):
@annotated_user_cached_property
def annotated_user_cached_property(self): return 42

cls = A()
annotated_user_cached_p = cls.annotated_user_cached_property
"""
)
self._is_property(ast, "annotated_user_cached_p")

def test_is_not_property(self) -> None:
ast = builder.parse(
"""
from collections.abc import Iterator

class cached_property: pass
# If a decorator is named cached_property, we will accept it as a property,
# even if it isn't functools.cached_property.
# However, do not extend the same leniency to superclasses of decorators.
class wrong_superclass_type1(cached_property): pass
class wrong_superclass_type2(cached_property[float]): pass
cachedproperty = { float: int }
class wrong_superclass_type3(cachedproperty[float]): pass
class wrong_superclass_type4(Iterator[float]): pass

class A(object):
def no_decorator(self): return 42

def property(self): return 42

@wrong_superclass_type1
def wrong_superclass_type1(self): return 42

@wrong_superclass_type2
def wrong_superclass_type2(self): return 42

@wrong_superclass_type3
def wrong_superclass_type3(self): return 42

@wrong_superclass_type4
def wrong_superclass_type4(self): return 42

cls = A()
no_decorator = cls.no_decorator
not_prop = cls.property
bad_superclass1 = cls.wrong_superclass_type1
bad_superclass2 = cls.wrong_superclass_type2
bad_superclass3 = cls.wrong_superclass_type3
bad_superclass4 = cls.wrong_superclass_type4
"""
)
for prop in (
"no_decorator",
"not_prop",
"bad_superclass1",
"bad_superclass2",
"bad_superclass3",
"bad_superclass4",
):
inferred = next(ast[prop].infer())
self.assertIsInstance(inferred, bases.BoundMethod)


class AliasesTest(unittest.TestCase):
Expand Down