Skip to content

Commit 80b67a9

Browse files
authored
Use functools.cached_property for 3.8+ (#1417)
1 parent da74553 commit 80b67a9

File tree

8 files changed

+84
-24
lines changed

8 files changed

+84
-24
lines changed

ChangeLog

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ Release date: TBA
99
* Add new (optional) ``doc_node`` attribute to ``nodes.Module``, ``nodes.ClassDef``,
1010
and ``nodes.FunctionDef``.
1111

12+
* Replace custom ``cachedproperty`` with ``functools.cached_property`` and deprecate it
13+
for Python 3.8+.
14+
15+
Closes #1410
16+
1217

1318
What's New in astroid 2.10.1?
1419
=============================

astroid/decorators.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ def cached(func, instance, args, kwargs):
5252
return result
5353

5454

55+
# TODO: Remove when support for 3.7 is dropped
56+
# TODO: astroid 3.0 -> move class behind sys.version_info < (3, 8) guard
5557
class cachedproperty:
5658
"""Provides a cached property equivalent to the stacking of
5759
@cached and @property, but more efficient.
@@ -70,6 +72,12 @@ class cachedproperty:
7072
__slots__ = ("wrapped",)
7173

7274
def __init__(self, wrapped):
75+
if sys.version_info >= (3, 8):
76+
warnings.warn(
77+
"cachedproperty has been deprecated and will be removed in astroid 3.0 for Python 3.8+. "
78+
"Use functools.cached_property instead.",
79+
DeprecationWarning,
80+
)
7381
try:
7482
wrapped.__name__
7583
except AttributeError as exc:

astroid/mixins.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"""This module contains some mixins for the different nodes.
1919
"""
2020
import itertools
21+
import sys
2122
from typing import TYPE_CHECKING, Optional
2223

2324
from astroid import decorators
@@ -26,11 +27,16 @@
2627
if TYPE_CHECKING:
2728
from astroid import nodes
2829

30+
if sys.version_info >= (3, 8) or TYPE_CHECKING:
31+
from functools import cached_property
32+
else:
33+
from astroid.decorators import cachedproperty as cached_property
34+
2935

3036
class BlockRangeMixIn:
3137
"""override block range"""
3238

33-
@decorators.cachedproperty
39+
@cached_property
3440
def blockstart_tolineno(self):
3541
return self.lineno
3642

@@ -135,7 +141,7 @@ class MultiLineBlockMixin:
135141
Assign nodes, etc.
136142
"""
137143

138-
@decorators.cachedproperty
144+
@cached_property
139145
def _multi_line_blocks(self):
140146
return tuple(getattr(self, field) for field in self._multi_line_block_fields)
141147

astroid/nodes/node_classes.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@
8080
from astroid import nodes
8181
from astroid.nodes import LocalsDictNodeNG
8282

83+
if sys.version_info >= (3, 8) or TYPE_CHECKING:
84+
# pylint: disable-next=ungrouped-imports
85+
from functools import cached_property
86+
else:
87+
from astroid.decorators import cachedproperty as cached_property
88+
8389

8490
def _is_const(value):
8591
return isinstance(value, tuple(CONST_CLS))
@@ -824,7 +830,7 @@ def _infer_name(self, frame, name):
824830
return name
825831
return None
826832

827-
@decorators.cachedproperty
833+
@cached_property
828834
def fromlineno(self):
829835
"""The first line that this node appears on in the source code.
830836
@@ -833,7 +839,7 @@ def fromlineno(self):
833839
lineno = super().fromlineno
834840
return max(lineno, self.parent.fromlineno or 0)
835841

836-
@decorators.cachedproperty
842+
@cached_property
837843
def arguments(self):
838844
"""Get all the arguments for this node, including positional only and positional and keyword"""
839845
return list(itertools.chain((self.posonlyargs or ()), self.args or ()))
@@ -2601,7 +2607,7 @@ def postinit(
26012607
if body is not None:
26022608
self.body = body
26032609

2604-
@decorators.cachedproperty
2610+
@cached_property
26052611
def blockstart_tolineno(self):
26062612
"""The line on which the beginning of this block ends.
26072613
@@ -2734,7 +2740,7 @@ def postinit(
27342740
See astroid/protocols.py for actual implementation.
27352741
"""
27362742

2737-
@decorators.cachedproperty
2743+
@cached_property
27382744
def blockstart_tolineno(self):
27392745
"""The line on which the beginning of this block ends.
27402746
@@ -3093,7 +3099,7 @@ def postinit(
30933099
if isinstance(self.parent, If) and self in self.parent.orelse:
30943100
self.is_orelse = True
30953101

3096-
@decorators.cachedproperty
3102+
@cached_property
30973103
def blockstart_tolineno(self):
30983104
"""The line on which the beginning of this block ends.
30993105
@@ -3762,7 +3768,7 @@ def _wrap_attribute(self, attr):
37623768
return const
37633769
return attr
37643770

3765-
@decorators.cachedproperty
3771+
@cached_property
37663772
def _proxied(self):
37673773
builtins = AstroidManager().builtins_module
37683774
return builtins.getattr("slice")[0]
@@ -4384,7 +4390,7 @@ def postinit(
43844390
if orelse is not None:
43854391
self.orelse = orelse
43864392

4387-
@decorators.cachedproperty
4393+
@cached_property
43884394
def blockstart_tolineno(self):
43894395
"""The line on which the beginning of this block ends.
43904396
@@ -4500,7 +4506,7 @@ def postinit(
45004506
See astroid/protocols.py for actual implementation.
45014507
"""
45024508

4503-
@decorators.cachedproperty
4509+
@cached_property
45044510
def blockstart_tolineno(self):
45054511
"""The line on which the beginning of this block ends.
45064512

astroid/nodes/node_ng.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@
3838
else:
3939
from typing_extensions import Literal
4040

41+
if sys.version_info >= (3, 8) or TYPE_CHECKING:
42+
# pylint: disable-next=ungrouped-imports
43+
from functools import cached_property
44+
else:
45+
# pylint: disable-next=ungrouped-imports
46+
from astroid.decorators import cachedproperty as cached_property
4147

4248
# Types for 'NodeNG.nodes_of_class()'
4349
T_Nodes = TypeVar("T_Nodes", bound="NodeNG")
@@ -435,14 +441,14 @@ def previous_sibling(self):
435441
# these are lazy because they're relatively expensive to compute for every
436442
# single node, and they rarely get looked at
437443

438-
@decorators.cachedproperty
444+
@cached_property
439445
def fromlineno(self) -> Optional[int]:
440446
"""The first line that this node appears on in the source code."""
441447
if self.lineno is None:
442448
return self._fixed_source_line()
443449
return self.lineno
444450

445-
@decorators.cachedproperty
451+
@cached_property
446452
def tolineno(self) -> Optional[int]:
447453
"""The last line that this node appears on in the source code."""
448454
if self.end_lineno is not None:

astroid/nodes/scoped_nodes/scoped_nodes.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
import sys
5353
import typing
5454
import warnings
55-
from typing import Dict, List, Optional, Set, TypeVar, Union, overload
55+
from typing import TYPE_CHECKING, Dict, List, Optional, Set, TypeVar, Union, overload
5656

5757
from astroid import bases
5858
from astroid import decorators as decorators_mod
@@ -93,6 +93,12 @@
9393
else:
9494
from typing_extensions import Literal
9595

96+
if sys.version_info >= (3, 8) or TYPE_CHECKING:
97+
from functools import cached_property
98+
else:
99+
# pylint: disable-next=ungrouped-imports
100+
from astroid.decorators import cachedproperty as cached_property
101+
96102

97103
ITER_METHODS = ("__iter__", "__getitem__")
98104
EXCEPTION_BASE_CLASSES = frozenset({"Exception", "BaseException"})
@@ -1611,7 +1617,7 @@ def postinit(
16111617
self.position = position
16121618
self.doc_node = doc_node
16131619

1614-
@decorators_mod.cachedproperty
1620+
@cached_property
16151621
def extra_decorators(self) -> List[node_classes.Call]:
16161622
"""The extra decorators that this function can have.
16171623
@@ -1652,7 +1658,7 @@ def extra_decorators(self) -> List[node_classes.Call]:
16521658
decorators.append(assign.value)
16531659
return decorators
16541660

1655-
@decorators_mod.cachedproperty
1661+
@cached_property
16561662
def type(
16571663
self,
16581664
): # pylint: disable=invalid-overridden-method,too-many-return-statements
@@ -1726,7 +1732,7 @@ def type(
17261732
pass
17271733
return type_name
17281734

1729-
@decorators_mod.cachedproperty
1735+
@cached_property
17301736
def fromlineno(self) -> Optional[int]:
17311737
"""The first line that this node appears on in the source code."""
17321738
# lineno is the line number of the first decorator, we want the def
@@ -1739,7 +1745,7 @@ def fromlineno(self) -> Optional[int]:
17391745

17401746
return lineno
17411747

1742-
@decorators_mod.cachedproperty
1748+
@cached_property
17431749
def blockstart_tolineno(self):
17441750
"""The line on which the beginning of this block ends.
17451751
@@ -2337,7 +2343,7 @@ def _newstyle_impl(self, context=None):
23372343
doc=("Whether this is a new style class or not\n\n" ":type: bool or None"),
23382344
)
23392345

2340-
@decorators_mod.cachedproperty
2346+
@cached_property
23412347
def fromlineno(self) -> Optional[int]:
23422348
"""The first line that this node appears on in the source code."""
23432349
if not PY38_PLUS:
@@ -2352,7 +2358,7 @@ def fromlineno(self) -> Optional[int]:
23522358
return lineno
23532359
return super().fromlineno
23542360

2355-
@decorators_mod.cachedproperty
2361+
@cached_property
23562362
def blockstart_tolineno(self):
23572363
"""The line on which the beginning of this block ends.
23582364

astroid/objects.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
Call(func=Name('frozenset'), args=Tuple(...))
2323
"""
2424

25+
import sys
26+
from typing import TYPE_CHECKING
2527

26-
from astroid import bases, decorators, util
28+
from astroid import bases, util
2729
from astroid.exceptions import (
2830
AttributeInferenceError,
2931
InferenceError,
@@ -35,6 +37,11 @@
3537

3638
objectmodel = util.lazy_import("interpreter.objectmodel")
3739

40+
if sys.version_info >= (3, 8) or TYPE_CHECKING:
41+
from functools import cached_property
42+
else:
43+
from astroid.decorators import cachedproperty as cached_property
44+
3845

3946
class FrozenSet(node_classes.BaseContainer):
4047
"""class representing a FrozenSet composite node"""
@@ -45,7 +52,7 @@ def pytype(self):
4552
def _infer(self, context=None):
4653
yield self
4754

48-
@decorators.cachedproperty
55+
@cached_property
4956
def _proxied(self): # pylint: disable=method-hidden
5057
ast_builtins = AstroidManager().builtins_module
5158
return ast_builtins.getattr("frozenset")[0]
@@ -114,7 +121,7 @@ def super_mro(self):
114121
index = mro.index(self.mro_pointer)
115122
return mro[index + 1 :]
116123

117-
@decorators.cachedproperty
124+
@cached_property
118125
def _proxied(self):
119126
ast_builtins = AstroidManager().builtins_module
120127
return ast_builtins.getattr("super")[0]
@@ -218,7 +225,7 @@ class ExceptionInstance(bases.Instance):
218225
the case of .args.
219226
"""
220227

221-
@decorators.cachedproperty
228+
@cached_property
222229
def special_attributes(self):
223230
qname = self.qname()
224231
instance = objectmodel.BUILTIN_EXCEPTIONS.get(

tests/unittest_decorators.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import pytest
22
from _pytest.recwarn import WarningsRecorder
33

4-
from astroid.decorators import deprecate_default_argument_values
4+
from astroid.const import PY38_PLUS
5+
from astroid.decorators import cachedproperty, deprecate_default_argument_values
56

67

78
class SomeClass:
@@ -97,3 +98,18 @@ def test_deprecated_default_argument_values_ok(recwarn: WarningsRecorder) -> Non
9798
instance = SomeClass(name="some_name")
9899
instance.func(name="", var=42)
99100
assert len(recwarn) == 0
101+
102+
103+
@pytest.mark.skipif(not PY38_PLUS, reason="Requires Python 3.8 or higher")
104+
def test_deprecation_warning_on_cachedproperty() -> None:
105+
"""Check the DeprecationWarning on cachedproperty."""
106+
107+
with pytest.warns(DeprecationWarning) as records:
108+
109+
class MyClass: # pylint: disable=unused-variable
110+
@cachedproperty
111+
def my_property(self):
112+
return 1
113+
114+
assert len(records) == 1
115+
assert "functools.cached_property" in records[0].message.args[0]

0 commit comments

Comments
 (0)