Skip to content

Commit 994f3dd

Browse files
authored
Add B911: itertools.batched without strict= (#502)
1 parent 108bba4 commit 994f3dd

File tree

4 files changed

+61
-0
lines changed

4 files changed

+61
-0
lines changed

README.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,10 @@ This is meant to be enabled by developers writing visitors using the ``ast`` mod
258258

259259
**B910**: Use Counter() instead of defaultdict(int) to avoid excessive memory use as the default dict will record missing keys with the default value when accessed.
260260

261+
**B911**: ``itertools.batched()`` without an explicit `strict=` parameter set. ``strict=True`` causes the resulting iterator to raise a ``ValueError`` if the final batch is shorter than ``n``.
262+
263+
The ``strict=`` argument was added in Python 3.13, so don't enable this flag for code that should work on <3.13.
264+
261265
**B950**: Line too long. This is a pragmatic equivalent of
262266
``pycodestyle``'s ``E501``: it considers "max-line-length" but only triggers
263267
when the value has been exceeded by **more than 10%**. ``noqa`` and ``type: ignore`` comments are ignored. You will no

bugbear.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,7 @@ def visit_Call(self, node) -> None:
515515
self.check_for_b039(node)
516516
self.check_for_b905(node)
517517
self.check_for_b910(node)
518+
self.check_for_b911(node)
518519

519520
# no need for copying, if used in nested calls it will be set to None
520521
current_b040_caught_exception = self.b040_caught_exception
@@ -1757,6 +1758,18 @@ def check_for_b910(self, node: ast.Call) -> None:
17571758
):
17581759
self.errors.append(B910(node.lineno, node.col_offset))
17591760

1761+
def check_for_b911(self, node: ast.Call) -> None:
1762+
if (
1763+
(isinstance(node.func, ast.Name) and node.func.id == "batched")
1764+
or (
1765+
isinstance(node.func, ast.Attribute)
1766+
and node.func.attr == "batched"
1767+
and isinstance(node.func.value, ast.Name)
1768+
and node.func.value.id == "itertools"
1769+
)
1770+
) and not any(kw.arg == "strict" for kw in node.keywords):
1771+
self.errors.append(B911(node.lineno, node.col_offset))
1772+
17601773

17611774
def compose_call_path(node):
17621775
if isinstance(node, ast.Attribute):
@@ -2436,6 +2449,9 @@ def visit_Lambda(self, node) -> None:
24362449
B910 = Error(
24372450
message="B910 Use Counter() instead of defaultdict(int) to avoid excessive memory use"
24382451
)
2452+
B911 = Error(
2453+
message="B911 `itertools.batched()` without an explicit `strict=` parameter."
2454+
)
24392455
B950 = Error(message="B950 line too long ({} > {} characters)")
24402456

24412457

@@ -2449,5 +2465,6 @@ def visit_Lambda(self, node) -> None:
24492465
"B908",
24502466
"B909",
24512467
"B910",
2468+
"B911",
24522469
"B950",
24532470
]

tests/b911_py313.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import itertools
2+
from itertools import batched
3+
4+
# Expect B911
5+
batched(range(3), 2)
6+
batched(range(3), n=2)
7+
batched(iterable=range(3), n=2)
8+
itertools.batched(range(3), 2)
9+
itertools.batched(range(3), n=2)
10+
itertools.batched(iterable=range(3), n=2)
11+
12+
# OK
13+
batched(range(3), 2, strict=True)
14+
batched(range(3), n=2, strict=True)
15+
batched(iterable=range(3), n=2, strict=True)
16+
batched(range(3), 2, strict=False)
17+
batched(range(3), n=2, strict=False)
18+
batched(iterable=range(3), n=2, strict=False)
19+
itertools.batched(range(3), 2, strict=True)
20+
itertools.batched(range(3), n=2, strict=True)
21+
itertools.batched(iterable=range(3), n=2, strict=True)
22+
itertools.batched(range(3), 2, strict=False)
23+
itertools.batched(range(3), n=2, strict=False)
24+
itertools.batched(iterable=range(3), n=2, strict=False)

tests/test_bugbear.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
B908,
6060
B909,
6161
B910,
62+
B911,
6263
B950,
6364
BugBearChecker,
6465
BugBearVisitor,
@@ -1074,6 +1075,21 @@ def test_b910(self):
10741075
]
10751076
self.assertEqual(errors, self.errors(*expected))
10761077

1078+
@unittest.skipIf(sys.version_info < (3, 13), "requires 3.13+")
1079+
def test_b911(self):
1080+
filename = Path(__file__).absolute().parent / "b911_py313.py"
1081+
bbc = BugBearChecker(filename=str(filename))
1082+
errors = list(bbc.run())
1083+
expected = [
1084+
B911(5, 0),
1085+
B911(6, 0),
1086+
B911(7, 0),
1087+
B911(8, 0),
1088+
B911(9, 0),
1089+
B911(10, 0),
1090+
]
1091+
self.assertEqual(errors, self.errors(*expected))
1092+
10771093

10781094
class TestFuzz(unittest.TestCase):
10791095
from hypothesis import HealthCheck, given, settings

0 commit comments

Comments
 (0)