Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

B026 - Argument unpacking after keyword argument #287

Merged
merged 4 commits into from
Sep 22, 2022
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
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ the loop, because `late-binding closures are a classic gotcha
This check identifies exception types that are specified in multiple ``except``
clauses. The first specification is the only one ever considered, so all others can be removed.

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

Opinionated warnings
~~~~~~~~~~~~~~~~~~~~

Expand Down
26 changes: 26 additions & 0 deletions bugbear.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,8 @@ def visit_Call(self, node):
):
self.errors.append(B010(node.lineno, node.col_offset))

self.check_for_b026(node)

self.generic_visit(node)

def visit_Assign(self, node):
Expand Down Expand Up @@ -641,6 +643,22 @@ def is_abstract_decorator(expr):

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

def check_for_b026(self, call: ast.Call):
if not call.keywords:
return

starreds = [arg for arg in call.args if isinstance(arg, ast.Starred)]
if not starreds:
return

first_keyword = call.keywords[0].value
for starred in starreds:
if (starred.lineno, starred.col_offset) > (
first_keyword.lineno,
first_keyword.col_offset,
):
self.errors.append(B026(starred.lineno, starred.col_offset))

def _get_assigned_names(self, loop_node):
loop_targets = (ast.For, ast.AsyncFor, ast.comprehension)
for node in children_in_scope(loop_node):
Expand Down Expand Up @@ -1203,6 +1221,14 @@ def visit_Lambda(self, node):
" will be considered and all other except catches can be safely removed."
)
)
B026 = Error(
message=(
"B026 Star-arg unpacking after a keyword argument is strongly discouraged, "
"because it only works when the keyword parameter is declared after all "
"parameters supplied by the unpacked sequence, and this change of ordering can "
"surprise and mislead readers."
)
)

# Warnings disabled by default.
B901 = Error(
Expand Down
21 changes: 21 additions & 0 deletions tests/b026.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""
Should emit:
B026 - on lines 16, 17, 18, 19, 20, 21
"""


def foo(bar, baz, bam):
pass


bar_baz = ["bar", "baz"]

foo("bar", "baz", bam="bam")
foo("bar", baz="baz", bam="bam")
foo(bar="bar", baz="baz", bam="bam")
foo(bam="bam", *["bar", "baz"])
foo(bam="bam", *bar_baz)
foo(baz="baz", bam="bam", *["bar"])
foo(bar="bar", baz="baz", bam="bam", *[])
foo(bam="bam", *["bar"], *["baz"])
foo(*["bar"], bam="bam", *["baz"])
18 changes: 18 additions & 0 deletions tests/test_bugbear.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
B023,
B024,
B025,
B026,
B901,
B902,
B903,
Expand Down Expand Up @@ -381,6 +382,23 @@ def test_b025(self):
),
)

def test_b026(self):
filename = Path(__file__).absolute().parent / "b026.py"
bbc = BugBearChecker(filename=str(filename))
errors = list(bbc.run())
self.assertEqual(
errors,
self.errors(
B026(16, 15),
B026(17, 15),
B026(18, 26),
B026(19, 37),
B026(20, 15),
B026(20, 25),
B026(21, 25),
),
)

def test_b901(self):
filename = Path(__file__).absolute().parent / "b901.py"
bbc = BugBearChecker(filename=str(filename))
Expand Down