diff --git a/README.rst b/README.rst index 4bb4317..4b25c02 100644 --- a/README.rst +++ b/README.rst @@ -220,8 +220,11 @@ See `the exception chaining tutorial `_, `itertools.cycle `_ and `itertools.repeat `_ (with times=None) since they are infinite iterators. + +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/ **B906**: ``visit_`` function with no further call to a ``visit`` function. This is often an error, and will stop the visitor from recursing into the subnodes of a visited node. Consider adding a call ``self.generic_visit(node)`` at the end of the function. diff --git a/bugbear.py b/bugbear.py index 64d1e70..819e58e 100644 --- a/bugbear.py +++ b/bugbear.py @@ -1169,12 +1169,46 @@ 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) + @staticmethod + def _is_infinite_iterator(node: ast.expr) -> bool: + if not ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) + and node.func.value.id == "itertools" ): + return False + if node.func.attr in {"cycle", "count"}: + return True + elif node.func.attr == "repeat": + if len(node.args) == 1 and len(node.keywords) == 0: + # itertools.repeat(iterable) + return True + if ( + len(node.args) == 2 + and isinstance(node.args[1], ast.Constant) + and node.args[1].value is None + ): + # itertools.repeat(iterable, None) + return True + for kw in node.keywords: + # itertools.repeat(iterable, times=None) + if ( + kw.arg == "times" + and isinstance(kw.value, ast.Constant) + and kw.value.value is None + ): + return True + + return False + + def check_for_b905(self, node): + if not (isinstance(node.func, ast.Name) and node.func.id == "zip"): + return + for arg in node.args: + if self._is_infinite_iterator(arg): + return + if not any(kw.arg == "strict" for kw in node.keywords): self.errors.append(B905(node.lineno, node.col_offset)) def check_for_b906(self, node: ast.FunctionDef): diff --git a/tests/b905_py310.py b/tests/b905_py310.py index 1a01b5e..07f6d59 100644 --- a/tests/b905_py310.py +++ b/tests/b905_py310.py @@ -8,3 +8,15 @@ zip(range(3), strict=True) zip("a", "b", strict=False) zip("a", "b", "c", strict=True) + +# infinite iterators from itertools module should not raise errors +import itertools + +zip([1, 2, 3], itertools.cycle("ABCDEF")) +zip([1, 2, 3], itertools.count()) +zip([1, 2, 3], itertools.repeat(1)) +zip([1, 2, 3], itertools.repeat(1, None)) +zip([1, 2, 3], itertools.repeat(1, times=None)) + +zip([1, 2, 3], itertools.repeat(1, 1)) +zip([1, 2, 3], itertools.repeat(1, times=4)) diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py index e83769c..c2697e6 100644 --- a/tests/test_bugbear.py +++ b/tests/test_bugbear.py @@ -700,6 +700,8 @@ def test_b905(self): B905(4, 15), B905(5, 4), B905(6, 0), + B905(21, 0), + B905(22, 0), ] self.assertEqual(errors, self.errors(*expected))