diff --git a/ChangeLog b/ChangeLog index 539a393f47..cfe17a8b8e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -137,6 +137,14 @@ Release date: TBA Closes #5936 +* The ``cache-max-size-none`` checker has been renamed to ``method-cache-max-size-none``. + + Closes #5670 + +* The ``method-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/m/method-cache-max-size-none/bad.py b/doc/data/messages/m/method-cache-max-size-none/bad.py new file mode 100644 index 0000000000..7e70b688bc --- /dev/null +++ b/doc/data/messages/m/method-cache-max-size-none/bad.py @@ -0,0 +1,9 @@ +import functools + + +class Fibonnaci: + @functools.lru_cache(maxsize=None) # [method-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/m/method-cache-max-size-none/good.py b/doc/data/messages/m/method-cache-max-size-none/good.py new file mode 100644 index 0000000000..6f3ca256ae --- /dev/null +++ b/doc/data/messages/m/method-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 bb6cb95b1c..6dc159e099 100644 --- a/doc/whatsnew/2.14.rst +++ b/doc/whatsnew/2.14.rst @@ -172,6 +172,14 @@ Other Changes * The concept of checker priority has been removed. +* The ``cache-max-size-none`` checker has been renamed to ``method-cache-max-size-none``. + + Closes #5670 + +* The ``method-cache-max-size-none`` checker will now also check ``functools.cache``. + + Closes #5670 + * ``BaseChecker`` classes now require the ``linter`` argument to be passed. * The ``set_config_directly`` decorator has been removed. diff --git a/pylint/checkers/stdlib.py b/pylint/checkers/stdlib.py index da73fbcee5..72d5a0454d 100644 --- a/pylint/checkers/stdlib.py +++ b/pylint/checkers/stdlib.py @@ -420,15 +420,20 @@ class StdlibChecker(DeprecatedMixin, BaseChecker): "Calls to breakpoint(), sys.breakpointhook() and pdb.set_trace() should be removed " "from code that is not actively being debugged.", ), - "W1517": ( - "'lru_cache(maxsize=None)' 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 " + "W1518": ( + "'lru_cache(maxsize=None)' or 'cache' will keep all method args alive indefinitely, including 'self'", + "method-cache-max-size-none", + "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.", - {"old_names": [("W1516", "lru-cache-decorating-method")]}, + { + "old_names": [ + ("W1516", "lru-cache-decorating-method"), + ("W1517", "cache-max-size-none"), + ] + }, ), } @@ -551,7 +556,7 @@ def visit_boolop(self, node: nodes.BoolOp) -> None: for value in node.values: self._check_datetime(value) - @utils.check_messages("cache-max-size-none") + @utils.check_messages("method-cache-max-size-none") def visit_functiondef(self, node: nodes.FunctionDef) -> None: if node.decorators and isinstance(node.parent, nodes.ClassDef): self._check_lru_cache_decorators(node.decorators) @@ -563,28 +568,32 @@ 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: self.add_message( - "cache-max-size-none", + "method-cache-max-size-none", node=lru_cache_node, confidence=interfaces.INFERENCE, ) diff --git a/pylint/message/message_definition_store.py b/pylint/message/message_definition_store.py index 0c4cdb3080..252c1a3b38 100644 --- a/pylint/message/message_definition_store.py +++ b/pylint/message/message_definition_store.py @@ -55,7 +55,7 @@ def register_message(self, message: MessageDefinition) -> None: # and the arguments are relatively small in size we do not run the # risk of creating a large memory leak. # See discussion in: https://github.com/PyCQA/pylint/pull/5673 - @functools.lru_cache(maxsize=None) # pylint: disable=cache-max-size-none + @functools.lru_cache(maxsize=None) # pylint: disable=method-cache-max-size-none def get_message_definitions(self, msgid_or_symbol: str) -> list[MessageDefinition]: """Returns the Message definition for either a numeric or symbolic id. diff --git a/tests/functional/c/cache_max_size_none.txt b/tests/functional/c/cache_max_size_none.txt deleted file mode 100644 index 6fb063df5b..0000000000 --- a/tests/functional/c/cache_max_size_none.txt +++ /dev/null @@ -1,7 +0,0 @@ -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 diff --git a/tests/functional/c/cache_max_size_none.py b/tests/functional/m/method_cache_max_size_none.py similarity index 73% rename from tests/functional/c/cache_max_size_none.py rename to tests/functional/m/method_cache_max_size_none.py index e4c157ddb0..508b3d6f2a 100644 --- a/tests/functional/c/cache_max_size_none.py +++ b/tests/functional/m/method_cache_max_size_none.py @@ -1,4 +1,4 @@ -"""Tests for cache-max-size-none""" +"""Tests for method-cache-max-size-none""" # pylint: disable=no-self-use, missing-function-docstring, reimported, too-few-public-methods # pylint: disable=missing-class-docstring, function-redefined @@ -22,25 +22,25 @@ def my_func(self, param): def my_func(self, param): return param + 1 - @lru_cache(None) # [cache-max-size-none] + @lru_cache(None) # [method-cache-max-size-none] def my_func(self, param): return param + 1 - @functools.lru_cache(None) # [cache-max-size-none] + @functools.lru_cache(None) # [method-cache-max-size-none] def my_func(self, param): return param + 1 - @aliased_functools.lru_cache(None) # [cache-max-size-none] + @aliased_functools.lru_cache(None) # [method-cache-max-size-none] def my_func(self, param): return param + 1 - @aliased_cache(None) # [cache-max-size-none] + @aliased_cache(None) # [method-cache-max-size-none] def my_func(self, param): return param + 1 # Check double decorating to check robustness of checker itself - @aliased_cache(None) # [cache-max-size-none] - @aliased_cache(None) # [cache-max-size-none] + @aliased_cache(None) # [method-cache-max-size-none] + @aliased_cache(None) # [method-cache-max-size-none] def my_func(self, param): return param + 1 @@ -70,6 +70,11 @@ def my_func(self, param): def my_func(self, param): return param + 1 - @lru_cache(typed=True, maxsize=None) # [cache-max-size-none] + @lru_cache(typed=True, maxsize=None) # [method-cache-max-size-none] def my_func(self, param): return param + 1 + + +@lru_cache(maxsize=None) +def my_func(param): + return param + 1 diff --git a/tests/functional/m/method_cache_max_size_none.txt b/tests/functional/m/method_cache_max_size_none.txt new file mode 100644 index 0000000000..6a12d97ce1 --- /dev/null +++ b/tests/functional/m/method_cache_max_size_none.txt @@ -0,0 +1,7 @@ +method-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 +method-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 +method-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 +method-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 +method-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 +method-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 +method-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/m/method_cache_max_size_none_py39.py b/tests/functional/m/method_cache_max_size_none_py39.py new file mode 100644 index 0000000000..6e499e83c8 --- /dev/null +++ b/tests/functional/m/method_cache_max_size_none_py39.py @@ -0,0 +1,47 @@ +"""Tests for method-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 # [method-cache-max-size-none] + def my_func(self, param): + return param + 1 + + @functools.cache # [method-cache-max-size-none] + def my_func(self, param): + return param + 1 + + @aliased_functools.cache # [method-cache-max-size-none] + def my_func(self, param): + return param + 1 + + @aliased_cache # [method-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 # [method-cache-max-size-none] + def my_func(self, param): + return param + 1 diff --git a/tests/functional/m/method_cache_max_size_none_py39.rc b/tests/functional/m/method_cache_max_size_none_py39.rc new file mode 100644 index 0000000000..15ad50f5ab --- /dev/null +++ b/tests/functional/m/method_cache_max_size_none_py39.rc @@ -0,0 +1,2 @@ +[testoptions] +min_pyver = 3.9 diff --git a/tests/functional/m/method_cache_max_size_none_py39.txt b/tests/functional/m/method_cache_max_size_none_py39.txt new file mode 100644 index 0000000000..e364e50ef5 --- /dev/null +++ b/tests/functional/m/method_cache_max_size_none_py39.txt @@ -0,0 +1,5 @@ +method-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 +method-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 +method-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 +method-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 +method-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