Skip to content

Commit 4a92ce5

Browse files
sco1cooperlees
andauthored
Add B019 check to find cache decorators on class methods (#218)
* Add B019 check to find cache decorators on class methods * B019: Change decorator resolution approach to retain lineno Starting in Python 3.8, the function node definition's `lineno` is changed to index its `def ...` line rather than the first line where its decorators start. This causes inconsistent line numbers across Python versions for the line reported by Flake8. We can use the decorator node location instead, which provides a consistent location, and makes sense because this hits on decorators. * Update README verbiage * Prefer `extend-select` and `extend-ignore` for configuring opinionated warnings (`B9`) * Add deprecation note for Bugbear's internal handling of whether or not to emit `B9` codes * Add an example for `extend-immutable-call` specification * B9: Make Bugbear aware of flake8's `extend-select` * The code for Bugbear's built-in filtering for opinionated warnings predates the addition of `extend-select` to flake8 (`v4.0`) so it's not part of the check for explicit specification of `B9` codes. * Switch from `Mock` to `argparse.Namespace` for specifying options to tests to match the incoming type from `flake8` and avoid mocking side-effects. * Style fixes from review Co-authored-by: Cooper Ry Lees <[email protected]> Co-authored-by: Cooper Ry Lees <[email protected]>
1 parent 83a8227 commit 4a92ce5

File tree

4 files changed

+271
-37
lines changed

4 files changed

+271
-37
lines changed

README.rst

+52-33
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ flake8-bugbear
88
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
99
:target: https://github.com/psf/black
1010

11-
A plugin for Flake8 finding likely bugs and design problems in your
12-
program. Contains warnings that don't belong in pyflakes and
13-
pycodestyle::
11+
A plugin for ``flake8`` finding likely bugs and design problems in your
12+
program. Contains warnings that don't belong in ``pyflakes`` and
13+
``pycodestyle``::
1414

1515
bug·bear (bŭg′bâr′)
1616
n.
@@ -57,7 +57,7 @@ List of warnings
5757
**B001**: Do not use bare ``except:``, it also catches unexpected events
5858
like memory errors, interrupts, system exit, and so on. Prefer ``except
5959
Exception:``. If you're sure what you're doing, be explicit and write
60-
``except BaseException:``. Disable E722 to avoid duplicate warnings.
60+
``except BaseException:``. Disable ``E722`` to avoid duplicate warnings.
6161

6262
**B002**: Python does not support the unary prefix increment. Writing
6363
``++n`` is equivalent to ``+(+(n))``, which equals ``n``. You meant ``n
@@ -132,12 +132,15 @@ data available in ``ex``.
132132

133133
**B018**: Found useless expression. Either assign it to a variable or remove it.
134134

135+
**B019**: Use of ``functools.lru_cache`` or ``functools.cache`` on class methods
136+
can lead to memory leaks. The cache may retain instance references, preventing
137+
garbage collection.
138+
135139
**B020**: Loop control variable overrides iterable it iterates
136140

137141
**B021**: f-string used as docstring. This will be interpreted by python
138142
as a joined string rather than a docstring.
139143

140-
141144
Opinionated warnings
142145
~~~~~~~~~~~~~~~~~~~~
143146

@@ -170,15 +173,15 @@ See `the exception chaining tutorial <https://docs.python.org/3/tutorial/errors.
170173
for details.
171174

172175
**B950**: Line too long. This is a pragmatic equivalent of
173-
``pycodestyle``'s E501: it considers "max-line-length" but only triggers
176+
``pycodestyle``'s ``E501``: it considers "max-line-length" but only triggers
174177
when the value has been exceeded by **more than 10%**. You will no
175178
longer be forced to reformat code due to the closing parenthesis being
176179
one character too far to satisfy the linter. At the same time, if you do
177180
significantly violate the line length, you will receive a message that
178181
states what the actual limit is. This is inspired by Raymond Hettinger's
179182
`"Beyond PEP 8" talk <https://www.youtube.com/watch?v=wf-BqAjZb8M>`_ and
180183
highway patrol not stopping you if you drive < 5mph too fast. Disable
181-
E501 to avoid duplicate warnings. Like E501, this error ignores long shebangs
184+
``E501`` to avoid duplicate warnings. Like ``E501``, this error ignores long shebangs
182185
on the first line and urls or paths that are on their own line::
183186

184187
#! long shebang ignored
@@ -192,41 +195,57 @@ on the first line and urls or paths that are on their own line::
192195
How to enable opinionated warnings
193196
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
194197

195-
To enable these checks, specify a ``--select`` command-line option or
196-
``select=`` option in your config file. As of Flake8 3.0, this option
197-
is a whitelist (checks not listed are being implicitly disabled), so you
198-
have to explicitly specify all checks you want enabled. For example::
199-
200-
[flake8]
201-
max-line-length = 80
202-
max-complexity = 12
203-
...
204-
ignore = E501
205-
select = C,E,F,W,B,B901
206-
207-
Note that we're enabling the complexity checks, the PEP8 ``pycodestyle``
208-
errors and warnings, the pyflakes fatals and all default Bugbear checks.
209-
Finally, we're also specifying B901 as a check that we want enabled.
210-
Some checks might need other flake8 checks disabled - e.g. E501 must be
211-
disabled for B950 to be hit.
212-
213-
If you'd like all optional warnings to be enabled for you (future proof
214-
your config!), say ``B9`` instead of ``B901``. You will need Flake8 3.2+
215-
for this feature.
216-
217-
Note that ``pycodestyle`` also has a bunch of warnings that are disabled
218-
by default. Those get enabled as soon as there is an ``ignore =`` line
219-
in your configuration. I think this behavior is surprising so Bugbear's
198+
To enable Bugbear's opinionated checks (``B9xx``), specify an ``--extend-select``
199+
command-line option or ``extend-select=`` option in your config file
200+
(requires ``flake8 >=4.0``)::
201+
202+
[flake8]
203+
max-line-length = 80
204+
max-complexity = 12
205+
...
206+
extend-ignore = E501
207+
extend-select = B950
208+
209+
Some of Bugbear's checks require other ``flake8`` checks disabled - e.g. ``E501`` must
210+
be disabled when enabling ``B950``.
211+
212+
If you'd like all optional warnings to be enabled for you (future proof your config!),
213+
say ``B9`` instead of ``B950``. You will need ``flake8 >=3.2`` for this feature.
214+
215+
For ``flake8 <=4.0``, you will need to use the ``--select`` command-line option or
216+
``select=`` option in your config file. For ``flake8 >=3.0``, this option is a whitelist
217+
(checks not listed are implicitly disabled), so you have to explicitly specify all
218+
checks you want enabled (e.g. ``select = C,E,F,W,B,B950``).
219+
220+
The ``--extend-ignore`` command-line option and ``extend-ignore=`` config file option
221+
require ``flake8 >=3.6``. For older ``flake8`` versions, the ``--ignore`` and
222+
``ignore=`` options can be used. Using ``ignore`` will override all codes that are
223+
disabled by default from all installed linters, so you will need to specify these codes
224+
in your configuration to silence them. I think this behavior is surprising so Bugbear's
220225
opinionated warnings require explicit selection.
221226

