From feac7fb86f4e540200950f18d517c5766e297917 Mon Sep 17 00:00:00 2001 From: "Chris (ChrisJr404)" <11917633+ChrisJr404@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:03:30 -0400 Subject: [PATCH] Extend B019 to alru_cache and async methods B019 warns about functools.lru_cache/cache on methods because the cache holds instance references and prevents garbage collection. The same problem applies to async_lru.alru_cache, which is the async equivalent, and to plain lru_cache/cache used on async methods (which the check previously skipped because it only visited sync function defs). Add async_lru.alru_cache and alru_cache to the recognised cache decorators and run the check on async def methods too. --- README.rst | 8 ++++--- bugbear.py | 11 +++++---- tests/eval_files/b019.py | 48 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index bb92e02..51cca44 100644 --- a/README.rst +++ b/README.rst @@ -180,9 +180,10 @@ which is an unassigned tuple. Simply remove the comma to clear the error. .. _B019: -**B019**: Use of ``functools.lru_cache`` or ``functools.cache`` on methods -can lead to memory leaks. The cache may retain instance references, preventing -garbage collection. +**B019**: Use of ``functools.lru_cache``, ``functools.cache`` or +``async_lru.alru_cache`` on methods can lead to memory leaks. The cache may +retain instance references, preventing garbage collection. This is also checked +on ``async def`` methods, where ``alru_cache`` is typically used. .. _B020: @@ -499,6 +500,7 @@ Change Log UNRELEASED ~~~~~~~~~~ +* B019: also flag `async_lru.alru_cache` and check cache decorators on `async def` methods (#488) * B018: handle also useless calls such as `isinstance(x, int)` without assigning or using the result * B031: don't count a store-context reference (e.g. an annotation target like `group: T`) as a use of the `groupby` generator (#465) * B902: don't raise a false positive on a metaclass defined with a dotted base such as `abc.ABCMeta` or `enum.EnumMeta` (#411) diff --git a/bugbear.py b/bugbear.py index e7ae9c9..1b49311 100644 --- a/bugbear.py +++ b/bugbear.py @@ -647,6 +647,7 @@ def visit_Assert(self, node: ast.Assert) -> None: def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: self.check_for_b902(node) self.check_for_b006_and_b008(node) + self.check_for_b019(node) self.generic_visit(node) def visit_FunctionDef(self, node: ast.FunctionDef) -> None: @@ -952,7 +953,7 @@ def check_for_b017(self, node: ast.With) -> None: ): self.add_error("B017", node) - def check_for_b019(self, node: ast.FunctionDef) -> None: + def check_for_b019(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None: if ( len(node.decorator_list) == 0 or len(self.contexts) < 2 @@ -2283,6 +2284,8 @@ def visit_Lambda(self, node) -> None: "functools.lru_cache", "cache", "lru_cache", + "async_lru.alru_cache", + "alru_cache", } B902_IMPLICIT_CLASSMETHODS = {"__new__", "__init_subclass__", "__class_getitem__"} B902_SELF = ["self"] # it's a list because the first is preferred @@ -2437,9 +2440,9 @@ def __call__(self, lineno: int, col: int, vars: tuple[object, ...] = ()) -> erro ), "B019": Error( message=( - "B019 Use of `functools.lru_cache` or `functools.cache` on methods " - "can lead to memory leaks. The cache may retain instance references, " - "preventing garbage collection." + "B019 Use of `functools.lru_cache`, `functools.cache` or " + "`async_lru.alru_cache` on methods can lead to memory leaks. The cache " + "may retain instance references, preventing garbage collection." ) ), "B020": Error( diff --git a/tests/eval_files/b019.py b/tests/eval_files/b019.py index 445e445..869552b 100644 --- a/tests/eval_files/b019.py +++ b/tests/eval_files/b019.py @@ -1,11 +1,15 @@ """ Should emit: -B019 - on lines 73, 77, 81, 85, 89, 93, 97, 101 +B019 - on each of the cache-decorated instance methods below, both the +synchronous methods and the `async def` methods (including `alru_cache`). """ import functools from functools import cache, cached_property, lru_cache +import async_lru +from async_lru import alru_cache + def some_other_cache(): ... @@ -80,4 +84,44 @@ def another_lru_cached_method(self, y): ... def called_lru_cached_method(self, y): ... @lru_cache() # B019: 5 - def another_called_lru_cached_method(self, y): ... \ No newline at end of file + def another_called_lru_cached_method(self, y): ... + + +class AsyncFoo: + async def compute_method(self, y): ... + + @some_other_cache + async def user_cached_method(self, y): ... + + @classmethod + @alru_cache + async def cached_classmethod(cls, y): ... + + @staticmethod + @alru_cache + async def cached_staticmethod(y): ... + + # Remaining methods should emit B019 + @functools.cache # B019: 5 + async def cached_method(self, y): ... + + @cache # B019: 5 + async def another_cached_method(self, y): ... + + @functools.lru_cache # B019: 5 + async def lru_cached_method(self, y): ... + + @lru_cache # B019: 5 + async def another_lru_cached_method(self, y): ... + + @async_lru.alru_cache # B019: 5 + async def alru_cached_method(self, y): ... + + @alru_cache # B019: 5 + async def another_alru_cached_method(self, y): ... + + @async_lru.alru_cache() # B019: 5 + async def called_alru_cached_method(self, y): ... + + @alru_cache() # B019: 5 + async def another_called_alru_cached_method(self, y): ... \ No newline at end of file