Skip to content

Commit 88febe6

Browse files
author
Roman Tretiak
committed
Add async_lru and aiocache decorators to the B019 rule checker
1 parent 0d615b8 commit 88febe6

File tree

3 files changed

+545
-100
lines changed

3 files changed

+545
-100
lines changed

crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B019.py

Lines changed: 195 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
"""
2-
Should emit:
3-
B019 - on lines 73, 77, 81, 85, 89, 93, 97, 101
4-
"""
1+
import aiocache
2+
from aiocache import cached, cached_stampede, multi_cached
3+
import async_lru
4+
from async_lru import alru_cache
55
import functools
66
from functools import cache, cached_property, lru_cache
77

@@ -14,6 +14,21 @@ def some_other_cache():
1414
def compute_func(self, y):
1515
...
1616

17+
@async_lru.alru_cache
18+
async def async_lru_compute_func(self, y):
19+
...
20+
21+
@aiocache.cached
22+
async def aiocache_compute_func(self, y):
23+
...
24+
25+
@aiocache.cached_stampede
26+
async def aiocache_compute_func(self, y):
27+
...
28+
29+
@aiocache.multi_cached
30+
async def aiocache_compute_func(self, y):
31+
...
1732

1833
class Foo:
1934
def __init__(self, x):
@@ -25,6 +40,12 @@ def compute_method(self, y):
2540
@some_other_cache
2641
def user_cached_instance_method(self, y):
2742
...
43+
44+
@some_other_cache
45+
async def async_user_cached_instance_method(self, y):
46+
...
47+
48+
# functools
2849

2950
@classmethod
3051
@functools.cache
@@ -74,7 +95,93 @@ def some_cached_property(self):
7495
def some_other_cached_property(self):
7596
...
7697

98+
# async_lru
99+
100+
@classmethod
101+
@async_lru.alru_cache
102+
def alru_cached_classmethod(cls, y):
103+
...
104+
105+
@classmethod
106+
@alru_cache
107+
def other_alru_cached_classmethod(cls, y):
108+
...
109+
110+
@staticmethod
111+
@async_lru.alru_cache
112+
def alru_cached_staticmethod(y):
113+
...
114+
115+
@staticmethod
116+
@alru_cache
117+
def other_alru_cached_staticmethod(y):
118+
...
119+
120+
# aiocache
121+
122+
@classmethod
123+
@aiocache.cached
124+
def aiocache_cached_classmethod(cls, y):
125+
...
126+
127+
@classmethod
128+
@cached
129+
def other_aiocache_cached_classmethod(cls, y):
130+
...
131+
132+
@staticmethod
133+
@aiocache.cached
134+
def aiocache_cached_staticmethod(y):
135+
...
136+
137+
@staticmethod
138+
@cached
139+
def other_aiocache_cached_staticmethod(y):
140+
...
141+
142+
@classmethod
143+
@aiocache.cached_stampede
144+
def aiocache_cached_stampede_classmethod(cls, y):
145+
...
146+
147+
@classmethod
148+
@cached_stampede
149+
def other_aiocache_cached_stampede_classmethod(cls, y):
150+
...
151+
152+
@staticmethod
153+
@aiocache.cached_stampede
154+
def aiocache_cached_stampede_staticmethod(y):
155+
...
156+
157+
@staticmethod
158+
@cached_stampede
159+
def other_aiocache_cached_stampede_staticmethod(y):
160+
...
161+
162+
@classmethod
163+
@aiocache.multi_cached
164+
def aiocache_multi_cached_classmethod(cls, y):
165+
...
166+
167+
@classmethod
168+
@multi_cached
169+
def other_aiocache_multi_cached_classmethod(cls, y):
170+
...
171+
172+
@staticmethod
173+
@aiocache.multi_cached
174+
def aiocache_multi_cached_staticmethod(y):
175+
...
176+
177+
@staticmethod
178+
@multi_cached
179+
def other_aiocache_multi_cached_staticmethod(y):
180+
...
181+
77182
# Remaining methods should emit B019
183+
184+
# functools
78185
@functools.cache
79186
def cached_instance_method(self, y):
80187
...
@@ -106,6 +213,74 @@ def called_lru_cached_instance_method(self, y):
106213
@lru_cache()
107214
def another_called_lru_cached_instance_method(self, y):
108215
...
216+
217+
# async_lru
218+
219+
@async_lru.alru_cache
220+
async def alru_cached_instance_method(self, y):
221+
...
222+
223+
@alru_cache
224+
async def another_alru_cached_instance_method(self, y):
225+
...
226+
227+
@async_lru.alru_cache()
228+
async def called_alru_cached_instance_method(self, y):
229+
...
230+
231+
@alru_cache()
232+
async def another_called_alru_cached_instance_method(self, y):
233+
...
234+
235+
# aiocache
236+
237+
@aiocache.cached
238+
async def aiocache_cached_instance_method(self, y):
239+
...
240+
241+
@cached
242+
async def another_aiocache_cached_instance_method(self, y):
243+
...
244+
245+
@aiocache.cached()
246+
async def called_aiocache_cached_instance_method(self, y):
247+
...
248+
249+
@cached()
250+
async def another_called_aiocache_cached_instance_method(self, y):
251+
...
252+
253+
@aiocache.cached_stampede
254+
async def aiocache_cached_stampede_instance_method(self, y):
255+
...
256+
257+
@cached_stampede
258+
async def another_aiocache_cached_stampede_instance_method(self, y):
259+
...
260+
261+
@aiocache.cached_stampede()
262+
async def called_aiocache_cached_stampede_instance_method(self, y):
263+
...
264+
265+
@cached_stampede()
266+
async def another_cached_stampede_aiocache_cached_instance_method(self, y):
267+
...
268+
269+
@aiocache.multi_cached
270+
async def aiocache_multi_cached_instance_method(self, y):
271+
...
272+
273+
@multi_cached
274+
async def another_aiocache_multi_cached_instance_method(self, y):
275+
...
276+
277+
@aiocache.multi_cached()
278+
async def called_aiocache_multi_cached_instance_method(self, y):
279+
...
280+
281+
@multi_cached()
282+
async def another_called_aiocache_multi_cached_instance_method(self, y):
283+
...
109284