227+
**Note:** Bugbear's enforcement of explicit opinionated warning selection is deprecated
228+
and will be removed in a future release. It is recommended to use ``extend-ignore`` and
229+
``extend-select`` in your ``flake8`` configuration to avoid implicitly altering selected
230+
and/or ignored codes.
231+
222232
Configuration
223233
-------------
224234

225235
The plugin currently has one setting:
226236

227237
``extend-immutable-calls``: Specify a list of additional immutable calls.
228238
This could be useful, when using other libraries that provide more immutable calls,
229-
beside those already handled by ``flake8-bugbear``. Calls to these method will no longer raise a ``B008`` warning.
239+
beside those already handled by ``flake8-bugbear``. Calls to these method will no longer
240+
raise a ``B008`` warning.
241+
242+
For example::
243+
244+
[flake8]
245+
max-line-length = 80
246+
max-complexity = 12
247+
...
248+
extend-immutable-calls = pathlib.Path, Path
230249

231250
Tests / Lints
232251
---------------

bugbear.py

+56-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def add_options(optmanager):
127127
help="Skip B008 test for additional immutable calls.",
128128
)
129129

130-
@lru_cache()
130+
@lru_cache() # noqa: B019
131131
def should_warn(self, code):
132132
"""Returns `True` if Bugbear should emit a particular warning.
133133
@@ -138,12 +138,17 @@ def should_warn(self, code):
138138
139139
As documented in the README, the user is expected to explicitly select
140140
the warnings.
141+
142+
NOTE: This method is deprecated and will be removed in a future release. It is
143+
recommended to use `extend-ignore` and `extend-select` in your flake8
144+
configuration to avoid implicitly altering selected and ignored codes.
141145
"""
142146
if code[:2] != "B9":
143147
# Normal warnings are safe for emission.
144148
return True
145149

146150
if self.options is None:
151+
# Without options configured, Bugbear will emit B9 but flake8 will ignore
147152
LOG.info(
148153
"Options not provided to Bugbear, optional warning %s selected.", code
149154
)
@@ -153,6 +158,13 @@ def should_warn(self, code):
153158
if code[:i] in self.options.select:
154159
return True
155160

