Skip to content

Commit 2b4c163

Browse files
authored
B026 - Argument unpacking after keyword argument (#287)
* add B026 - Argument unpacking after keyword argument * add support for multiple unpackings * fix wording
1 parent 3f3fd33 commit 2b4c163

File tree

4 files changed

+72
-0
lines changed

4 files changed

+72
-0
lines changed

README.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,13 @@ the loop, because `late-binding closures are a classic gotcha
160160
This check identifies exception types that are specified in multiple ``except``
161161
clauses. The first specification is the only one ever considered, so all others can be removed.
162162

163+
**B026**: Star-arg unpacking after a keyword argument is strongly discouraged, because
164+
it only works when the keyword parameter is declared after all parameters supplied by
165+
the unpacked sequence, and this change of ordering can surprise and mislead readers.
166+
There was `cpython discussion of disallowing this syntax
167+
<https://github.com/python/cpython/issues/82741>`_, but legacy usage and parser
168+
limitations make it difficult.
169+
163170
Opinionated warnings
164171
~~~~~~~~~~~~~~~~~~~~
165172

bugbear.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,8 @@ def visit_Call(self, node):
354354
):
355355
self.errors.append(B010(node.lineno, node.col_offset))
356356

357+
self.check_for_b026(node)
358+
357359
self.generic_visit(node)
358360

359361
def visit_Assign(self, node):
@@ -641,6 +643,22 @@ def is_abstract_decorator(expr):
641643

642644
self.errors.append(B024(node.lineno, node.col_offset, vars=(node.name,)))
643645

646+
def check_for_b026(self, call: ast.Call):
647+
if not call.keywords:
648+
return
649+
650+
starreds = [arg for arg in call.args if isinstance(arg, ast.Starred)]
651+
if not starreds:
652+
return
653+
654+
first_keyword = call.keywords[0].value
655+
for starred in starreds:
656+
if (starred.lineno, starred.col_offset) > (
657+
first_keyword.lineno,
658+
first_keyword.col_offset,
659+
):
660+
self.errors.append(B026(starred.lineno, starred.col_offset))
661+
644662
def _get_assigned_names(self, loop_node):
645663
loop_targets = (ast.For, ast.AsyncFor, ast.comprehension)
646664
for node in children_in_scope(loop_node):
@@ -1203,6 +1221,14 @@ def visit_Lambda(self, node):
12031221
" will be considered and all other except catches can be safely removed."
12041222
)
12051223
)
1224+
B026 = Error(
1225+
message=(
1226+
"B026 Star-arg unpacking after a keyword argument is strongly discouraged, "
1227+
"because it only works when the keyword parameter is declared after all "
1228+
"parameters supplied by the unpacked sequence, and this change of ordering can "
1229+
"surprise and mislead readers."
1230+
)
1231+
)
12061232

12071233
# Warnings disabled by default.
12081234
B901 = Error(

tests/b026.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""
2+
Should emit:
3+
B026 - on lines 16, 17, 18, 19, 20, 21
4+
"""
5+
6+
7+
def foo(bar, baz, bam):
8+
pass
9+
10+
11+
bar_baz = ["bar", "baz"]
12+
13+
foo("bar", "baz", bam="bam")
14+
foo("bar", baz="baz", bam="bam")
15+
foo(bar="bar", baz="baz", bam="bam")
16+
foo(bam="bam", *["bar", "baz"])
17+
foo(bam="bam", *bar_baz)
18+
foo(baz="baz", bam="bam", *["bar"])
19+
foo(bar="bar", baz="baz", bam="bam", *[])
20+
foo(bam="bam", *["bar"], *["baz"])
21+
foo(*["bar"], bam="bam", *["baz"])

tests/test_bugbear.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
B023,
3737
B024,
3838
B025,
39+
B026,
3940
B901,
4041
B902,
4142
B903,
@@ -381,6 +382,23 @@ def test_b025(self):
381382
),
382383
)
383384

385+
def test_b026(self):
386+
filename = Path(__file__).absolute().parent / "b026.py"
387+
bbc = BugBearChecker(filename=str(filename))
388+
errors = list(bbc.run())
389+
self.assertEqual(
390+
errors,
391+
self.errors(
392+
B026(16, 15),
393+
B026(17, 15),
394+
B026(18, 26),
395+
B026(19, 37),
396+
B026(20, 15),
397+
B026(20, 25),
398+
B026(21, 25),
399+
),
400+
)
401+
384402
def test_b901(self):
385403
filename = Path(__file__).absolute().parent / "b901.py"
386404
bbc = BugBearChecker(filename=str(filename))

0 commit comments

Comments
 (0)