diff --git a/ChangeLog b/ChangeLog index a8dae77624..009ea0dac1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -40,6 +40,10 @@ Release date: TBA Closes #5936 +* * Added ``cache-max-size-none`` checker will now also check ``functools.cache``. + + Closes #5670 + * ``potential-index-error``: Emitted when the index of a list or tuple exceeds its length. This checker is currently quite conservative to avoid false positives. We welcome suggestions for improvements. diff --git a/doc/data/messages/c/cache-max-size-none/bad.py b/doc/data/messages/c/cache-max-size-none/bad.py new file mode 100644 index 0000000000..f1f0c340c3 --- /dev/null +++ b/doc/data/messages/c/cache-max-size-none/bad.py @@ -0,0 +1,9 @@ +import functools + + +class Fibonnaci: + @functools.cache # [cache-max-size-none] + def fibonacci(self, n): + if n in {0, 1}: + return n + return self.fibonacci(n - 1) + self.fibonacci(n - 2) diff --git a/doc/data/messages/c/cache-max-size-none/good.py b/doc/data/messages/c/cache-max-size-none/good.py new file mode 100644 index 0000000000..6f3ca256ae --- /dev/null +++ b/doc/data/messages/c/cache-max-size-none/good.py @@ -0,0 +1,13 @@ +import functools + + +@functools.cache +def cached_fibonacci(n): + if n in {0, 1}: + return n + return cached_fibonacci(n - 1) + cached_fibonacci(n - 2) + + +class Fibonnaci: + def fibonacci(self, n): + return cached_fibonacci(n) diff --git a/doc/whatsnew/2.14.rst b/doc/whatsnew/2.14.rst index 82a9e7e1a0..c9c9680faf 100644 --- a/doc/whatsnew/2.14.rst +++ b/doc/whatsnew/2.14.rst @@ -71,6 +71,10 @@ Other Changes * The concept of checker priority has been removed. +* Added ``cache-max-size-none`` checker will now also check ``functools.cache``. + + Closes #5670 + * The ``set_config_directly`` decorator has been removed. * The ``ignore-mixin-members`` option has been deprecated. You should now use the new diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py index 6eeaa20a90..2b5484f438 100644 --- a/pylint/checkers/stdlib.py +++ b/pylint/checkers/stdlib.py @@ -429,10 +429,10 @@ class StdlibChecker(DeprecatedMixin, BaseChecker): "from code that is not actively being debugged.", ), "W1517": ( - "'lru_cache(maxsize=None)' will keep all method args alive indefinitely, including 'self'", + "'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self'", "cache-max-size-none", - "By decorating a method with lru_cache the 'self' argument will be linked to " - "the lru_cache function and therefore never garbage collected. Unless your instance " + "By decorating a method with lru_cache or cache the 'self' argument will be linked to " + "the function and therefore never garbage collected. Unless your instance " "will never need to be garbage collected (singleton) it is recommended to refactor " "code to avoid this pattern or add a maxsize to the cache." "The default value for maxsize is 128.", @@ -575,23 +575,27 @@ def _check_lru_cache_decorators(self, decorators: nodes.Decorators) -> None: try: for infered_node in d_node.infer(): q_name = infered_node.qname() - if q_name in NON_INSTANCE_METHODS or q_name not in LRU_CACHE: + if q_name in NON_INSTANCE_METHODS: return # Check if there is a maxsize argument set to None in the call - if isinstance(d_node, nodes.Call): + if q_name in LRU_CACHE and isinstance(d_node, nodes.Call): try: arg = utils.get_argument_from_call( d_node, position=0, keyword="maxsize" ) except utils.NoSuchArgumentError: - return + break if not isinstance(arg, nodes.Const) or arg.value is not None: - return + break - lru_cache_nodes.append(d_node) - break + lru_cache_nodes.append(d_node) + break + + if q_name == "functools.cache": + lru_cache_nodes.append(d_node) + break except astroid.InferenceError: pass for lru_cache_node in lru_cache_nodes: diff --git a/tests/functional/c/cache_max_size_none.txt b/tests/functional/c/cache_max_size_none.txt index 6fb063df5b..614d523ad7 100644 --- a/tests/functional/c/cache_max_size_none.txt +++ b/tests/functional/c/cache_max_size_none.txt @@ -1,7 +1,7 @@ -cache-max-size-none:25:5:25:20:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' will keep all method args alive indefinitely, including 'self':INFERENCE -cache-max-size-none:29:5:29:30:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' will keep all method args alive indefinitely, including 'self':INFERENCE -cache-max-size-none:33:5:33:38:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' will keep all method args alive indefinitely, including 'self':INFERENCE -cache-max-size-none:37:5:37:24:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' will keep all method args alive indefinitely, including 'self':INFERENCE -cache-max-size-none:42:5:42:24:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' will keep all method args alive indefinitely, including 'self':INFERENCE -cache-max-size-none:43:5:43:24:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' will keep all method args alive indefinitely, including 'self':INFERENCE -cache-max-size-none:73:5:73:40:MyClassWithMethodsAndMaxSize.my_func:'lru_cache(maxsize=None)' will keep all method args alive indefinitely, including 'self':INFERENCE +cache-max-size-none:25:5:25:20:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE +cache-max-size-none:29:5:29:30:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE +cache-max-size-none:33:5:33:38:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE +cache-max-size-none:37:5:37:24:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE +cache-max-size-none:42:5:42:24:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE +cache-max-size-none:43:5:43:24:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE +cache-max-size-none:73:5:73:40:MyClassWithMethodsAndMaxSize.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE diff --git a/tests/functional/c/cache_max_size_none_py39.py b/tests/functional/c/cache_max_size_none_py39.py new file mode 100644 index 0000000000..a86dc7424f --- /dev/null +++ b/tests/functional/c/cache_max_size_none_py39.py @@ -0,0 +1,47 @@ +"""Tests for cache-max-size-none""" +# pylint: disable=no-self-use, missing-function-docstring, reimported, too-few-public-methods +# pylint: disable=missing-class-docstring, function-redefined + +import functools +import functools as aliased_functools +from functools import cache +from functools import cache as aliased_cache + + +@cache +def my_func(param): + return param + 1 + + +class MyClassWithMethods: + @cache + @staticmethod + def my_func(param): + return param + 1 + + @cache + @classmethod + def my_func(cls, param): + return param + 1 + + @cache # [cache-max-size-none] + def my_func(self, param): + return param + 1 + + @functools.cache # [cache-max-size-none] + def my_func(self, param): + return param + 1 + + @aliased_functools.cache # [cache-max-size-none] + def my_func(self, param): + return param + 1 + + @aliased_cache # [cache-max-size-none] + def my_func(self, param): + return param + 1 + + # Check double decorating to check robustness of checker itself + @functools.lru_cache(maxsize=1) + @aliased_cache # [cache-max-size-none] + def my_func(self, param): + return param + 1 diff --git a/tests/functional/c/cache_max_size_none_py39.rc b/tests/functional/c/cache_max_size_none_py39.rc new file mode 100644 index 0000000000..15ad50f5ab --- /dev/null +++ b/tests/functional/c/cache_max_size_none_py39.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver = 3.9 diff --git a/tests/functional/c/cache_max_size_none_py39.txt b/tests/functional/c/cache_max_size_none_py39.txt new file mode 100644 index 0000000000..ed84fa5895 --- /dev/null +++ b/tests/functional/c/cache_max_size_none_py39.txt @@ -0,0 +1,5 @@ +cache-max-size-none:27:5:27:10:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE +cache-max-size-none:31:5:31:20:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE +cache-max-size-none:35:5:35:28:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE +cache-max-size-none:39:5:39:18:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE +cache-max-size-none:45:5:45:18:MyClassWithMethods.my_func:'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self':INFERENCE