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 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.rst
Expand Up @@ -160,6 +160,8 @@ 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**: Argument unpacking after keyword argument.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**B026**: Argument unpacking after keyword argument.
**B026**: Argument unpacking after keyword argument is strongly discouraged. This behavior is uncommon and can lead to unnecessary confusion when reading the code thus making it prone to causing bugs. There was cpython discussion of disallowing but legacy usage and parser limitations make it difficult.

+1 on more description here. A suggestion that I'm more than happy to have reworded + made better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**B026**: Argument unpacking after keyword argument.
**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.

I think this is compact enough to also use as the runtime message, and explains specifically what we think is wrong (the mismatched order can be misleading) rather than non-actionable historical details.

I do like the historical details and I'd be happy to keep "There was cpython discussion of disallowing this syntax, but legacy usage and parser limitations make it difficult." with that link in the readme too, but not print it at runtime.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super happy about the wording and needed to read it multiple times to understand that it indeed correctly explains the issue. Still, I have no better way so what gives. This is really hard to explain without an example TBH.

+1 on keeping the historical details to the README.


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

Expand Down
16 changes: 16 additions & 0 deletions bugbear.py
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,19 @@ 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):
try:
pmeier marked this conversation as resolved.
Show resolved Hide resolved
starred = next(arg for arg in call.args if isinstance(arg, ast.Starred))
except StopIteration:
return

if any(
(keyword.value.lineno, keyword.value.col_offset)
< (starred.lineno, starred.col_offset)
for keyword in call.keywords
):
pmeier marked this conversation as resolved.
Show resolved Hide resolved
self.errors.append(B026(call.lineno, call.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 +1218,7 @@ def visit_Lambda(self, node):
" will be considered and all other except catches can be safely removed."
)
)
B026 = Error(message="B026 Argument unpacking after keyword argument.")

# Warnings disabled by default.
B901 = Error(
Expand Down
16 changes: 16 additions & 0 deletions tests/b026.py
@@ -0,0 +1,16 @@
"""
Should emit:
B026 - on lines 14, 15, 16
"""


def foo(bar, baz, bam):
pass


foo("bar", "baz", bam="bam")
foo("bar", baz="baz", bam="bam")
foo(bar="bar", baz="baz", bam="bam")
foo(bam="bam", *["bar", "baz"])
foo(baz="baz", bam="bam", *["bar"])
foo(bar="bar", baz="baz", bam="bam", *[])
pmeier marked this conversation as resolved.
Show resolved Hide resolved
14 changes: 14 additions & 0 deletions tests/test_bugbear.py
Expand Up @@ -36,6 +36,7 @@
B023,
B024,
B025,
B026,
B901,
B902,
B903,
Expand Down Expand Up @@ -381,6 +382,19 @@ 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(14, 0),
B026(15, 0),
B026(16, 0),
),
)

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