diff --git a/README.rst b/README.rst index 15ced9a..789951b 100644 --- a/README.rst +++ b/README.rst @@ -200,6 +200,11 @@ or ``raise ... from None`` to distinguish them from errors in exception handling See `the exception chaining tutorial `_ for details. +**B905**: ``zip()`` without an explicit `strict=` parameter set. ``strict=True`` causes the resulting iterator +to raise a ``ValueError`` if the arguments are exhausted at differing lengths. The ``strict=`` argument +was added in Python 3.10, so don't enable this flag for code that should work on <3.10. +For more information: https://peps.python.org/pep-0618/ + **B950**: Line too long. This is a pragmatic equivalent of ``pycodestyle``'s ``E501``: it considers "max-line-length" but only triggers when the value has been exceeded by **more than 10%**. You will no @@ -299,7 +304,8 @@ Change Log Future ~~~~~~~~~ -* B027: ignore @overload when typing is import with other names +* B027: ignore @overload when typing is imported with other names +* Add B905: `zip()` without an explicit `strict=` parameter. 22.10.27 ~~~~~~~~~ diff --git a/bugbear.py b/bugbear.py index db4e0c0..3344204 100644 --- a/bugbear.py +++ b/bugbear.py @@ -356,6 +356,7 @@ def visit_Call(self, node): self.check_for_b026(node) + self.check_for_b905(node) self.generic_visit(node) def visit_Assign(self, node): @@ -960,6 +961,14 @@ def check_for_b025(self, node): for duplicate in duplicates: self.errors.append(B025(node.lineno, node.col_offset, vars=(duplicate,))) + def check_for_b905(self, node): + if ( + isinstance(node.func, ast.Name) + and node.func.id == "zip" + and not any(kw.arg == "strict" for kw in node.keywords) + ): + self.errors.append(B905(node.lineno, node.col_offset)) + def compose_call_path(node): if isinstance(node, ast.Attribute): @@ -1360,6 +1369,8 @@ def visit_Lambda(self, node): ) ) +B905 = Error(message="B905 `zip()` without an explicit `strict=` parameter.") + B950 = Error(message="B950 line too long ({} > {} characters)") -disabled_by_default = ["B901", "B902", "B903", "B904", "B950"] +disabled_by_default = ["B901", "B902", "B903", "B904", "B905", "B950"] diff --git a/tests/b905_py310.py b/tests/b905_py310.py new file mode 100644 index 0000000..1a01b5e --- /dev/null +++ b/tests/b905_py310.py @@ -0,0 +1,10 @@ +zip() +zip(range(3)) +zip("a", "b") +zip("a", "b", *zip("c")) +zip(zip("a"), strict=False) +zip(zip("a", strict=True)) + +zip(range(3), strict=True) +zip("a", "b", strict=False) +zip("a", "b", "c", strict=True) diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py index 7773221..bfd536c 100644 --- a/tests/test_bugbear.py +++ b/tests/test_bugbear.py @@ -42,6 +42,7 @@ B902, B903, B904, + B905, B950, BugBearChecker, BugBearVisitor, @@ -297,7 +298,7 @@ def test_b019(self): def test_b020(self): filename = Path(__file__).absolute().parent / "b020.py" bbc = BugBearChecker(filename=str(filename)) - errors = list(bbc.run()) + errors = list(e for e in bbc.run() if e[2][:4] == "B020") self.assertEqual( errors, self.errors( @@ -484,6 +485,22 @@ def test_b904(self): ] self.assertEqual(errors, self.errors(*expected)) + @unittest.skipIf(sys.version_info < (3, 10), "requires 3.10+") + def test_b905(self): + filename = Path(__file__).absolute().parent / "b905_py310.py" + bbc = BugBearChecker(filename=str(filename)) + errors = list(bbc.run()) + expected = [ + B905(1, 0), + B905(2, 0), + B905(3, 0), + B905(4, 0), + B905(4, 15), + B905(5, 4), + B905(6, 0), + ] + self.assertEqual(errors, self.errors(*expected)) + def test_b950(self): filename = Path(__file__).absolute().parent / "b950.py" bbc = BugBearChecker(filename=str(filename))