110285

111286
import enum
@@ -124,3 +299,19 @@ class Metaclass(type):
124299
@functools.lru_cache
125300
def lru_cached_instance_method_on_metaclass(cls, x: int):
126301
...
302+
303+
@async_lru.alru_cache
304+
async def alru_cached_instance_method_on_metaclass(cls, x: int):
305+
...
306+
307+
@aiocache.cached
308+
async def aiocache_cached_instance_method_on_metaclass(cls, x: int):
309+
...
310+
311+
@aiocache.cached_stampede
312+
async def aiocache_cached_stampede_instance_method_on_metaclass(cls, x: int):
313+
...
314+
315+
@aiocache.multi_cached
316+
async def aiocache_multi_cached_instance_method_on_metaclass(cls, x: int):
317+
...

crates/ruff_linter/src/rules/flake8_bugbear/rules/cached_instance_method.rs

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use ruff_diagnostics::{Diagnostic, Violation};
22
use ruff_macros::{derive_message_formats, ViolationMetadata};
33
use ruff_python_ast::helpers::map_callable;
4+
use ruff_python_ast::name::QualifiedName;
45
use ruff_python_ast::{self as ast, Expr};
56
use ruff_python_semantic::analyze::{class, function_type};
67
use ruff_python_semantic::{ScopeKind, SemanticModel};
@@ -9,17 +10,17 @@ use ruff_text_size::Ranged;
910
use crate::checkers::ast::Checker;
1011