161+
# flake8 >=4.0: Also check for codes in extend_select
162+
if (
163+
hasattr(self.options, "extend_select")
164+
and code[:i] in self.options.extend_select
165+
):
166+
return True
167+
156168
LOG.info(
157169
"Optional warning %s not present in selected warnings: %r. Not "
158170
"firing it at all.",
@@ -350,6 +362,7 @@ def visit_FunctionDef(self, node):
350362
self.check_for_b902(node)
351363
self.check_for_b006(node)
352364
self.check_for_b018(node)
365+
self.check_for_b019(node)
353366
self.check_for_b021(node)
354367
self.generic_visit(node)
355368

@@ -380,6 +393,8 @@ def compose_call_path(self, node):
380393
if isinstance(node, ast.Attribute):
381394
yield from self.compose_call_path(node.value)
382395
yield node.attr
396+
elif isinstance(node, ast.Call):
397+
yield from self.compose_call_path(node.func)
383398
elif isinstance(node, ast.Name):
384399
yield node.id
385400

@@ -509,6 +524,33 @@ def check_for_b017(self, node):
509524
):
510525
self.errors.append(B017(node.lineno, node.col_offset))
511526

527+
def check_for_b019(self, node):
528+
if (
529+
len(node.decorator_list) == 0
530+
or len(self.contexts) < 2
531+
or not isinstance(self.contexts[-2].node, ast.ClassDef)
532+
):
533+
return
534+
535+
# Preserve decorator order so we can get the lineno from the decorator node
536+
# rather than the function node (this location definition changes in Python 3.8)
537+
resolved_decorators = (
538+
".".join(self.compose_call_path(decorator))
539+
for decorator in node.decorator_list
540+
)
541+
for idx, decorator in enumerate(resolved_decorators):
542+
if decorator in {"classmethod", "staticmethod"}:
543+
return
544+
545+
if decorator in B019.caches:
546+
self.errors.append(
547+
B019(
548+
node.decorator_list[idx].lineno,
549+
node.decorator_list[idx].col_offset,
550+
)
551+
)
552+
return
553+
512554
def check_for_b020(self, node):
513555
targets = NameFinder()
514556
targets.visit(node.target)
@@ -898,6 +940,19 @@ def visit(self, node):
898940
"B018 Found useless expression. Either assign it to a variable or remove it."
899941
)
900942
)
943+
B019 = Error(
944+
message=(
945+
"B019 Use of `functools.lru_cache` or `functools.cache` on class methods "
946+
"can lead to memory leaks. The cache may retain instance references, "
947+
"preventing garbage collection."
948+
)
949+
)
950+
B019.caches = {
951+
"functools.cache",
952+
"functools.lru_cache",
953+
"cache",
954+
"lru_cache",
955+
}
901956
B020 = Error(
902957
message=(
903958
"B020 Found for loop that reassigns the iterable it is iterating "

tests/b019.py

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""
2+
Should emit:
3+
B019 - on lines 73, 77, 81, 85, 89, 93, 97, 101
4+
"""
5+
import functools
6+
from functools import cache, cached_property, lru_cache
7+
8+
9+
def some_other_cache():
10+
...
11+
12+
13+
class Foo:
14+
def __init__(self, x):
15+
self.x = x
16+
17+
def compute_method(self, y):
18+
...
19+
20+
@some_other_cache
21+
def user_cached_method(self, y):
22+
...
23+
24+
@classmethod
25+
@functools.cache
26+
def cached_classmethod(cls, y):
27+
...
28+
29+
@classmethod
30+
@cache
31+
def other_cached_classmethod(cls, y):
32+
...
33+
34+
@classmethod
35+
@functools.lru_cache
36+
def lru_cached_classmethod(cls, y):
37+
...
38+
39+
@classmethod
40+
@lru_cache
41+
def other_lru_cached_classmethod(cls, y):
42+
...
43+
44+
@staticmethod
45+
@functools.cache
46+
def cached_staticmethod(y):
47+
...
48+
49+
@staticmethod
50+
@cache
51+
def other_cached_staticmethod(y):
52+
...
53+
54+
@staticmethod
55+
@functools.lru_cache
56+
def lru_cached_staticmethod(y):
57+
...
58+
59+
@staticmethod
60+
@lru_cache
61+
def other_lru_cached_staticmethod(y):
62+
...
63+
64+
@functools.cached_property
65+
def some_cached_property(self):
66+
...
67+
68+
@cached_property
69+
def some_other_cached_property(self):
70+
...
71+
72+
# Remaining methods should emit B019
73+
@functools.cache
74+
def cached_method(self, y):
75+
...
76+
77+
@cache
78+
def another_cached_method(self, y):
79+
...
80+
81+
@functools.cache()
82+
def called_cached_method(self, y):
83+
...
84+
85+
@cache()
86+
def another_called_cached_method(self, y):
87+
...
88+
89+
@functools.lru_cache
90+
def lru_cached_method(self, y):
91+
...
92+
93+
@lru_cache
94+
def another_lru_cached_method(self, y):
95+
...
96+
97+
@functools.lru_cache()
98+
def called_lru_cached_method(self, y):
99+
...
100+
101+
@lru_cache()
102+
def another_called_lru_cached_method(self, y):
103+
...

0 commit comments

Comments
 (0)