Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 5 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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)
Expand Down
11 changes: 7 additions & 4 deletions bugbear.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
48 changes: 46 additions & 2 deletions tests/eval_files/b019.py
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +10 to +11

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be a dep for the test running? How does this not cause an error or am I forgetting we have some import mocking magic?



def some_other_cache(): ...

Expand Down Expand Up @@ -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): ...
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): ...