1112
/// ## What it does
12-
/// Checks for uses of the `functools.lru_cache` and `functools.cache`
13-
/// decorators on methods.
13+
/// Checks for uses of caching decorators (e.g., `functools.lru_cache`,
14+
/// `functools.cache`) or async caching decorators (e.g., `async_lru.alru_cache`, `aiocache.cached`,
15+
/// `aiocache.cached_stampede`, `aiocache.multi_cached`) on methods.
1416
///
1517
/// ## Why is this bad?
16-
/// Using the `functools.lru_cache` and `functools.cache` decorators on methods
17-
/// can lead to memory leaks, as the global cache will retain a reference to
18-
/// the instance, preventing it from being garbage collected.
18+
/// Using cache decorators on methods can lead to memory leaks, as the global
19+
/// cache will retain a reference to the instance, preventing it from being
20+
/// garbage collected.
1921
///
2022
/// Instead, refactor the method to depend only on its arguments and not on the
21-
/// instance of the class, or use the `@lru_cache` decorator on a function
22-
/// outside of the class.
23+
/// instance of the class, or use the decorator on a function outside of the class.
2324
///
2425
/// This rule ignores instance methods on enumeration classes, as enum members
2526
/// are singletons.
@@ -61,15 +62,65 @@ use crate::checkers::ast::Checker;
6162
/// ## References
6263
/// - [Python documentation: `functools.lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache)
6364
/// - [Python documentation: `functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache)
65+
/// - [Github: `async-lru`](https://github.com/aio-libs/async-lru)
66+
/// - [Github: `aiocache`](https://github.com/aio-libs/aiocache)
6467
/// - [don't lru_cache methods!](https://www.youtube.com/watch?v=sVjtp6tGo0g)
68+
#[derive(Copy, Clone, Debug)]
69+
pub(crate)enum LruDecorator {
70+
FuncToolsLruCache,
71+
FunctoolsCache,
72+
AsyncLru,
73+
AiocacheCached,
74+
AiocacheCachedStampede,
75+
AiocacheMultiCached
76+
}
77+
78+
impl LruDecorator {
79+
fn from_qualified_name(qualified_name: &QualifiedName<'_>) -> Option<Self> {
80+
match qualified_name.segments() {
81+
["functools", "lru_cache"] => Some(Self::FuncToolsLruCache),
82+
["functools", "cache"] => Some(Self::FunctoolsCache),
83+
["async_lru", "alru_cache"] => Some(Self::AsyncLru),
84+
["aiocache", "cached"] => Some(Self::AiocacheCached),
85+
["aiocache", "cached_stampede"] => Some(Self::AiocacheCachedStampede),
86+
["aiocache", "multi_cached"] => Some(Self::AiocacheMultiCached),
87+
_ => None,
88+
}
89+
}
90+
}
91+
92+
impl std::fmt::Display for LruDecorator {
93+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
94+
match self {
95+
Self::FuncToolsLruCache => f.write_str("functools.lru_cache"),
96+
Self::FunctoolsCache => f.write_str("functools.cache"),
97+
Self::AsyncLru => f.write_str("async_lru.alru_cache"),
98+
Self::AiocacheCached => f.write_str("aiocache.cached"),
99+
Self::AiocacheCachedStampede => f.write_str("aiocache.cached_stampede"),
100+
Self::AiocacheMultiCached => f.write_str("aiocache.multi_cached"),
101+
}
102+
}
103+
}
104+
105+
65106
#[derive(ViolationMetadata)]
66-
pub(crate) struct CachedInstanceMethod;
107+
pub(crate) struct CachedInstanceMethod {
108+
decorator_name: LruDecorator,
109+
}
110+
111+
impl CachedInstanceMethod {
112+
pub(crate) fn new(decorator_name: LruDecorator) -> Self {
113+
Self { decorator_name }
114+
}
115+
}
67116

68117
impl Violation for CachedInstanceMethod {
69118
#[derive_message_formats]
70119
fn message(&self) -> String {
71-
"Use of `functools.lru_cache` or `functools.cache` on methods can lead to memory leaks"
72-
.to_string()
120+
format!(
121+
"Use of `{}` on methods can lead to memory leaks",
122+
self.decorator_name
123+
)
73124
}
74125
}
75126

@@ -96,25 +147,28 @@ pub(crate) fn cached_instance_method(checker: &Checker, function_def: &ast::Stmt
96147
}
97148

98149
for decorator in &function_def.decorator_list {
99-
if is_cache_func(map_callable(&decorator.expression), checker.semantic()) {
100-
// If we found a cached instance method, validate (lazily) that the class is not an enum.
150+
if let Some(decorator_name) =
151+
get_cache_decorator_name(map_callable(&decorator.expression), checker.semantic())
152+
{
153+
// Ignore if class is an enum (enum members are singletons).
101154
if class::is_enumeration(class_def, checker.semantic()) {
102155
return;
103156
}
104157

105-
checker.report_diagnostic(Diagnostic::new(CachedInstanceMethod, decorator.range()));
158+
checker.report_diagnostic(Diagnostic::new(
159+
CachedInstanceMethod::new(decorator_name),
160+
decorator.range(),
161+
));
106162
}
107163
}
108164
}
109165

110-
/// Returns `true` if the given expression is a call to `functools.lru_cache` or `functools.cache`.
111-
fn is_cache_func(expr: &Expr, semantic: &SemanticModel) -> bool {
112-
semantic
113-
.resolve_qualified_name(expr)
114-
.is_some_and(|qualified_name| {
115-
matches!(
116-
qualified_name.segments(),
117-
["functools", "lru_cache" | "cache"]
118-
)
119-
})
166+
/// Returns `Some(<decorator_name>)` if the given expression is one of the known
167+
/// cache decorators, otherwise `None`.
168+
fn get_cache_decorator_name(expr: &Expr, semantic: &SemanticModel) -> Option<LruDecorator> {
169+
if let Some(qualified_name) = semantic.resolve_qualified_name(expr) {
170+
LruDecorator::from_qualified_name(&qualified_name)
171+
} else {
172+
None
173+
}
120174
}

0 commit comments

Comments
 (0)